diff --git a/dbal/development/src/blob/providers/tenant-aware-storage.ts b/dbal/development/src/blob/providers/tenant-aware-storage.ts index 33fa59a9d..c8f14cee9 100644 --- a/dbal/development/src/blob/providers/tenant-aware-storage.ts +++ b/dbal/development/src/blob/providers/tenant-aware-storage.ts @@ -1 +1,5 @@ export { TenantAwareBlobStorage } from './tenant-aware-storage/index' +export type { TenantAwareDeps } from './tenant-aware-storage/context' +export { scopeKey, unscopeKey } from './tenant-aware-storage/context' +export { ensurePermission, resolveTenantContext } from './tenant-aware-storage/tenant-context' +export { auditCopy, auditDeletion, auditUpload } from './tenant-aware-storage/audit-hooks' diff --git a/dbal/development/src/blob/providers/tenant-aware-storage/audit-hooks.ts b/dbal/development/src/blob/providers/tenant-aware-storage/audit-hooks.ts new file mode 100644 index 000000000..8aeb80c80 --- /dev/null +++ b/dbal/development/src/blob/providers/tenant-aware-storage/audit-hooks.ts @@ -0,0 +1,17 @@ +import type { TenantAwareDeps } from './context' + +const recordUsageChange = async (deps: TenantAwareDeps, bytesChange: number, countChange: number): Promise => { + await deps.tenantManager.updateBlobUsage(deps.tenantId, bytesChange, countChange) +} + +export const auditUpload = async (deps: TenantAwareDeps, sizeBytes: number): Promise => { + await recordUsageChange(deps, sizeBytes, 1) +} + +export const auditDeletion = async (deps: TenantAwareDeps, sizeBytes: number): Promise => { + await recordUsageChange(deps, -sizeBytes, -1) +} + +export const auditCopy = async (deps: TenantAwareDeps, sizeBytes: number): Promise => { + await recordUsageChange(deps, sizeBytes, 1) +} diff --git a/dbal/development/src/blob/providers/tenant-aware-storage/context.ts b/dbal/development/src/blob/providers/tenant-aware-storage/context.ts index 234816666..067d7ff99 100644 --- a/dbal/development/src/blob/providers/tenant-aware-storage/context.ts +++ b/dbal/development/src/blob/providers/tenant-aware-storage/context.ts @@ -1,5 +1,4 @@ -import { DBALError } from '../../core/foundation/errors' -import type { TenantContext, TenantManager } from '../../core/foundation/tenant-context' +import type { TenantManager } from '../../core/foundation/tenant-context' import type { BlobStorage } from '../blob-storage' export interface TenantAwareDeps { @@ -9,10 +8,6 @@ export interface TenantAwareDeps { userId: string } -export const getContext = async ({ tenantManager, tenantId, userId }: TenantAwareDeps): Promise => { - return tenantManager.getTenantContext(tenantId, userId) -} - export const scopeKey = (key: string, namespace: string): string => { const cleanKey = key.startsWith('/') ? key.substring(1) : key return `${namespace}${cleanKey}` @@ -24,17 +19,3 @@ export const unscopeKey = (scopedKey: string, namespace: string): string => { } return scopedKey } - -export const ensurePermission = (context: TenantContext, action: 'read' | 'write' | 'delete'): void => { - const accessCheck = - action === 'read' ? context.canRead('blob') : action === 'write' ? context.canWrite('blob') : context.canDelete('blob') - - if (!accessCheck) { - const verbs: Record = { - read: 'read', - write: 'write', - delete: 'delete', - } - throw DBALError.forbidden(`Permission denied: cannot ${verbs[action]} blobs`) - } -} diff --git a/dbal/development/src/blob/providers/tenant-aware-storage/mutations.ts b/dbal/development/src/blob/providers/tenant-aware-storage/mutations.ts index 6ec400af4..b518eb1c0 100644 --- a/dbal/development/src/blob/providers/tenant-aware-storage/mutations.ts +++ b/dbal/development/src/blob/providers/tenant-aware-storage/mutations.ts @@ -1,10 +1,12 @@ import { DBALError } from '../../core/foundation/errors' import type { BlobMetadata } from '../blob-storage' -import { ensurePermission, getContext, scopeKey } from './context' +import { auditCopy, auditDeletion } from './audit-hooks' import type { TenantAwareDeps } from './context' +import { scopeKey } from './context' +import { ensurePermission, resolveTenantContext } from './tenant-context' export const deleteBlob = async (deps: TenantAwareDeps, key: string): Promise => { - const context = await getContext(deps) + const context = await resolveTenantContext(deps) ensurePermission(context, 'delete') const scopedKey = scopeKey(key, context.namespace) @@ -14,7 +16,7 @@ export const deleteBlob = async (deps: TenantAwareDeps, key: string): Promise => { - const context = await getContext(deps) + const context = await resolveTenantContext(deps) ensurePermission(context, 'read') const scopedKey = scopeKey(key, context.namespace) @@ -36,7 +38,7 @@ export const copyBlob = async ( sourceKey: string, destKey: string, ): Promise => { - const context = await getContext(deps) + const context = await resolveTenantContext(deps) ensurePermission(context, 'read') ensurePermission(context, 'write') @@ -50,7 +52,7 @@ export const copyBlob = async ( const destScoped = scopeKey(destKey, context.namespace) const metadata = await deps.baseStorage.copy(sourceScoped, destScoped) - await deps.tenantManager.updateBlobUsage(deps.tenantId, sourceMetadata.size, 1) + await auditCopy(deps, sourceMetadata.size) return { ...metadata, @@ -59,7 +61,7 @@ export const copyBlob = async ( } export const getStats = async (deps: TenantAwareDeps) => { - const context = await getContext(deps) + const context = await resolveTenantContext(deps) return { count: context.quota.currentBlobCount, totalSize: context.quota.currentBlobStorageBytes, diff --git a/dbal/development/src/blob/providers/tenant-aware-storage/reads.ts b/dbal/development/src/blob/providers/tenant-aware-storage/reads.ts index 5ba718d0d..9fc52a58b 100644 --- a/dbal/development/src/blob/providers/tenant-aware-storage/reads.ts +++ b/dbal/development/src/blob/providers/tenant-aware-storage/reads.ts @@ -1,9 +1,10 @@ import type { DownloadOptions, BlobMetadata, BlobListOptions, BlobListResult } from '../blob-storage' -import { ensurePermission, getContext, scopeKey, unscopeKey } from './context' import type { TenantAwareDeps } from './context' +import { scopeKey, unscopeKey } from './context' +import { ensurePermission, resolveTenantContext } from './tenant-context' export const downloadBuffer = async (deps: TenantAwareDeps, key: string): Promise => { - const context = await getContext(deps) + const context = await resolveTenantContext(deps) ensurePermission(context, 'read') const scopedKey = scopeKey(key, context.namespace) @@ -15,7 +16,7 @@ export const downloadStream = async ( key: string, options?: DownloadOptions, ): Promise => { - const context = await getContext(deps) + const context = await resolveTenantContext(deps) ensurePermission(context, 'read') const scopedKey = scopeKey(key, context.namespace) @@ -26,7 +27,7 @@ export const listBlobs = async ( deps: TenantAwareDeps, options: BlobListOptions = {}, ): Promise => { - const context = await getContext(deps) + const context = await resolveTenantContext(deps) ensurePermission(context, 'read') const scopedOptions: BlobListOptions = { @@ -46,7 +47,7 @@ export const listBlobs = async ( } export const getMetadata = async (deps: TenantAwareDeps, key: string): Promise => { - const context = await getContext(deps) + const context = await resolveTenantContext(deps) ensurePermission(context, 'read') const scopedKey = scopeKey(key, context.namespace) @@ -63,7 +64,7 @@ export const generatePresignedUrl = async ( key: string, expiresIn: number, ): Promise => { - const context = await getContext(deps) + const context = await resolveTenantContext(deps) ensurePermission(context, 'read') const scopedKey = scopeKey(key, context.namespace) diff --git a/dbal/development/src/blob/providers/tenant-aware-storage/tenant-context.ts b/dbal/development/src/blob/providers/tenant-aware-storage/tenant-context.ts new file mode 100644 index 000000000..acdd36720 --- /dev/null +++ b/dbal/development/src/blob/providers/tenant-aware-storage/tenant-context.ts @@ -0,0 +1,21 @@ +import { DBALError } from '../../core/foundation/errors' +import type { TenantContext } from '../../core/foundation/tenant-context' +import type { TenantAwareDeps } from './context' + +export const resolveTenantContext = async ({ tenantManager, tenantId, userId }: TenantAwareDeps): Promise => { + return tenantManager.getTenantContext(tenantId, userId) +} + +export const ensurePermission = (context: TenantContext, action: 'read' | 'write' | 'delete'): void => { + const accessCheck = + action === 'read' ? context.canRead('blob') : action === 'write' ? context.canWrite('blob') : context.canDelete('blob') + + if (!accessCheck) { + const verbs: Record = { + read: 'read', + write: 'write', + delete: 'delete', + } + throw DBALError.forbidden(`Permission denied: cannot ${verbs[action]} blobs`) + } +} diff --git a/dbal/development/src/blob/providers/tenant-aware-storage/uploads.ts b/dbal/development/src/blob/providers/tenant-aware-storage/uploads.ts index cd787a533..382fc4881 100644 --- a/dbal/development/src/blob/providers/tenant-aware-storage/uploads.ts +++ b/dbal/development/src/blob/providers/tenant-aware-storage/uploads.ts @@ -1,7 +1,9 @@ import { DBALError } from '../../core/foundation/errors' -import type { UploadOptions, BlobMetadata } from '../blob-storage' +import { auditUpload } from './audit-hooks' import type { TenantAwareDeps } from './context' -import { ensurePermission, getContext, scopeKey } from './context' +import { scopeKey } from './context' +import { ensurePermission, resolveTenantContext } from './tenant-context' +import type { UploadOptions, BlobMetadata } from '../blob-storage' export const uploadBuffer = async ( deps: TenantAwareDeps, @@ -9,7 +11,7 @@ export const uploadBuffer = async ( data: Buffer, options?: UploadOptions, ): Promise => { - const context = await getContext(deps) + const context = await resolveTenantContext(deps) ensurePermission(context, 'write') if (!context.canUploadBlob(data.length)) { @@ -18,7 +20,7 @@ export const uploadBuffer = async ( const scopedKey = scopeKey(key, context.namespace) const metadata = await deps.baseStorage.upload(scopedKey, data, options) - await deps.tenantManager.updateBlobUsage(deps.tenantId, data.length, 1) + await auditUpload(deps, data.length) return { ...metadata, @@ -33,7 +35,7 @@ export const uploadStream = async ( size: number, options?: UploadOptions, ): Promise => { - const context = await getContext(deps) + const context = await resolveTenantContext(deps) ensurePermission(context, 'write') if (!context.canUploadBlob(size)) { @@ -42,7 +44,7 @@ export const uploadStream = async ( const scopedKey = scopeKey(key, context.namespace) const metadata = await deps.baseStorage.uploadStream(scopedKey, stream, size, options) - await deps.tenantManager.updateBlobUsage(deps.tenantId, size, 1) + await auditUpload(deps, size) return { ...metadata,