Skip to content

Commit

Permalink
Filter by tags (#2311)
Browse files Browse the repository at this point in the history
* Filter blob by tags

* Added cases for filter/Tag permission check

* Resove merge conflict
  • Loading branch information
EmmaZhu authored Oct 23, 2024
1 parent f2e1619 commit 1e7d69b
Show file tree
Hide file tree
Showing 42 changed files with 3,353 additions and 281 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -981,7 +981,8 @@ Detailed support matrix:
- OAuth authentication
- Shared Access Signature Account Level
- Shared Access Signature Service Level (Not support response header override in service SAS)
- Container Public Access
- Container Public Access
- Blob Tags (preview)
- Supported REST APIs
- List Containers
- Set Service Properties
Expand Down Expand Up @@ -1017,7 +1018,6 @@ Detailed support matrix:
- Soft delete & Undelete Container
- Soft delete & Undelete Blob
- Incremental Copy Blob
- Blob Tags
- Blob Query
- Blob Versions
- Blob Last Access Time
Expand Down
1 change: 1 addition & 0 deletions src/blob/authentication/ContainerSASPermissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ export enum ContainerSASPermission {
Write = "w",
Delete = "d",
List = "l",
Filter = "f",
Any = "AnyPermission" // This is only for blob batch operation.
}
4 changes: 4 additions & 0 deletions src/blob/authentication/OperationBlobSASPermission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,10 @@ OPERATION_BLOB_SAS_CONTAINER_PERMISSIONS.set(
Operation.Container_ListBlobFlatSegment,
new OperationBlobSASPermission(ContainerSASPermission.List)
);
OPERATION_BLOB_SAS_CONTAINER_PERMISSIONS.set(
Operation.Container_FilterBlobs,
new OperationBlobSASPermission(ContainerSASPermission.Filter)
);
OPERATION_BLOB_SAS_CONTAINER_PERMISSIONS.set(
Operation.Container_ListBlobHierarchySegment,
new OperationBlobSASPermission(ContainerSASPermission.List)
Expand Down
10 changes: 9 additions & 1 deletion src/blob/conditions/ConditionResourceAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { BlobModel, ContainerModel } from "../persistence/IBlobMetadataStore";
import { BlobModel, ContainerModel, FilterBlobModel } from "../persistence/IBlobMetadataStore";
import IConditionResource from "./IConditionResource";

export default class ConditionResourceAdapter implements IConditionResource {
public exist: boolean;
public etag: string;
public lastModified: Date;
public blobItemWithTags?: FilterBlobModel;

public constructor(resource: BlobModel | ContainerModel | undefined | null) {
if (
Expand Down Expand Up @@ -33,5 +34,12 @@ export default class ConditionResourceAdapter implements IConditionResource {

this.lastModified = new Date(resource.properties.lastModified);
this.lastModified.setMilliseconds(0); // Precision to seconds

const blobItem = resource as BlobModel;
this.blobItemWithTags = {
name: blobItem.name,
containerName: blobItem.containerName,
tags: blobItem.blobTags
};
}
}
3 changes: 3 additions & 0 deletions src/blob/conditions/ConditionalHeadersAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default class ConditionalHeadersAdapter implements IConditionalHeaders {
public ifUnmodifiedSince?: Date;
public ifMatch?: string[];
public ifNoneMatch?: string[];
public ifTags?: string;

public constructor(
context: Context,
Expand Down Expand Up @@ -43,5 +44,7 @@ export default class ConditionalHeadersAdapter implements IConditionalHeaders {
if (this.ifUnmodifiedSince) {
this.ifUnmodifiedSince.setMilliseconds(0); // Precision to seconds
}

this.ifTags = modifiedAccessConditions.ifTags;
}
}
3 changes: 3 additions & 0 deletions src/blob/conditions/IConditionResource.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { FilterBlobModel } from "../persistence/IBlobMetadataStore";

export default interface IConditionResource {
/**
* Whether resource exists or not.
Expand All @@ -13,4 +15,5 @@ export default interface IConditionResource {
* last modified time for container or blob.
*/
lastModified: Date;
blobItemWithTags?: FilterBlobModel;
}
5 changes: 5 additions & 0 deletions src/blob/conditions/IConditionalHeaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ export interface IConditionalHeaders {
* If-None-Match etag list without quotes.
*/
ifNoneMatch?: string[];

/**
* Specify a SQL where clause on blob tags to operate only on blobs with a matching value.
*/
ifTags?: string;
}
3 changes: 2 additions & 1 deletion src/blob/conditions/IConditionalHeadersValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface IConditionalHeadersValidator {
validate(
context: Context,
conditionalHeaders: IConditionalHeaders,
resource: IConditionResource
resource: IConditionResource,
isSourceBlob?: boolean
): void;
}
23 changes: 19 additions & 4 deletions src/blob/conditions/ReadConditionalHeadersValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import StorageErrorFactory from "../errors/StorageErrorFactory";
import { ModifiedAccessConditions } from "../generated/artifacts/models";
import Context from "../generated/Context";
import { BlobModel, ContainerModel } from "../persistence/IBlobMetadataStore";
import { generateQueryBlobWithTagsWhereFunction } from "../persistence/QueryInterpreter/QueryInterpreter";
import ConditionalHeadersAdapter from "./ConditionalHeadersAdapter";
import ConditionResourceAdapter from "./ConditionResourceAdapter";
import { IConditionalHeaders } from "./IConditionalHeaders";
Expand All @@ -11,12 +12,14 @@ import IConditionResource from "./IConditionResource";
export function validateReadConditions(
context: Context,
conditionalHeaders?: ModifiedAccessConditions,
model?: BlobModel | ContainerModel | null
model?: BlobModel | ContainerModel | null,
isSourceBlob?: boolean
) {
new ReadConditionalHeadersValidator().validate(
context,
new ConditionalHeadersAdapter(context, conditionalHeaders),
new ConditionResourceAdapter(model)
new ConditionResourceAdapter(model),
isSourceBlob
);
}

Expand All @@ -30,11 +33,13 @@ export default class ReadConditionalHeadersValidator
* @param context
* @param conditionalHeaders
* @param resource
* @param isSourceBlob
*/
public validate(
context: Context,
conditionalHeaders: IConditionalHeaders,
resource: IConditionResource
resource: IConditionResource,
isSourceBlob?: boolean
): void {
// If-Match && If-Unmodified-Since && (If-None-Match || If-Modified-Since)

Expand Down Expand Up @@ -66,7 +71,7 @@ export default class ReadConditionalHeadersValidator
// If-Match
const ifMatchPass = conditionalHeaders.ifMatch
? conditionalHeaders.ifMatch.includes(resource.etag) ||
conditionalHeaders.ifMatch[0] === "*"
conditionalHeaders.ifMatch[0] === "*"
: undefined;

// If-Unmodified-Since
Expand Down Expand Up @@ -107,6 +112,16 @@ export default class ReadConditionalHeadersValidator
if (isModifiedSincePass === false && ifNoneMatchPass !== true) {
throw StorageErrorFactory.getNotModified(context.contextId!);
}

if (conditionalHeaders.ifTags) {
const againstSourceBlob = isSourceBlob === undefined ? false : isSourceBlob;
const validateFunction = generateQueryBlobWithTagsWhereFunction(context, conditionalHeaders.ifTags, againstSourceBlob ? 'x-ms-source-if-tags' : 'x-ms-if-tags');

if (conditionalHeaders?.ifTags !== undefined
&& validateFunction(resource.blobItemWithTags).length === 0) {
throw StorageErrorFactory.getConditionNotMet(context.contextId!);
}
}
}
}
}
16 changes: 13 additions & 3 deletions src/blob/conditions/WriteConditionalHeadersValidator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from "../generated/artifacts/models";
import Context from "../generated/Context";
import { BlobModel, ContainerModel } from "../persistence/IBlobMetadataStore";
import { generateQueryBlobWithTagsWhereFunction } from "../persistence/QueryInterpreter/QueryInterpreter";
import ConditionalHeadersAdapter from "./ConditionalHeadersAdapter";
import ConditionResourceAdapter from "./ConditionResourceAdapter";
import { IConditionalHeaders } from "./IConditionalHeaders";
Expand All @@ -29,7 +30,7 @@ export function validateSequenceNumberWriteConditions(
if (
conditionalHeaders.ifSequenceNumberLessThanOrEqualTo !== undefined &&
conditionalHeaders.ifSequenceNumberLessThanOrEqualTo <
model.properties.blobSequenceNumber
model.properties.blobSequenceNumber
) {
throw StorageErrorFactory.getSequenceNumberConditionNotMet(
context.contextId!
Expand All @@ -39,7 +40,7 @@ export function validateSequenceNumberWriteConditions(
if (
conditionalHeaders.ifSequenceNumberLessThan !== undefined &&
conditionalHeaders.ifSequenceNumberLessThan <=
model.properties.blobSequenceNumber
model.properties.blobSequenceNumber
) {
throw StorageErrorFactory.getSequenceNumberConditionNotMet(
context.contextId!
Expand All @@ -49,7 +50,7 @@ export function validateSequenceNumberWriteConditions(
if (
conditionalHeaders.ifSequenceNumberEqualTo !== undefined &&
conditionalHeaders.ifSequenceNumberEqualTo !==
model.properties.blobSequenceNumber
model.properties.blobSequenceNumber
) {
throw StorageErrorFactory.getSequenceNumberConditionNotMet(
context.contextId!
Expand Down Expand Up @@ -167,6 +168,15 @@ export default class WriteConditionalHeadersValidator
}
return;
}

if (conditionalHeaders.ifTags) {
const validateFunction = generateQueryBlobWithTagsWhereFunction(context, conditionalHeaders.ifTags, 'x-ms-if-tags');

if (conditionalHeaders?.ifTags !== undefined
&& validateFunction(resource.blobItemWithTags).length === 0) {
throw StorageErrorFactory.getConditionNotMet(context.contextId!);
}
}
}
}

Expand Down
3 changes: 1 addition & 2 deletions src/blob/errors/StorageErrorFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,6 @@ export default class StorageErrorFactory {
{ ReceivedCopyStatus: copyStatus }
);
}


public static getInvalidMetadata(contextID: string): StorageError {
return new StorageError(
Expand Down Expand Up @@ -825,7 +824,7 @@ export default class StorageErrorFactory {
"The tags specified are invalid. It contains characters that are not permitted.",
contextID
);
}
}

public static getInvalidXmlDocument(
contextID: string = ""
Expand Down
19 changes: 10 additions & 9 deletions src/blob/handlers/BlockBlobHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default class BlockBlobHandler
"application/octet-stream";
const contentMD5 = context.request!.getHeader("content-md5")
? options.blobHTTPHeaders.blobContentMD5 ||
context.request!.getHeader("content-md5")
context.request!.getHeader("content-md5")
: undefined;

await this.metadataStore.checkContainerExist(
Expand Down Expand Up @@ -161,9 +161,9 @@ export default class BlockBlobHandler
}

public async putBlobFromUrl(contentLength: number, copySource: string, options: Models.BlockBlobPutBlobFromUrlOptionalParams, context: Context
): Promise<Models.BlockBlobPutBlobFromUrlResponse> {
): Promise<Models.BlockBlobPutBlobFromUrlResponse> {
throw new NotImplementedError(context.contextId);
}
}

public async stageBlock(
blockId: string,
Expand All @@ -183,7 +183,7 @@ export default class BlockBlobHandler
// options.blobHTTPHeaders = options.blobHTTPHeaders || {};
const contentMD5 = context.request!.getHeader("content-md5")
? options.transactionalContentMD5 ||
context.request!.getHeader("content-md5")
context.request!.getHeader("content-md5")
: undefined;

this.validateBlockId(blockId, blobCtx);
Expand Down Expand Up @@ -332,7 +332,7 @@ export default class BlockBlobHandler
accountName,
containerName,
name: blobName,
snapshot: "",
snapshot: "",
blobTags: options.blobTagsString === undefined ? undefined : getTagsFromString(options.blobTagsString, context.contextId!),
properties: {
lastModified: context.startTime!,
Expand Down Expand Up @@ -412,7 +412,8 @@ export default class BlockBlobHandler
blobName,
options.snapshot,
undefined,
options.leaseAccessConditions
options.leaseAccessConditions,
options.modifiedAccessConditions
);

// TODO: Create uncommitted blockblob when stage block
Expand All @@ -437,16 +438,16 @@ export default class BlockBlobHandler
(options.listType.toLowerCase() ===
Models.BlockListType.All.toLowerCase() ||
options.listType.toLowerCase() ===
Models.BlockListType.Uncommitted.toLowerCase())
Models.BlockListType.Uncommitted.toLowerCase())
) {
response.uncommittedBlocks = res.uncommittedBlocks;
}
if (
options.listType === undefined ||
options.listType.toLowerCase() ===
Models.BlockListType.All.toLowerCase() ||
Models.BlockListType.All.toLowerCase() ||
options.listType.toLowerCase() ===
Models.BlockListType.Committed.toLowerCase()
Models.BlockListType.Committed.toLowerCase()
) {
response.committedBlocks = res.committedBlocks;
}
Expand Down
43 changes: 42 additions & 1 deletion src/blob/handlers/ContainerHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,48 @@ export default class ContainerHandler extends BaseHandler

public async filterBlobs(options: Models.ContainerFilterBlobsOptionalParams, context: Context
): Promise<Models.ContainerFilterBlobsResponse> {
throw new NotImplementedError(context.contextId!);
const blobCtx = new BlobStorageContext(context);
const accountName = blobCtx.account!;
const containerName = blobCtx.container!;
await this.metadataStore.checkContainerExist(
context,
accountName,
containerName
);

const request = context.request!;
const marker = options.marker;
options.marker = options.marker || "";
if (
options.maxresults === undefined ||
options.maxresults > DEFAULT_LIST_BLOBS_MAX_RESULTS
) {
options.maxresults = DEFAULT_LIST_BLOBS_MAX_RESULTS;
}

const [blobs, nextMarker] = await this.metadataStore.filterBlobs(
context,
accountName,
containerName,
options.where,
options.maxresults,
marker,
);

const serviceEndpoint = `${request.getEndpoint()}/${accountName}`;
const response: Models.ContainerFilterBlobsResponse = {
statusCode: 200,
requestId: context.contextId,
version: BLOB_API_VERSION,
date: context.startTime,
serviceEndpoint,
where: options.where!,
blobs: blobs,
clientRequestId: options.requestId,
nextMarker: `${nextMarker || ""}`
};

return response;
}

/**
Expand Down
Loading

0 comments on commit 1e7d69b

Please sign in to comment.