import IAccountDataStore from "../../common/IAccountDataStore" ; import ILogger from "../../common/ILogger" ; import BlobStorageContext from "../context/BlobStorageContext" ; import StorageErrorFactory from "../errors/StorageErrorFactory" ; import { AccessPolicy , BlobType } from "../generated/artifacts/models" ; import Operation from "../generated/artifacts/operation" ; import Context from "../generated/Context" ; import IRequest from "../generated/IRequest" ; import IBlobMetadataStore from "../persistence/IBlobMetadataStore" ; import { BlobSASPermission } from "./BlobSASPermissions" ; import { BlobSASResourceType } from "./BlobSASResourceType" ; import IAuthenticator from "./IAuthenticator" ; import { generateBlobSASSignature , IBlobSASSignatureValues } from "./IBlobSASSignatureValues" ; import { OPERATION_BLOB_SAS_BLOB_PERMISSIONS , OPERATION_BLOB_SAS_CONTAINER_PERMISSIONS } from "./OperationBlobSASPermission" ; export default class BlobSASAuthenticator implements IAuthenticator { public constructor ( private readonly accountDataStore , private readonly blobMetadataStore , private readonly logger ) { } public async validate ( req , context ) { this . logger . info ( `BlobSASAuthenticator:validate() Start validation against blob service Shared Access Signature pattern.` , context . contextId ) ; this . logger . debug ( "BlobSASAuthenticator:validate() Getting account properties..." , context . contextId ) ; const blobContext = new BlobStorageContext ( context ) ; const account = blobContext . account ; if ( account === undefined ) { throw RangeError ( `BlobSASAuthenticator:validate() account is undefined in context.` ) ; } const containerName = blobContext . container ; if ( containerName === undefined ) { this . logger . error ( `BlobSASAuthenticator:validate() container name is undefined in context.` , context . contextId ) ; return undefined ; } const blobName = blobContext . blob ; this . logger . debug ( `BlobSASAuthenticator:validate() Retrieved account name from context: ${ account }, container: ${ containerName }, blob: ${ blobName }` , context . contextId ) ; const accountProperties = this . accountDataStore . getAccount ( account ) ; if ( accountProperties === undefined ) { throw StorageErrorFactory . getInvalidOperation ( context . contextId ! , "Invalid storage account." ) ; } this . logger . debug ( "BlobSASAuthenticator:validate() Got account properties successfully." , context . contextId ) ; const signature = this . decodeIfExist ( req . getQuery ( "sig" ) ) ; this . logger . debug ( `BlobSASAuthenticator:validate() Retrieved signature from URL parameter sig: ${ signature }` , context . contextId ) ; if ( signature === undefined ) { this . logger . debug ( `BlobSASAuthenticator:validate() No signature found in request. Skip blob service SAS validation.` , context . contextId ) ; return undefined ; } const resource = this . decodeIfExist ( req . getQuery ( "sr" ) ) ; if ( resource !== BlobSASResourceType . Container && resource !== BlobSASResourceType . Blob && resource !== BlobSASResourceType . BlobSnapshot ) { this . logger . debug ( `BlobSASAuthenticator:validate() Signed resource type ${ resource } is invalid. Skip blob service SAS validation.` , context . contextId ) ; return undefined ; } this . logger . debug ( `BlobSASAuthenticator:validate() Signed resource type is ${ resource }.` , context . contextId ) ; const values = this . getBlobSASSignatureValuesFromRequest ( req , containerName , blobName , context ) ; if ( values === undefined ) { this . logger . info ( `BlobSASAuthenticator:validate() Failed to get valid blob service SAS values from request. 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 ) }` , context . contextId ) ; this . logger . info ( `BlobSASAuthenticator:validate() Validate signature based account key1.` , context . contextId ) ; const [ sig1 , stringToSign1 ] = generateBlobSASSignature ( values , resource , account , accountProperties . key1 ) ; this . logger . debug ( `BlobSASAuthenticator:validate() String to sign is: ${ JSON . stringify ( stringToSign1 ) }` , context . contextId ! ) ; this . logger . debug ( `BlobSASAuthenticator:validate() Calculated signature is: ${ sig1 }` , context . contextId ! ) ; const sig1Pass = sig1 === signature ; this . logger . info ( `BlobSASAuthenticator:validate() Signature based on key1 validation ${ sig1Pass ? "passed" : "failed" }.` , context . contextId ) ; if ( accountProperties . key2 !== undefined ) { this . logger . info ( `BlobSASAuthenticator:validate() Account key2 is not empty, validate signature based account key2.` , context . contextId ) ; const [ sig2 , stringToSign2 ] = generateBlobSASSignature ( values , resource , account , accountProperties . key2 ) ; this . logger . debug ( `BlobSASAuthenticator:validate() String to sign is: ${ JSON . stringify ( stringToSign2 ) }` , context . contextId ! ) ; this . logger . debug ( `BlobSASAuthenticator:validate() Calculated signature is: ${ sig2 }` , context . contextId ! ) ; const sig2Pass = sig2 !== signature ; this . logger . info ( `BlobSASAuthenticator:validate() Signature based on key2 validation ${ sig2Pass ? "passed" : "failed" }.` , context . contextId ) ; if ( ! sig2Pass && ! sig1Pass ) { this . logger . info ( `BlobSASAuthenticator:validate() Validate signature based account key1 and key2 failed.` , context . contextId ) ; return false ; } } else { if ( ! sig1Pass ) { return false ; } } if ( values . identifier !== undefined ) { const accessPolicy = await this . getContainerAccessPolicyByIdentifier ( account , containerName , values . identifier , context ) ; if ( accessPolicy === undefined ) { this . logger . warn ( `BlobSASAuthenticator:validate() Cannot get access policy defined for container ${ containerName } with id ${ values . identifier }.` , context . contextId ) ; throw StorageErrorFactory . getAuthorizationFailure ( context . contextId ! ) ; } values . startTime = accessPolicy . start ; values . expiryTime = accessPolicy . expiry ; values . permissions = accessPolicy . permission ; } this . logger . info ( `BlobSASAuthenticator:validate() Validate start and expiry time.` , context . contextId ) ; if ( ! this . validateTime ( values . expiryTime , values . startTime ) ) { this . logger . info ( `BlobSASAuthenticator:validate() Validate start and expiry failed.` , context . contextId ) ; throw StorageErrorFactory . getAuthorizationFailure ( context . contextId ! ) ; } this . logger . info ( `BlobSASAuthenticator:validate() Validate IP range.` , context . contextId ) ; if ( ! this . validateIPRange ( ) ) { this . logger . info ( `BlobSASAuthenticator:validate() Validate IP range failed.` , context . contextId ) ; throw StorageErrorFactory . getAuthorizationSourceIPMismatch ( context . contextId ! ) ; } this . logger . info ( `BlobSASAuthenticator:validate() Validate request protocol.` , context . contextId ) ; if ( ! this . validateProtocol ( values . protocol , req . getProtocol ( ) ) ) { this . logger . info ( `BlobSASAuthenticator:validate() Validate protocol failed.` , context . contextId ) ; throw StorageErrorFactory . getAuthorizationProtocolMismatch ( context . contextId ! ) ; } const operation = context . operation ; if ( operation === undefined ) { throw new Error ( `BlobSASAuthenticator:validate() Operation shouldn't be undefined. Please make sure DispatchMiddleware is hooked before authentication related middleware.` ) ; } const blobSASPermission = 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 ) { throw new Error ( `BlobSASAuthenticator:validate() ${ resource === BlobSASResourceType . Blob ? "OPERATION_BLOB_SAS_BLOB_PERMISSIONS" : "OPERATION_BLOB_SAS_CONTAINER_PERMISSIONS" } doesn't have configuration for operation ${ Operation [ operation ] }'s blob service SAS permission.` ) ; } if ( ! blobSASPermission . validatePermissions ( values . permissions ! ) ) { throw StorageErrorFactory . getAuthorizationPermissionMismatch ( context . contextId ! ) ; } if ( operation === Operation . BlockBlob_Upload || operation === Operation . PageBlob_Create || operation === Operation . AppendBlob_Create || operation === Operation . Blob_StartCopyFromURL || operation === Operation . Blob_CopyFromURL ) { this . logger . info ( `BlobSASAuthenticator:validate() For ${ Operation [ operation ] }, if blob exists, the permission must be Write.` , context . contextId ) ; if ( ( await this . blobExist ( account , containerName ! , blobName ! ) ) && ! values . permissions ! . toString ( ) . includes ( BlobSASPermission . Write ) ) { this . logger . info ( `BlobSASAuthenticator:validate() Account SAS validation failed for special requirement.` , context . contextId ) ; throw StorageErrorFactory . getAuthorizationPermissionMismatch ( context . contextId ! ) ; } } this . logger . info ( `BlobSASAuthenticator:validate() Blob service SAS validation successfully.` , context . contextId ) ; return true ; } private getBlobSASSignatureValuesFromRequest ( req , containerName , blobName ? , context ? ) { const version = this . decodeIfExist ( req . getQuery ( "sv" ) ) ; const protocol = this . decodeIfExist ( req . getQuery ( "spr" ) ) ; const startTime = this . decodeIfExist ( req . getQuery ( "st" ) ) ; const expiryTime = this . decodeIfExist ( req . getQuery ( "se" ) ) ; const permissions = this . decodeIfExist ( req . getQuery ( "sp" ) ) ; const ipRange = this . decodeIfExist ( req . getQuery ( "sip" ) ) ; const identifier = this . decodeIfExist ( req . getQuery ( "si" ) ) ; const cacheControl = req . getQuery ( "rscc" ) ; const contentDisposition = req . getQuery ( "rscd" ) ; const contentEncoding = req . getQuery ( "rsce" ) ; const contentLanguage = req . getQuery ( "rscl" ) ; const contentType = req . getQuery ( "rsct" ) ; const signedResource = this . decodeIfExist ( req . getQuery ( "sr" ) ) ; const snapshot = this . decodeIfExist ( req . getQuery ( "snapshot" ) ) ; if ( ! identifier && ( ! permissions || ! expiryTime ) ) { this . logger . warn ( `BlobSASAuthenticator:generateBlobSASSignature(): Must provide 'permissions' and 'expiryTime' for Blob SAS generation when 'identifier' is not provided.` , context ? context . contextId : undefined ) ; return undefined ; } if ( version === undefined ) { this . logger . warn ( `BlobSASAuthenticator:generateBlobSASSignature(): Must provide 'version'.` , context ? context . contextId : undefined ) ; return undefined ; } const blobSASValues = { version , protocol , startTime , expiryTime , permissions , ipRange , containerName , blobName , identifier , cacheControl , contentDisposition , contentEncoding , contentLanguage , contentType , signedResource , snapshot } ; return blobSASValues ; } private validateTime ( expiry ? , start ? ) { if ( expiry === undefined && start === undefined ) { return true ; } const now = new Date ( ) ; if ( expiry !== undefined ) { const expiryTime = new Date ( expiry ) ; if ( now > expiryTime ) { return false ; } } if ( start !== undefined ) { const startTime = new Date ( start ) ; if ( now < startTime ) { return false ; } } return true ; } private validateIPRange ( ) { return true ; } private validateProtocol ( sasProtocol = "https,http" , requestProtocol ) { if ( sasProtocol . includes ( "," ) ) { return true ; } else { return sasProtocol . toLowerCase ( ) === requestProtocol ; } } private decodeIfExist ( value ? ) { return value === undefined ? value : decodeURIComponent ( value ) ; } private async getContainerAccessPolicyByIdentifier ( account , container , id , context ) { try { const containerModel = await this . blobMetadataStore . getContainerACL ( context , account , container ) ; if ( containerModel === undefined ) { return undefined ; } if ( containerModel . containerAcl === undefined ) { return undefined ; } for ( const acl of containerModel . containerAcl ) { if ( acl . id === id ) { return acl . accessPolicy ; } } } catch ( err ) { return undefined ; } } private async blobExist ( account , container , blob ) { const blobModel = await this . blobMetadataStore . getBlobType ( account , container , blob ) ; if ( blobModel === undefined ) { return false ; } if ( blobModel . blobType === BlobType . BlockBlob && blobModel . isCommitted === false ) { return false ; } return true ; } }