Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Blob:
- Fixed Stage Block responses for API version `2019-02-02` and later to return `Content-MD5` when the request supplies `Content-MD5`, matching Azure Storage while preserving CRC64 responses for requests without MD5. (issue #2394)
- Bumped the default Blob service API version to `2026-10-06`, and added `2026-10-06` to the supported Blob service API versions.
- Fixed Blob GET and HEAD requests for nonexistent blobs with `If-Match` so they now return `BlobNotFound` instead of `ConditionNotMet`, matching Azure Storage. (issue #2751)
- Accept a service SAS or user delegation SAS scoped to a blob version (signed resource `bv`), which Azurite previously refused, so a version can be read, or copied from with Copy Blob and Put Blob From URL, by a SAS URL for it. Such a SAS reaches only the version it names: it reads the version, its properties and its tags, sets its tier and tags, and deletes it, which takes the Delete version permission (`x`), and anything that would act on the base blob is refused with `AuthorizationResourceTypeMismatch`. A user delegation SAS for a blob snapshot (`bs`) is also accepted now: it signs the snapshot time, which Azurite left out of the string to sign. A snapshot SAS is likewise confined to the snapshot it names -- its content, properties, tags, block list and page ranges -- which a writable one could get past by setting the base blob's metadata, and a snapshot or version SAS is refused with a signed version before `2018-11-09`, whose signature does not bind the snapshot or version.
- Fixed Get Block List on a blob snapshot returning the base blob's uncommitted blocks with `blocklisttype=all` or `uncommitted`. A snapshot has no uncommitted blocks.
- Added opt-in, per-account Blob Versioning configured with `--accountConfigFilePath` or `--accountConfigAsJson`; Azurite preserves previous versions on supported blob writes and supports listing, reading, restoring, and deleting specific versions.
- List Blobs continuation tokens are now opaque, matching Azure Storage. Tokens are base64url encoded, versioned JSON carrying the `[name, timestamp, recordId]` tuple used to sort records, filter records after the marker, and build the next marker, which makes paging deterministic when several records share a blob name. Tokens issued by previous versions of Azurite (plain blob names) are still accepted.
- Reject cross-type Put Blob and Copy Blob replacements while a versioned blob retains a current blob or previous versions.
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,7 @@ azurite --accountConfigAsJson "{\"isBlobVersioningEnabled\":true}"
#### Limitations

- Soft delete and blob expiration are not integrated with versioning.
- SAS URIs for specific versions and version-level immutability policies are not supported.
- Version-level immutability policies are not supported.
- `include=deletedwithversions` on List Blobs is accepted but ignored. Azurite does not return `HasVersionsOnly` entries for deleted blobs that still have previous versions; use `include=versions` to list them.
- `Put Page` and `Append Block` do not create versions. Other supported writes, including `Put Blob`, `Put Block List`, `Set Blob Metadata`, and `Copy Blob`, do.
- Blob batch sub-requests use the versioning setting of the account the batch request was sent to. Azure always runs a batch's sub-requests against that account. With path-style URLs, Azurite also accepts a sub-request whose path names a different account, and runs it there with versioning treated as disabled. For example, a batch sent to `devstoreaccount1` that deletes a blob in `devstoreaccount2` permanently deletes a blob that has no version ID yet, even if `devstoreaccount2` has versioning enabled. Send each batch to the account that owns its blobs.
Expand Down Expand Up @@ -1207,7 +1207,7 @@ Detailed support matrix:
- Shared Access Signature Service Level (Not support response header override in service SAS)
- Container Public Access
- Blob Tags (preview)
- Blob versioning (Only in LokiDb instances of Azurite, which is the default. Does not support SAS URIs targeting specific blob versions)
- Blob versioning (Only in LokiDb instances of Azurite, which is the default)
- CRC-64/NVME transactional checksums (`x-ms-content-crc64`)
- Supported REST APIs

Expand Down Expand Up @@ -1257,7 +1257,6 @@ Detailed support matrix:
- Encryption Scope
- Get Page Ranges Continuation Token
- Blob Immutability Policy and Legal Hold
- SAS URIs for Blob Versions
- List Blobs `include=deletedwithversions` (accepted but ignored)

Latest version supports **2026-06-06** API version **queue** service.
Expand Down
4 changes: 1 addition & 3 deletions docs/designs/2025-12-blob-versioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,7 @@ flowchart TD

Versioning does not bypass or replace Azurite authentication. Requests still pass through the existing public access, Shared Key, account SAS, blob SAS, and optional OAuth authenticators before reaching handlers.

The current limitation is that Azurite does not generate or validate SAS URIs scoped to a specific blob version. Normal authenticated version operations continue through the existing authorization pipeline.
A service SAS or user delegation SAS scoped to one blob version (signed resource `bv`) signs the version ID in the field that a snapshot SAS signs the snapshot time in. Azurite reads that value from the request's `versionid` query parameter, so such a SAS authorizes requests for the version it names and no other. It is accepted only for the operations that act on that version -- Get Blob, Get Blob Properties, Get and Set Blob Tags, Set Blob Tier, and Delete Blob -- because every other operation would act on the base blob, which a version SAS does not grant; those answer `AuthorizationResourceTypeMismatch`. Deleting the version takes the Delete version permission (`x`), as Azure requires, rather than Delete (`d`). A version SAS signed in a format older than `2018-11-09` is refused, since those formats sign neither the resource nor the version ID.

## Error and validation behavior

Expand Down Expand Up @@ -555,7 +555,6 @@ The implementation preserves existing lease and conditional-request checks and a

- Versioning works only with LokiJS metadata, not `AZURITE_DB` SQL metadata.
- Soft delete and blob expiration are not integrated with versioning.
- Version-specific SAS URIs are not supported.
- Version-level immutability policies are not supported.
- List Blobs `include=deletedwithversions` is accepted but ignored. Azure returns one `HasVersionsOnly=true` entry per deleted blob that still has versions; Azurite returns nothing for such blobs unless `include=versions` is also requested.
- Azurite's timestamps use JavaScript time plus a local per-blob counter. They reproduce the external seven-digit shape, but not Azure's internal clock implementation.
Expand Down Expand Up @@ -619,6 +618,5 @@ The SQL metadata schema and query paths require separate migrations and parity v

- Add SQL metadata schema and query support for versions.
- Integrate versions with soft delete and lifecycle expiration.
- Add version-scoped SAS support.
- Add version-level immutability policies.
- Add automated live-Azure parity infrastructure for account-level setting transitions.
51 changes: 46 additions & 5 deletions src/blob/authentication/BlobSASAuthenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import {
} from "./IBlobSASSignatureValues";
import {
OPERATION_BLOB_SAS_BLOB_PERMISSIONS,
OPERATION_BLOB_SAS_BLOB_SNAPSHOT_PERMISSIONS,
OPERATION_BLOB_SAS_BLOB_VERSION_PERMISSIONS,
OPERATION_BLOB_SAS_CONTAINER_PERMISSIONS
} from "./OperationBlobSASPermission";

Expand Down Expand Up @@ -105,7 +107,8 @@ export default class BlobSASAuthenticator implements IAuthenticator {
if (
resource !== BlobSASResourceType.Container &&
resource !== BlobSASResourceType.Blob &&
resource !== BlobSASResourceType.BlobSnapshot
resource !== BlobSASResourceType.BlobSnapshot &&
resource !== BlobSASResourceType.BlobVersion
) {
this.logger.debug(
// tslint:disable-next-line:max-line-length
Expand Down Expand Up @@ -134,6 +137,22 @@ export default class BlobSASAuthenticator implements IAuthenticator {
return undefined;
}

// The bs and bv resources date from 2018-11-09, the first signing format
// to sign the resource and the snapshot time or version ID. An older
// format signs neither, so it could not bind the SAS to the snapshot or
// version it names.
if (
(resource === BlobSASResourceType.BlobSnapshot ||
resource === BlobSASResourceType.BlobVersion) &&
values.version < "2018-11-09"
) {
this.logger.info(
`BlobSASAuthenticator:validate() Signed resource type ${resource} needs version 2018-11-09 or later, not ${values.version}. Skip blob service SAS validation.`,
context.contextId
);
return undefined;
}

this.logger.debug(
`BlobSASAuthenticator:validate() Successfully got valid blob service SAS values from request. ${JSON.stringify(
values
Expand Down Expand Up @@ -388,17 +407,31 @@ export default class BlobSASAuthenticator implements IAuthenticator {
AUTHENTICATION_BEARERTOKEN_REQUIRED);
}

const selectsBlob =
resource === BlobSASResourceType.BlobSnapshot ||
resource === BlobSASResourceType.BlobVersion;
const blobSASPermission =
resource === BlobSASResourceType.Blob
? OPERATION_BLOB_SAS_BLOB_PERMISSIONS.get(operation)
: OPERATION_BLOB_SAS_CONTAINER_PERMISSIONS.get(operation);
resource === BlobSASResourceType.BlobVersion
? OPERATION_BLOB_SAS_BLOB_VERSION_PERMISSIONS.get(operation)
: resource === BlobSASResourceType.BlobSnapshot
? OPERATION_BLOB_SAS_BLOB_SNAPSHOT_PERMISSIONS.get(operation)
: resource === BlobSASResourceType.Blob
? OPERATION_BLOB_SAS_BLOB_PERMISSIONS.get(operation)
: OPERATION_BLOB_SAS_CONTAINER_PERMISSIONS.get(operation);

this.logger.debug(
`BlobSASAuthenticator:validate() Got permission requirements for operation ${
Operation[operation]
} - ${JSON.stringify(blobSASPermission)}`,
context.contextId
);
if (blobSASPermission === undefined && selectsBlob) {
// An operation that does not act on the snapshot or version the SAS
// names would act on the base blob, which such a SAS does not grant.
throw StorageErrorFactory.getAuthorizationResourceTypeMismatch(
context.contextId!
);
}
if (blobSASPermission === undefined) {
throw new Error(
// tslint:disable-next-line:max-line-length
Expand Down Expand Up @@ -499,7 +532,15 @@ export default class BlobSASAuthenticator implements IAuthenticator {
const contentLanguage = req.getQuery("rscl");
const contentType = req.getQuery("rsct");
const signedResource = this.decodeIfExist(req.getQuery("sr"));
const snapshot = this.decodeIfExist(req.getQuery("snapshot"));
// A SAS for a blob version signs its version ID in the field that a
// SAS for a snapshot signs its snapshot time in.
const snapshot = this.decodeIfExist(
req.getQuery(
signedResource === BlobSASResourceType.BlobVersion
? "versionid"
: "snapshot"
)
);
const encryptionScope = this.decodeIfExist(req.getQuery("ses"));
const signedObjectId = this.decodeIfExist(req.getQuery("skoid"));
const signedTenantId = this.decodeIfExist(req.getQuery("sktid"));
Expand Down
3 changes: 2 additions & 1 deletion src/blob/authentication/BlobSASResourceType.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export enum BlobSASResourceType {
Container = "c",
Blob = "b",
BlobSnapshot = "bs"
BlobSnapshot = "bs",
BlobVersion = "bv"
}
57 changes: 42 additions & 15 deletions src/blob/authentication/IBlobSASSignatureValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ export interface IBlobSASSignatureValues {
signedResource?: string;

/**
* Optional. Specifies the snapshot time. Required for version 2018-11-09 and later.
* Optional. Specifies the snapshot time, or for a SAS for a blob version
* (signed resource bv) the version ID. Required for version 2018-11-09 and
* later.
*
* @type {string}
* @memberof IBlobSASSignatureValues
Expand Down Expand Up @@ -365,8 +367,7 @@ function generateBlobSASSignature20201206(
getCanonicalName(
accountName,
blobSASSignatureValues.containerName,
resource === BlobSASResourceType.Blob ||
resource === BlobSASResourceType.BlobSnapshot
isBlobScoped(resource)
? blobSASSignatureValues.blobName
: ""
),
Expand Down Expand Up @@ -438,8 +439,7 @@ function generateBlobSASSignature20181109(
getCanonicalName(
accountName,
blobSASSignatureValues.containerName,
resource === BlobSASResourceType.Blob ||
resource === BlobSASResourceType.BlobSnapshot
isBlobScoped(resource)
? blobSASSignatureValues.blobName
: ""
),
Expand Down Expand Up @@ -507,7 +507,7 @@ function generateBlobSASSignature20150405(
getCanonicalName(
accountName,
blobSASSignatureValues.containerName,
resource === BlobSASResourceType.Blob
isBlobScoped(resource)
? blobSASSignatureValues.blobName
: ""
),
Expand Down Expand Up @@ -587,7 +587,7 @@ function generateBlobSASSignatureUDK20181109(
blobSASSignatureValues.protocol ? blobSASSignatureValues.protocol : "",
blobSASSignatureValues.version,
resource,
undefined, // blob version timestamp,
signedSnapshotTime(blobSASSignatureValues, resource),
blobSASSignatureValues.cacheControl,
blobSASSignatureValues.contentDisposition,
blobSASSignatureValues.contentEncoding,
Expand Down Expand Up @@ -651,7 +651,7 @@ function generateBlobSASSignatureWithUDK20200210(
blobSASSignatureValues.protocol ? blobSASSignatureValues.protocol : "",
blobSASSignatureValues.version,
resource,
undefined, // blob versiontimestamp,
signedSnapshotTime(blobSASSignatureValues, resource),
blobSASSignatureValues.cacheControl,
blobSASSignatureValues.contentDisposition,
blobSASSignatureValues.contentEncoding,
Expand Down Expand Up @@ -696,7 +696,7 @@ function generateBlobSASBlobSASSignatureWithUDK20201206(
getCanonicalName(
accountName,
blobSASSignatureValues.containerName,
resource === BlobSASResourceType.Blob
isBlobScoped(resource)
? blobSASSignatureValues.blobName
: ""
),
Expand All @@ -717,7 +717,7 @@ function generateBlobSASBlobSASSignatureWithUDK20201206(
blobSASSignatureValues.protocol ? blobSASSignatureValues.protocol : "",
blobSASSignatureValues.version,
resource,
undefined, // blob version timestamp,
signedSnapshotTime(blobSASSignatureValues, resource),
blobSASSignatureValues.encryptionScope,
blobSASSignatureValues.cacheControl,
blobSASSignatureValues.contentDisposition,
Expand Down Expand Up @@ -765,8 +765,7 @@ function generateBlobSASBlobSASSignatureWithUDK20260406(
getCanonicalName(
accountName,
blobSASSignatureValues.containerName,
resource === BlobSASResourceType.Blob ||
resource === BlobSASResourceType.BlobSnapshot
isBlobScoped(resource)
? blobSASSignatureValues.blobName
: ""
),
Expand All @@ -789,7 +788,7 @@ function generateBlobSASBlobSASSignatureWithUDK20260406(
blobSASSignatureValues.protocol ? blobSASSignatureValues.protocol : "",
blobSASSignatureValues.version,
resource,
undefined, // blob version timestamp,
signedSnapshotTime(blobSASSignatureValues, resource),
blobSASSignatureValues.encryptionScope,
undefined, // requestHeaders (srh), added in 2026-04-06
undefined, // requestQueryParameters (srq), added in 2026-04-06
Expand Down Expand Up @@ -837,7 +836,7 @@ function generateBlobSASBlobSASSignatureWithUDK20250705(
getCanonicalName(
accountName,
blobSASSignatureValues.containerName,
resource === BlobSASResourceType.Blob
isBlobScoped(resource)
? blobSASSignatureValues.blobName
: ""
),
Expand All @@ -860,7 +859,7 @@ function generateBlobSASBlobSASSignatureWithUDK20250705(
blobSASSignatureValues.protocol ? blobSASSignatureValues.protocol : "",
blobSASSignatureValues.version,
resource,
undefined, // blob version timestamp,
signedSnapshotTime(blobSASSignatureValues, resource),
blobSASSignatureValues.encryptionScope,
blobSASSignatureValues.cacheControl,
blobSASSignatureValues.contentDisposition,
Expand All @@ -873,6 +872,34 @@ function generateBlobSASBlobSASSignatureWithUDK20250705(
return [signature, stringToSign];
}

/**
* Whether a SAS for this resource names a blob, and so signs the blob's name
* in its canonicalized resource: a blob, a snapshot of one or a version of
* one, as against a container.
*/
function isBlobScoped(resource: BlobSASResourceType): boolean {
return (
resource === BlobSASResourceType.Blob ||
resource === BlobSASResourceType.BlobSnapshot ||
resource === BlobSASResourceType.BlobVersion
);
}

/**
* The signedSnapshotTime field of a user delegation SAS: the snapshot time of
* a SAS for a snapshot, the version ID of a SAS for a version, and empty
* otherwise.
*/
function signedSnapshotTime(
blobSASSignatureValues: IBlobSASSignatureValues,
resource: BlobSASResourceType
): string | undefined {
return resource === BlobSASResourceType.BlobSnapshot ||
resource === BlobSASResourceType.BlobVersion
? blobSASSignatureValues.snapshot
: undefined;
}

function getCanonicalName(
accountName: string,
containerName: string,
Expand Down
74 changes: 74 additions & 0 deletions src/blob/authentication/OperationBlobSASPermission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,80 @@ OPERATION_BLOB_SAS_BLOB_PERMISSIONS.set(
new OperationBlobSASPermission(BlobSASPermission.Tag)
);

// Blob Service SAS Permissions for a blob version (signed resource bv).
// Only the operations whose handlers act on the version that the request's
// versionid names are listed: a version SAS grants access to that version and
// not to the base blob, and every other operation would act on the base blob.
// Deleting a version takes the Delete version permission, x, rather than d.
// See https://learn.microsoft.com/en-us/azure/storage/blobs/versioning-overview#shared-access-signature-sas-parameters
export const OPERATION_BLOB_SAS_BLOB_VERSION_PERMISSIONS = new Map<
Operation,
OperationBlobSASPermission
>();

OPERATION_BLOB_SAS_BLOB_VERSION_PERMISSIONS.set(
Operation.Blob_Download,
new OperationBlobSASPermission(BlobSASPermission.Read)
);
OPERATION_BLOB_SAS_BLOB_VERSION_PERMISSIONS.set(
Operation.Blob_GetProperties,
new OperationBlobSASPermission(BlobSASPermission.Read)
);
OPERATION_BLOB_SAS_BLOB_VERSION_PERMISSIONS.set(
Operation.Blob_Delete,
new OperationBlobSASPermission(BlobSASPermission.DeleteVersion)
);
OPERATION_BLOB_SAS_BLOB_VERSION_PERMISSIONS.set(
Operation.Blob_SetTier,
new OperationBlobSASPermission(BlobSASPermission.Write)
);
OPERATION_BLOB_SAS_BLOB_VERSION_PERMISSIONS.set(
Operation.Blob_SetTags,
new OperationBlobSASPermission(BlobSASPermission.Tag)
);
OPERATION_BLOB_SAS_BLOB_VERSION_PERMISSIONS.set(
Operation.Blob_GetTags,
new OperationBlobSASPermission(BlobSASPermission.Tag)
);

// Blob Service SAS Permissions for a blob snapshot (signed resource bs), on
// the same reasoning: only the operations whose handlers act on the snapshot
// that the request's snapshot parameter names. Set Blob Tier is left out, as
// its handler acts on the base blob whatever snapshot is named.
export const OPERATION_BLOB_SAS_BLOB_SNAPSHOT_PERMISSIONS = new Map<
Operation,
OperationBlobSASPermission
>();
Comment thread
gaul marked this conversation as resolved.
Resolved

OPERATION_BLOB_SAS_BLOB_SNAPSHOT_PERMISSIONS.set(
Operation.Blob_Download,
new OperationBlobSASPermission(BlobSASPermission.Read)
);
OPERATION_BLOB_SAS_BLOB_SNAPSHOT_PERMISSIONS.set(
Operation.Blob_GetProperties,
new OperationBlobSASPermission(BlobSASPermission.Read)
);
OPERATION_BLOB_SAS_BLOB_SNAPSHOT_PERMISSIONS.set(
Operation.Blob_Delete,
new OperationBlobSASPermission(BlobSASPermission.Delete)
);
OPERATION_BLOB_SAS_BLOB_SNAPSHOT_PERMISSIONS.set(
Operation.BlockBlob_GetBlockList,
new OperationBlobSASPermission(BlobSASPermission.Read)
);
OPERATION_BLOB_SAS_BLOB_SNAPSHOT_PERMISSIONS.set(
Operation.PageBlob_GetPageRanges,
new OperationBlobSASPermission(BlobSASPermission.Read)
);
OPERATION_BLOB_SAS_BLOB_SNAPSHOT_PERMISSIONS.set(
Operation.Blob_SetTags,
new OperationBlobSASPermission(BlobSASPermission.Tag)
);
OPERATION_BLOB_SAS_BLOB_SNAPSHOT_PERMISSIONS.set(
Operation.Blob_GetTags,
new OperationBlobSASPermission(BlobSASPermission.Tag)
);

// Blob Service SAS Permissions for container level
export const OPERATION_BLOB_SAS_CONTAINER_PERMISSIONS = new Map<
Operation,
Expand Down
Loading