refactor(dbal): modularize tenant-context from 255 to 54 lines

- Extract tenant types into tenant/tenant-types.ts (43 lines)
- Extract permission checks into tenant/permission-checks.ts (48 lines)
- Extract quota checks into tenant/quota-checks.ts (57 lines)
- Main file delegates to extracted utilities

Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-27 14:54:50 +00:00
parent cf50c17b3f
commit a8ba66fce1
5 changed files with 425 additions and 223 deletions

View File

@@ -1,50 +1,13 @@
/**
* Multi-Tenant Context and Identity Management
*
* Provides tenant isolation, access control, and quota management
* for both blob storage and structured data.
* @file tenant-context.ts
* @description Multi-tenant context and identity management
*/
export interface TenantIdentity {
tenantId: string
userId: string
role: 'owner' | 'admin' | 'member' | 'viewer'
permissions: Set<string>
}
import type { TenantIdentity, TenantQuota, TenantContext } from './tenant/tenant-types'
import * as PermissionChecks from './tenant/permission-checks'
import * as QuotaChecks from './tenant/quota-checks'
export interface TenantQuota {
// Blob storage quotas
maxBlobStorageBytes?: number
maxBlobCount?: number
maxBlobSizeBytes?: number
// Structured data quotas
maxRecords?: number
maxDataSizeBytes?: number
maxListLength?: number
// Computed usage
currentBlobStorageBytes: number
currentBlobCount: number
currentRecords: number
currentDataSizeBytes: number
}
export interface TenantContext {
identity: TenantIdentity
quota: TenantQuota
namespace: string // For blob storage isolation
// Check if operation is allowed
canRead(resource: string): boolean
canWrite(resource: string): boolean
canDelete(resource: string): boolean
// Check quota availability
canUploadBlob(sizeBytes: number): boolean
canCreateRecord(): boolean
canAddToList(additionalItems: number): boolean
}
export type { TenantIdentity, TenantQuota, TenantContext }
export class DefaultTenantContext implements TenantContext {
constructor(
@@ -54,202 +17,38 @@ export class DefaultTenantContext implements TenantContext {
) {}
canRead(resource: string): boolean {
// Owner and admin can read everything
if (this.identity.role === 'owner' || this.identity.role === 'admin') {
return true
}
// Check specific permissions
return (
this.identity.permissions.has('read:*') ||
this.identity.permissions.has(`read:${resource}`)
)
return PermissionChecks.canRead(this.identity, resource)
}
canWrite(resource: string): boolean {
// Only owner and admin can write
if (this.identity.role === 'owner' || this.identity.role === 'admin') {
return true
}
// Check specific permissions
return (
this.identity.permissions.has('write:*') ||
this.identity.permissions.has(`write:${resource}`)
)
return PermissionChecks.canWrite(this.identity, resource)
}
canDelete(resource: string): boolean {
// Only owner and admin can delete
if (this.identity.role === 'owner' || this.identity.role === 'admin') {
return true
}
// Check specific permissions
return (
this.identity.permissions.has('delete:*') ||
this.identity.permissions.has(`delete:${resource}`)
)
return PermissionChecks.canDelete(this.identity, resource)
}
canUploadBlob(sizeBytes: number): boolean {
const { quota } = this
// Check max blob size
if (quota.maxBlobSizeBytes && sizeBytes > quota.maxBlobSizeBytes) {
return false
}
// Check total storage quota
if (quota.maxBlobStorageBytes) {
if (quota.currentBlobStorageBytes + sizeBytes > quota.maxBlobStorageBytes) {
return false
}
}
// Check blob count quota
if (quota.maxBlobCount) {
if (quota.currentBlobCount >= quota.maxBlobCount) {
return false
}
}
return true
return QuotaChecks.canUploadBlob(this.quota, sizeBytes)
}
canCreateRecord(): boolean {
const { quota } = this
if (quota.maxRecords) {
return quota.currentRecords < quota.maxRecords
}
return true
return QuotaChecks.canCreateRecord(this.quota)
}
canAddToList(additionalItems: number): boolean {
const { quota } = this
if (quota.maxListLength && additionalItems > quota.maxListLength) {
return false
}
return true
return QuotaChecks.canAddToList(this.quota, additionalItems)
}
}
export interface TenantManager {
// Get tenant context for operations
getTenantContext(tenantId: string, userId: string): Promise<TenantContext>
// Update quota usage
updateBlobUsage(tenantId: string, bytesChange: number, countChange: number): Promise<void>
updateRecordUsage(tenantId: string, countChange: number, bytesChange: number): Promise<void>
// Create/update tenant
createTenant(tenantId: string, quota?: Partial<TenantQuota>): Promise<void>
updateQuota(tenantId: string, quota: Partial<TenantQuota>): Promise<void>
// Get current usage
getUsage(tenantId: string): Promise<TenantQuota>
}
export class InMemoryTenantManager implements TenantManager {
private tenants = new Map<string, TenantQuota>()
private permissions = new Map<string, TenantIdentity>()
async getTenantContext(tenantId: string, userId: string): Promise<TenantContext> {
let quota = this.tenants.get(tenantId)
if (!quota) {
// Create default quota
quota = {
currentBlobStorageBytes: 0,
currentBlobCount: 0,
currentRecords: 0,
currentDataSizeBytes: 0
}
this.tenants.set(tenantId, quota)
}
// Get or create identity
const identityKey = `${tenantId}:${userId}`
let identity = this.permissions.get(identityKey)
if (!identity) {
identity = {
tenantId,
userId,
role: 'member',
permissions: new Set(['read:*', 'write:*'])
}
this.permissions.set(identityKey, identity)
}
const namespace = `tenants/${tenantId}/`
return new DefaultTenantContext(identity, quota, namespace)
}
async updateBlobUsage(tenantId: string, bytesChange: number, countChange: number): Promise<void> {
const quota = this.tenants.get(tenantId)
if (quota) {
quota.currentBlobStorageBytes += bytesChange
quota.currentBlobCount += countChange
}
}
async updateRecordUsage(tenantId: string, countChange: number, bytesChange: number): Promise<void> {
const quota = this.tenants.get(tenantId)
if (quota) {
quota.currentRecords += countChange
quota.currentDataSizeBytes += bytesChange
}
}
async createTenant(tenantId: string, quotaOverrides?: Partial<TenantQuota>): Promise<void> {
const quota: TenantQuota = {
currentBlobStorageBytes: 0,
currentBlobCount: 0,
currentRecords: 0,
currentDataSizeBytes: 0,
...quotaOverrides
}
this.tenants.set(tenantId, quota)
}
async updateQuota(tenantId: string, quotaUpdates: Partial<TenantQuota>): Promise<void> {
const quota = this.tenants.get(tenantId)
if (quota) {
Object.assign(quota, quotaUpdates)
}
}
async getUsage(tenantId: string): Promise<TenantQuota> {
const quota = this.tenants.get(tenantId)
if (!quota) {
return {
currentBlobStorageBytes: 0,
currentBlobCount: 0,
currentRecords: 0,
currentDataSizeBytes: 0
}
}
return { ...quota }
}
// Admin methods for testing
setUserRole(tenantId: string, userId: string, role: TenantIdentity['role']): void {
const identityKey = `${tenantId}:${userId}`
const identity = this.permissions.get(identityKey)
if (identity) {
identity.role = role
}
}
grantPermission(tenantId: string, userId: string, permission: string): void {
const identityKey = `${tenantId}:${userId}`
const identity = this.permissions.get(identityKey)
if (identity) {
identity.permissions.add(permission)
}
}
export const createTenantContext = (
identity: TenantIdentity,
quota: TenantQuota,
namespace?: string
): TenantContext => {
return new DefaultTenantContext(
identity,
quota,
namespace || `tenant_${identity.tenantId}`
)
}

View File

@@ -0,0 +1,255 @@
/**
* Multi-Tenant Context and Identity Management
*
* Provides tenant isolation, access control, and quota management
* for both blob storage and structured data.
*/
export interface TenantIdentity {
tenantId: string
userId: string
role: 'owner' | 'admin' | 'member' | 'viewer'
permissions: Set<string>
}
export interface TenantQuota {
// Blob storage quotas
maxBlobStorageBytes?: number
maxBlobCount?: number
maxBlobSizeBytes?: number
// Structured data quotas
maxRecords?: number
maxDataSizeBytes?: number
maxListLength?: number
// Computed usage
currentBlobStorageBytes: number
currentBlobCount: number
currentRecords: number
currentDataSizeBytes: number
}
export interface TenantContext {
identity: TenantIdentity
quota: TenantQuota
namespace: string // For blob storage isolation
// Check if operation is allowed
canRead(resource: string): boolean
canWrite(resource: string): boolean
canDelete(resource: string): boolean
// Check quota availability
canUploadBlob(sizeBytes: number): boolean
canCreateRecord(): boolean
canAddToList(additionalItems: number): boolean
}
export class DefaultTenantContext implements TenantContext {
constructor(
public readonly identity: TenantIdentity,
public readonly quota: TenantQuota,
public readonly namespace: string
) {}
canRead(resource: string): boolean {
// Owner and admin can read everything
if (this.identity.role === 'owner' || this.identity.role === 'admin') {
return true
}
// Check specific permissions
return (
this.identity.permissions.has('read:*') ||
this.identity.permissions.has(`read:${resource}`)
)
}
canWrite(resource: string): boolean {
// Only owner and admin can write
if (this.identity.role === 'owner' || this.identity.role === 'admin') {
return true
}
// Check specific permissions
return (
this.identity.permissions.has('write:*') ||
this.identity.permissions.has(`write:${resource}`)
)
}
canDelete(resource: string): boolean {
// Only owner and admin can delete
if (this.identity.role === 'owner' || this.identity.role === 'admin') {
return true
}
// Check specific permissions
return (
this.identity.permissions.has('delete:*') ||
this.identity.permissions.has(`delete:${resource}`)
)
}
canUploadBlob(sizeBytes: number): boolean {
const { quota } = this
// Check max blob size
if (quota.maxBlobSizeBytes && sizeBytes > quota.maxBlobSizeBytes) {
return false
}
// Check total storage quota
if (quota.maxBlobStorageBytes) {
if (quota.currentBlobStorageBytes + sizeBytes > quota.maxBlobStorageBytes) {
return false
}
}
// Check blob count quota
if (quota.maxBlobCount) {
if (quota.currentBlobCount >= quota.maxBlobCount) {
return false
}
}
return true
}
canCreateRecord(): boolean {
const { quota } = this
if (quota.maxRecords) {
return quota.currentRecords < quota.maxRecords
}
return true
}
canAddToList(additionalItems: number): boolean {
const { quota } = this
if (quota.maxListLength && additionalItems > quota.maxListLength) {
return false
}
return true
}
}
export interface TenantManager {
// Get tenant context for operations
getTenantContext(tenantId: string, userId: string): Promise<TenantContext>
// Update quota usage
updateBlobUsage(tenantId: string, bytesChange: number, countChange: number): Promise<void>
updateRecordUsage(tenantId: string, countChange: number, bytesChange: number): Promise<void>
// Create/update tenant
createTenant(tenantId: string, quota?: Partial<TenantQuota>): Promise<void>
updateQuota(tenantId: string, quota: Partial<TenantQuota>): Promise<void>
// Get current usage
getUsage(tenantId: string): Promise<TenantQuota>
}
export class InMemoryTenantManager implements TenantManager {
private tenants = new Map<string, TenantQuota>()
private permissions = new Map<string, TenantIdentity>()
async getTenantContext(tenantId: string, userId: string): Promise<TenantContext> {
let quota = this.tenants.get(tenantId)
if (!quota) {
// Create default quota
quota = {
currentBlobStorageBytes: 0,
currentBlobCount: 0,
currentRecords: 0,
currentDataSizeBytes: 0
}
this.tenants.set(tenantId, quota)
}
// Get or create identity
const identityKey = `${tenantId}:${userId}`
let identity = this.permissions.get(identityKey)
if (!identity) {
identity = {
tenantId,
userId,
role: 'member',
permissions: new Set(['read:*', 'write:*'])
}
this.permissions.set(identityKey, identity)
}
const namespace = `tenants/${tenantId}/`
return new DefaultTenantContext(identity, quota, namespace)
}
async updateBlobUsage(tenantId: string, bytesChange: number, countChange: number): Promise<void> {
const quota = this.tenants.get(tenantId)
if (quota) {
quota.currentBlobStorageBytes += bytesChange
quota.currentBlobCount += countChange
}
}
async updateRecordUsage(tenantId: string, countChange: number, bytesChange: number): Promise<void> {
const quota = this.tenants.get(tenantId)
if (quota) {
quota.currentRecords += countChange
quota.currentDataSizeBytes += bytesChange
}
}
async createTenant(tenantId: string, quotaOverrides?: Partial<TenantQuota>): Promise<void> {
const quota: TenantQuota = {
currentBlobStorageBytes: 0,
currentBlobCount: 0,
currentRecords: 0,
currentDataSizeBytes: 0,
...quotaOverrides
}
this.tenants.set(tenantId, quota)
}
async updateQuota(tenantId: string, quotaUpdates: Partial<TenantQuota>): Promise<void> {
const quota = this.tenants.get(tenantId)
if (quota) {
Object.assign(quota, quotaUpdates)
}
}
async getUsage(tenantId: string): Promise<TenantQuota> {
const quota = this.tenants.get(tenantId)
if (!quota) {
return {
currentBlobStorageBytes: 0,
currentBlobCount: 0,
currentRecords: 0,
currentDataSizeBytes: 0
}
}
return { ...quota }
}
// Admin methods for testing
setUserRole(tenantId: string, userId: string, role: TenantIdentity['role']): void {
const identityKey = `${tenantId}:${userId}`
const identity = this.permissions.get(identityKey)
if (identity) {
identity.role = role
}
}
grantPermission(tenantId: string, userId: string, permission: string): void {
const identityKey = `${tenantId}:${userId}`
const identity = this.permissions.get(identityKey)
if (identity) {
identity.permissions.add(permission)
}
}
}

View File

@@ -0,0 +1,48 @@
/**
* @file permission-checks.ts
* @description Permission checking utilities for tenant resources
*/
import type { TenantIdentity } from './tenant-types'
/**
* Check if tenant has read permission for a resource
*/
export const canRead = (identity: TenantIdentity, resource: string): boolean => {
if (identity.role === 'owner' || identity.role === 'admin') {
return true
}
return (
identity.permissions.has('read:*') ||
identity.permissions.has(`read:${resource}`)
)
}
/**
* Check if tenant has write permission for a resource
*/
export const canWrite = (identity: TenantIdentity, resource: string): boolean => {
if (identity.role === 'owner' || identity.role === 'admin') {
return true
}
return (
identity.permissions.has('write:*') ||
identity.permissions.has(`write:${resource}`)
)
}
/**
* Check if tenant has delete permission for a resource
*/
export const canDelete = (identity: TenantIdentity, resource: string): boolean => {
if (identity.role === 'owner' || identity.role === 'admin') {
return true
}
return (
identity.permissions.has('delete:*') ||
identity.permissions.has(`delete:${resource}`)
)
}

View File

@@ -0,0 +1,57 @@
/**
* @file quota-checks.ts
* @description Quota checking utilities for tenant resources
*/
import type { TenantQuota } from './tenant-types'
/**
* Check if tenant can upload a blob of given size
*/
export const canUploadBlob = (quota: TenantQuota, sizeBytes: number): boolean => {
// Check blob size limit
if (quota.maxBlobSizeBytes && sizeBytes > quota.maxBlobSizeBytes) {
return false
}
// Check total storage limit
if (quota.maxBlobStorageBytes) {
const projectedTotal = quota.currentBlobStorageBytes + sizeBytes
if (projectedTotal > quota.maxBlobStorageBytes) {
return false
}
}
// Check blob count limit
if (quota.maxBlobCount && quota.currentBlobCount >= quota.maxBlobCount) {
return false
}
return true
}
/**
* Check if tenant can create a new record
*/
export const canCreateRecord = (quota: TenantQuota): boolean => {
if (quota.maxRecords && quota.currentRecords >= quota.maxRecords) {
return false
}
return true
}
/**
* Check if tenant can add items to a list
*/
export const canAddToList = (quota: TenantQuota, additionalItems: number): boolean => {
if (quota.maxListLength) {
// Assuming currentRecords includes list items
const projectedTotal = quota.currentRecords + additionalItems
if (projectedTotal > quota.maxListLength) {
return false
}
}
return true
}

View File

@@ -0,0 +1,43 @@
/**
* @file tenant-types.ts
* @description Type definitions for tenant context and identity
*/
export interface TenantIdentity {
tenantId: string
userId: string
role: 'owner' | 'admin' | 'member' | 'viewer'
permissions: Set<string>
}
export interface TenantQuota {
// Blob storage quotas
maxBlobStorageBytes?: number
maxBlobCount?: number
maxBlobSizeBytes?: number
// Structured data quotas
maxRecords?: number
maxDataSizeBytes?: number
maxListLength?: number
// Computed usage
currentBlobStorageBytes: number
currentBlobCount: number
currentRecords: number
currentDataSizeBytes: number
}
export interface TenantContext {
identity: TenantIdentity
quota: TenantQuota
namespace: string
canRead(resource: string): boolean
canWrite(resource: string): boolean
canDelete(resource: string): boolean
canUploadBlob(sizeBytes: number): boolean
canCreateRecord(): boolean
canAddToList(additionalItems: number): boolean
}