Merge pull request #221 from johndoe6345789/codex/create-tenant-context-and-audit-hooks

Refactor tenant-aware blob storage context and hooks
This commit is contained in:
2025-12-27 18:37:46 +00:00
committed by GitHub
7 changed files with 67 additions and 39 deletions

View File

@@ -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'

View File

@@ -0,0 +1,17 @@
import type { TenantAwareDeps } from './context'
const recordUsageChange = async (deps: TenantAwareDeps, bytesChange: number, countChange: number): Promise<void> => {
await deps.tenantManager.updateBlobUsage(deps.tenantId, bytesChange, countChange)
}
export const auditUpload = async (deps: TenantAwareDeps, sizeBytes: number): Promise<void> => {
await recordUsageChange(deps, sizeBytes, 1)
}
export const auditDeletion = async (deps: TenantAwareDeps, sizeBytes: number): Promise<void> => {
await recordUsageChange(deps, -sizeBytes, -1)
}
export const auditCopy = async (deps: TenantAwareDeps, sizeBytes: number): Promise<void> => {
await recordUsageChange(deps, sizeBytes, 1)
}

View File

@@ -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<TenantContext> => {
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<typeof action, string> = {
read: 'read',
write: 'write',
delete: 'delete',
}
throw DBALError.forbidden(`Permission denied: cannot ${verbs[action]} blobs`)
}
}

View File

@@ -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<boolean> => {
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<bo
const deleted = await deps.baseStorage.delete(scopedKey)
if (deleted) {
await deps.tenantManager.updateBlobUsage(deps.tenantId, -metadata.size, -1)
await auditDeletion(deps, metadata.size)
}
return deleted
@@ -24,7 +26,7 @@ export const deleteBlob = async (deps: TenantAwareDeps, key: string): Promise<bo
}
export const exists = async (deps: TenantAwareDeps, key: string): Promise<boolean> => {
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<BlobMetadata> => {
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,

View File

@@ -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<Buffer> => {
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<ReadableStream | NodeJS.ReadableStream> => {
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<BlobListResult> => {
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<BlobMetadata> => {
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<string> => {
const context = await getContext(deps)
const context = await resolveTenantContext(deps)
ensurePermission(context, 'read')
const scopedKey = scopeKey(key, context.namespace)

View File

@@ -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<TenantContext> => {
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<typeof action, string> = {
read: 'read',
write: 'write',
delete: 'delete',
}
throw DBALError.forbidden(`Permission denied: cannot ${verbs[action]} blobs`)
}
}

View File

@@ -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<BlobMetadata> => {
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<BlobMetadata> => {
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,