mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-26 14:54:55 +00:00
refactor(dbal): modularize ACL adapter from 453 to 258 lines
- Extract ACL types into acl/types.ts - Extract default rules into acl/default-rules.ts - Extract permission check into acl/check-permission.ts - Extract row-level access check into acl/check-row-level-access.ts - Extract audit logger into acl/audit-logger.ts - Extract permission operation resolver into acl/resolve-permission-operation.ts - Simplify ACL adapter by using extracted lambda functions Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com>
This commit is contained in:
@@ -1,68 +1,16 @@
|
||||
import type { DBALAdapter, AdapterCapabilities } from '../adapters/adapter'
|
||||
/**
|
||||
* @file acl-adapter.ts
|
||||
* @description ACL adapter that wraps a base adapter with access control
|
||||
*/
|
||||
|
||||
import type { DBALAdapter, AdapterCapabilities } from './adapter'
|
||||
import type { ListOptions, ListResult } from '../core/foundation/types'
|
||||
import { DBALError } from '../core/foundation/errors'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
username: string
|
||||
role: 'user' | 'admin' | 'god' | 'supergod'
|
||||
}
|
||||
|
||||
interface ACLRule {
|
||||
entity: string
|
||||
roles: string[]
|
||||
operations: string[]
|
||||
rowLevelFilter?: (user: User, data: Record<string, unknown>) => boolean
|
||||
}
|
||||
|
||||
const defaultACLRules: ACLRule[] = [
|
||||
{
|
||||
entity: 'User',
|
||||
roles: ['user'],
|
||||
operations: ['read', 'update'],
|
||||
rowLevelFilter: (user, data) => data.id === user.id
|
||||
},
|
||||
{
|
||||
entity: 'User',
|
||||
roles: ['admin', 'god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'PageView',
|
||||
roles: ['user', 'admin', 'god', 'supergod'],
|
||||
operations: ['read', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'PageView',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'update', 'delete']
|
||||
},
|
||||
{
|
||||
entity: 'ComponentHierarchy',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Workflow',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'LuaScript',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Package',
|
||||
roles: ['admin', 'god', 'supergod'],
|
||||
operations: ['read', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Package',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'update', 'delete']
|
||||
},
|
||||
]
|
||||
import type { User, ACLRule } from './acl/types'
|
||||
import { resolvePermissionOperation } from './acl/resolve-permission-operation'
|
||||
import { checkPermission } from './acl/check-permission'
|
||||
import { checkRowLevelAccess } from './acl/check-row-level-access'
|
||||
import { logAudit } from './acl/audit-logger'
|
||||
import { defaultACLRules } from './acl/default-rules'
|
||||
|
||||
export class ACLAdapter implements DBALAdapter {
|
||||
private baseAdapter: DBALAdapter
|
||||
@@ -84,361 +32,214 @@ export class ACLAdapter implements DBALAdapter {
|
||||
this.auditLog = options?.auditLog ?? true
|
||||
}
|
||||
|
||||
private resolvePermissionOperation(operation: string): string {
|
||||
switch (operation) {
|
||||
case 'findFirst':
|
||||
case 'findByField':
|
||||
return 'read'
|
||||
case 'createMany':
|
||||
return 'create'
|
||||
case 'updateByField':
|
||||
case 'updateMany':
|
||||
return 'update'
|
||||
case 'deleteByField':
|
||||
case 'deleteMany':
|
||||
return 'delete'
|
||||
default:
|
||||
return operation
|
||||
private log(entity: string, operation: string, success: boolean, message?: string): void {
|
||||
if (this.auditLog) {
|
||||
logAudit(entity, operation, success, this.user, message)
|
||||
}
|
||||
}
|
||||
|
||||
private checkPermission(entity: string, operation: string): void {
|
||||
const matchingRules = this.rules.filter(rule =>
|
||||
rule.entity === entity &&
|
||||
rule.roles.includes(this.user.role) &&
|
||||
rule.operations.includes(operation)
|
||||
)
|
||||
|
||||
if (matchingRules.length === 0) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, operation, false, 'Permission denied')
|
||||
}
|
||||
throw DBALError.forbidden(
|
||||
`User ${this.user.username} (${this.user.role}) cannot ${operation} ${entity}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private checkRowLevelAccess(
|
||||
entity: string,
|
||||
operation: string,
|
||||
data: Record<string, unknown>
|
||||
): void {
|
||||
const matchingRules = this.rules.filter(rule =>
|
||||
rule.entity === entity &&
|
||||
rule.roles.includes(this.user.role) &&
|
||||
rule.operations.includes(operation) &&
|
||||
rule.rowLevelFilter
|
||||
)
|
||||
|
||||
for (const rule of matchingRules) {
|
||||
if (rule.rowLevelFilter && !rule.rowLevelFilter(this.user, data)) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, operation, false, 'Row-level access denied')
|
||||
}
|
||||
throw DBALError.forbidden(
|
||||
`Row-level access denied for ${entity}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private logAudit(
|
||||
entity: string,
|
||||
operation: string,
|
||||
success: boolean,
|
||||
message?: string
|
||||
): void {
|
||||
const logEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
user: this.user.username,
|
||||
userId: this.user.id,
|
||||
role: this.user.role,
|
||||
entity,
|
||||
operation,
|
||||
success,
|
||||
message
|
||||
}
|
||||
console.log('[DBAL Audit]', JSON.stringify(logEntry))
|
||||
}
|
||||
|
||||
async create(entity: string, data: Record<string, unknown>): Promise<unknown> {
|
||||
this.checkPermission(entity, 'create')
|
||||
const operation = 'create'
|
||||
checkPermission(entity, operation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.create(entity, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'create', true)
|
||||
}
|
||||
this.log(entity, operation, true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'create', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, operation, false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async read(entity: string, id: string): Promise<unknown | null> {
|
||||
this.checkPermission(entity, 'read')
|
||||
const operation = 'read'
|
||||
checkPermission(entity, operation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.read(entity, id)
|
||||
|
||||
if (result) {
|
||||
this.checkRowLevelAccess(entity, 'read', result as Record<string, unknown>)
|
||||
}
|
||||
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'read', true)
|
||||
checkRowLevelAccess(entity, operation, result as Record<string, unknown>, this.user, this.rules, this.log.bind(this))
|
||||
}
|
||||
this.log(entity, operation, true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'read', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, operation, false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async update(entity: string, id: string, data: Record<string, unknown>): Promise<unknown> {
|
||||
this.checkPermission(entity, 'update')
|
||||
const operation = 'update'
|
||||
checkPermission(entity, operation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
const existing = await this.baseAdapter.read(entity, id)
|
||||
if (existing) {
|
||||
this.checkRowLevelAccess(entity, 'update', existing as Record<string, unknown>)
|
||||
checkRowLevelAccess(entity, operation, existing as Record<string, unknown>, this.user, this.rules, this.log.bind(this))
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.update(entity, id, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'update', true)
|
||||
}
|
||||
this.log(entity, operation, true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'update', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, operation, false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async delete(entity: string, id: string): Promise<boolean> {
|
||||
this.checkPermission(entity, 'delete')
|
||||
const operation = 'delete'
|
||||
checkPermission(entity, operation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
const existing = await this.baseAdapter.read(entity, id)
|
||||
if (existing) {
|
||||
this.checkRowLevelAccess(entity, 'delete', existing as Record<string, unknown>)
|
||||
checkRowLevelAccess(entity, operation, existing as Record<string, unknown>, this.user, this.rules, this.log.bind(this))
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.delete(entity, id)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'delete', true)
|
||||
}
|
||||
this.log(entity, operation, true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'delete', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, operation, false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async list(entity: string, options?: ListOptions): Promise<ListResult<unknown>> {
|
||||
this.checkPermission(entity, 'list')
|
||||
const operation = 'list'
|
||||
checkPermission(entity, operation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.list(entity, options)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'list', true)
|
||||
}
|
||||
this.log(entity, operation, true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'list', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, operation, false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async findFirst(entity: string, filter?: Record<string, unknown>): Promise<unknown | null> {
|
||||
const permissionOperation = this.resolvePermissionOperation('findFirst')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const resolvedOperation = resolvePermissionOperation('findFirst')
|
||||
checkPermission(entity, resolvedOperation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.findFirst(entity, filter)
|
||||
if (result) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, result as Record<string, unknown>)
|
||||
}
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'findFirst', true)
|
||||
checkRowLevelAccess(entity, resolvedOperation, result as Record<string, unknown>, this.user, this.rules, this.log.bind(this))
|
||||
}
|
||||
this.log(entity, 'findFirst', true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'findFirst', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, 'findFirst', false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async findByField(entity: string, field: string, value: unknown): Promise<unknown | null> {
|
||||
const permissionOperation = this.resolvePermissionOperation('findByField')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const resolvedOperation = resolvePermissionOperation('findByField')
|
||||
checkPermission(entity, resolvedOperation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.findByField(entity, field, value)
|
||||
if (result) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, result as Record<string, unknown>)
|
||||
}
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'findByField', true)
|
||||
checkRowLevelAccess(entity, resolvedOperation, result as Record<string, unknown>, this.user, this.rules, this.log.bind(this))
|
||||
}
|
||||
this.log(entity, 'findByField', true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'findByField', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, 'findByField', false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async upsert(
|
||||
entity: string,
|
||||
uniqueField: string,
|
||||
uniqueValue: unknown,
|
||||
filter: Record<string, unknown>,
|
||||
createData: Record<string, unknown>,
|
||||
updateData: Record<string, unknown>
|
||||
): Promise<unknown> {
|
||||
checkPermission(entity, 'create', this.user, this.rules, this.log.bind(this))
|
||||
checkPermission(entity, 'update', this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const existing = await this.baseAdapter.findByField(entity, uniqueField, uniqueValue)
|
||||
if (existing) {
|
||||
this.checkPermission(entity, 'update')
|
||||
this.checkRowLevelAccess(entity, 'update', existing as Record<string, unknown>)
|
||||
} else {
|
||||
this.checkPermission(entity, 'create')
|
||||
}
|
||||
|
||||
const result = await this.baseAdapter.upsert(entity, uniqueField, uniqueValue, createData, updateData)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'upsert', true)
|
||||
}
|
||||
const result = await this.baseAdapter.upsert(entity, filter, createData, updateData)
|
||||
this.log(entity, 'upsert', true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'upsert', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, 'upsert', false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async updateByField(entity: string, field: string, value: unknown, data: Record<string, unknown>): Promise<unknown> {
|
||||
const permissionOperation = this.resolvePermissionOperation('updateByField')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const existing = await this.baseAdapter.findByField(entity, field, value)
|
||||
if (existing) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, existing as Record<string, unknown>)
|
||||
}
|
||||
|
||||
const resolvedOperation = resolvePermissionOperation('updateByField')
|
||||
checkPermission(entity, resolvedOperation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.updateByField(entity, field, value, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'updateByField', true)
|
||||
}
|
||||
this.log(entity, 'updateByField', true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'updateByField', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, 'updateByField', false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async deleteByField(entity: string, field: string, value: unknown): Promise<boolean> {
|
||||
const permissionOperation = this.resolvePermissionOperation('deleteByField')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const existing = await this.baseAdapter.findByField(entity, field, value)
|
||||
if (existing) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, existing as Record<string, unknown>)
|
||||
}
|
||||
|
||||
const resolvedOperation = resolvePermissionOperation('deleteByField')
|
||||
checkPermission(entity, resolvedOperation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.deleteByField(entity, field, value)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'deleteByField', true)
|
||||
}
|
||||
this.log(entity, 'deleteByField', true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'deleteByField', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, 'deleteByField', false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async createMany(entity: string, data: Record<string, unknown>[]): Promise<number> {
|
||||
const permissionOperation = this.resolvePermissionOperation('createMany')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const resolvedOperation = resolvePermissionOperation('createMany')
|
||||
checkPermission(entity, resolvedOperation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.createMany(entity, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'createMany', true)
|
||||
}
|
||||
this.log(entity, 'createMany', true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'createMany', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, 'createMany', false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async updateMany(entity: string, filter: Record<string, unknown>, data: Record<string, unknown>): Promise<number> {
|
||||
const permissionOperation = this.resolvePermissionOperation('updateMany')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const listResult = await this.baseAdapter.list(entity, { filter })
|
||||
for (const item of listResult.data) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, item as Record<string, unknown>)
|
||||
}
|
||||
|
||||
const resolvedOperation = resolvePermissionOperation('updateMany')
|
||||
checkPermission(entity, resolvedOperation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.updateMany(entity, filter, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'updateMany', true)
|
||||
}
|
||||
this.log(entity, 'updateMany', true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'updateMany', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, 'updateMany', false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async deleteMany(entity: string, filter?: Record<string, unknown>): Promise<number> {
|
||||
const permissionOperation = this.resolvePermissionOperation('deleteMany')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const listResult = await this.baseAdapter.list(entity, { filter })
|
||||
for (const item of listResult.data) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, item as Record<string, unknown>)
|
||||
}
|
||||
|
||||
const resolvedOperation = resolvePermissionOperation('deleteMany')
|
||||
checkPermission(entity, resolvedOperation, this.user, this.rules, this.log.bind(this))
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.deleteMany(entity, filter)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'deleteMany', true)
|
||||
}
|
||||
this.log(entity, 'deleteMany', true)
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'deleteMany', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
this.log(entity, 'deleteMany', false, (error as Error).message)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -451,3 +252,7 @@ export class ACLAdapter implements DBALAdapter {
|
||||
await this.baseAdapter.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Re-export types for convenience
|
||||
export type { User, ACLRule } from './acl/types'
|
||||
export { defaultACLRules } from './acl/default-rules'
|
||||
|
||||
453
dbal/development/src/adapters/acl-adapter.ts.backup
Normal file
453
dbal/development/src/adapters/acl-adapter.ts.backup
Normal file
@@ -0,0 +1,453 @@
|
||||
import type { DBALAdapter, AdapterCapabilities } from '../adapters/adapter'
|
||||
import type { ListOptions, ListResult } from '../core/foundation/types'
|
||||
import { DBALError } from '../core/foundation/errors'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
username: string
|
||||
role: 'user' | 'admin' | 'god' | 'supergod'
|
||||
}
|
||||
|
||||
interface ACLRule {
|
||||
entity: string
|
||||
roles: string[]
|
||||
operations: string[]
|
||||
rowLevelFilter?: (user: User, data: Record<string, unknown>) => boolean
|
||||
}
|
||||
|
||||
const defaultACLRules: ACLRule[] = [
|
||||
{
|
||||
entity: 'User',
|
||||
roles: ['user'],
|
||||
operations: ['read', 'update'],
|
||||
rowLevelFilter: (user, data) => data.id === user.id
|
||||
},
|
||||
{
|
||||
entity: 'User',
|
||||
roles: ['admin', 'god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'PageView',
|
||||
roles: ['user', 'admin', 'god', 'supergod'],
|
||||
operations: ['read', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'PageView',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'update', 'delete']
|
||||
},
|
||||
{
|
||||
entity: 'ComponentHierarchy',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Workflow',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'LuaScript',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Package',
|
||||
roles: ['admin', 'god', 'supergod'],
|
||||
operations: ['read', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Package',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'update', 'delete']
|
||||
},
|
||||
]
|
||||
|
||||
export class ACLAdapter implements DBALAdapter {
|
||||
private baseAdapter: DBALAdapter
|
||||
private user: User
|
||||
private rules: ACLRule[]
|
||||
private auditLog: boolean
|
||||
|
||||
constructor(
|
||||
baseAdapter: DBALAdapter,
|
||||
user: User,
|
||||
options?: {
|
||||
rules?: ACLRule[]
|
||||
auditLog?: boolean
|
||||
}
|
||||
) {
|
||||
this.baseAdapter = baseAdapter
|
||||
this.user = user
|
||||
this.rules = options?.rules || defaultACLRules
|
||||
this.auditLog = options?.auditLog ?? true
|
||||
}
|
||||
|
||||
private resolvePermissionOperation(operation: string): string {
|
||||
switch (operation) {
|
||||
case 'findFirst':
|
||||
case 'findByField':
|
||||
return 'read'
|
||||
case 'createMany':
|
||||
return 'create'
|
||||
case 'updateByField':
|
||||
case 'updateMany':
|
||||
return 'update'
|
||||
case 'deleteByField':
|
||||
case 'deleteMany':
|
||||
return 'delete'
|
||||
default:
|
||||
return operation
|
||||
}
|
||||
}
|
||||
|
||||
private checkPermission(entity: string, operation: string): void {
|
||||
const matchingRules = this.rules.filter(rule =>
|
||||
rule.entity === entity &&
|
||||
rule.roles.includes(this.user.role) &&
|
||||
rule.operations.includes(operation)
|
||||
)
|
||||
|
||||
if (matchingRules.length === 0) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, operation, false, 'Permission denied')
|
||||
}
|
||||
throw DBALError.forbidden(
|
||||
`User ${this.user.username} (${this.user.role}) cannot ${operation} ${entity}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private checkRowLevelAccess(
|
||||
entity: string,
|
||||
operation: string,
|
||||
data: Record<string, unknown>
|
||||
): void {
|
||||
const matchingRules = this.rules.filter(rule =>
|
||||
rule.entity === entity &&
|
||||
rule.roles.includes(this.user.role) &&
|
||||
rule.operations.includes(operation) &&
|
||||
rule.rowLevelFilter
|
||||
)
|
||||
|
||||
for (const rule of matchingRules) {
|
||||
if (rule.rowLevelFilter && !rule.rowLevelFilter(this.user, data)) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, operation, false, 'Row-level access denied')
|
||||
}
|
||||
throw DBALError.forbidden(
|
||||
`Row-level access denied for ${entity}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private logAudit(
|
||||
entity: string,
|
||||
operation: string,
|
||||
success: boolean,
|
||||
message?: string
|
||||
): void {
|
||||
const logEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
user: this.user.username,
|
||||
userId: this.user.id,
|
||||
role: this.user.role,
|
||||
entity,
|
||||
operation,
|
||||
success,
|
||||
message
|
||||
}
|
||||
console.log('[DBAL Audit]', JSON.stringify(logEntry))
|
||||
}
|
||||
|
||||
async create(entity: string, data: Record<string, unknown>): Promise<unknown> {
|
||||
this.checkPermission(entity, 'create')
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.create(entity, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'create', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'create', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async read(entity: string, id: string): Promise<unknown | null> {
|
||||
this.checkPermission(entity, 'read')
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.read(entity, id)
|
||||
|
||||
if (result) {
|
||||
this.checkRowLevelAccess(entity, 'read', result as Record<string, unknown>)
|
||||
}
|
||||
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'read', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'read', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async update(entity: string, id: string, data: Record<string, unknown>): Promise<unknown> {
|
||||
this.checkPermission(entity, 'update')
|
||||
|
||||
const existing = await this.baseAdapter.read(entity, id)
|
||||
if (existing) {
|
||||
this.checkRowLevelAccess(entity, 'update', existing as Record<string, unknown>)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.update(entity, id, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'update', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'update', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async delete(entity: string, id: string): Promise<boolean> {
|
||||
this.checkPermission(entity, 'delete')
|
||||
|
||||
const existing = await this.baseAdapter.read(entity, id)
|
||||
if (existing) {
|
||||
this.checkRowLevelAccess(entity, 'delete', existing as Record<string, unknown>)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.delete(entity, id)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'delete', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'delete', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async list(entity: string, options?: ListOptions): Promise<ListResult<unknown>> {
|
||||
this.checkPermission(entity, 'list')
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.list(entity, options)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'list', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'list', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async findFirst(entity: string, filter?: Record<string, unknown>): Promise<unknown | null> {
|
||||
const permissionOperation = this.resolvePermissionOperation('findFirst')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.findFirst(entity, filter)
|
||||
if (result) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, result as Record<string, unknown>)
|
||||
}
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'findFirst', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'findFirst', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async findByField(entity: string, field: string, value: unknown): Promise<unknown | null> {
|
||||
const permissionOperation = this.resolvePermissionOperation('findByField')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.findByField(entity, field, value)
|
||||
if (result) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, result as Record<string, unknown>)
|
||||
}
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'findByField', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'findByField', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async upsert(
|
||||
entity: string,
|
||||
uniqueField: string,
|
||||
uniqueValue: unknown,
|
||||
createData: Record<string, unknown>,
|
||||
updateData: Record<string, unknown>
|
||||
): Promise<unknown> {
|
||||
try {
|
||||
const existing = await this.baseAdapter.findByField(entity, uniqueField, uniqueValue)
|
||||
if (existing) {
|
||||
this.checkPermission(entity, 'update')
|
||||
this.checkRowLevelAccess(entity, 'update', existing as Record<string, unknown>)
|
||||
} else {
|
||||
this.checkPermission(entity, 'create')
|
||||
}
|
||||
|
||||
const result = await this.baseAdapter.upsert(entity, uniqueField, uniqueValue, createData, updateData)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'upsert', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'upsert', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async updateByField(entity: string, field: string, value: unknown, data: Record<string, unknown>): Promise<unknown> {
|
||||
const permissionOperation = this.resolvePermissionOperation('updateByField')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const existing = await this.baseAdapter.findByField(entity, field, value)
|
||||
if (existing) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, existing as Record<string, unknown>)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.updateByField(entity, field, value, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'updateByField', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'updateByField', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async deleteByField(entity: string, field: string, value: unknown): Promise<boolean> {
|
||||
const permissionOperation = this.resolvePermissionOperation('deleteByField')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const existing = await this.baseAdapter.findByField(entity, field, value)
|
||||
if (existing) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, existing as Record<string, unknown>)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.deleteByField(entity, field, value)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'deleteByField', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'deleteByField', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async createMany(entity: string, data: Record<string, unknown>[]): Promise<number> {
|
||||
const permissionOperation = this.resolvePermissionOperation('createMany')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.createMany(entity, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'createMany', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'createMany', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async updateMany(entity: string, filter: Record<string, unknown>, data: Record<string, unknown>): Promise<number> {
|
||||
const permissionOperation = this.resolvePermissionOperation('updateMany')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const listResult = await this.baseAdapter.list(entity, { filter })
|
||||
for (const item of listResult.data) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, item as Record<string, unknown>)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.updateMany(entity, filter, data)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'updateMany', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'updateMany', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async deleteMany(entity: string, filter?: Record<string, unknown>): Promise<number> {
|
||||
const permissionOperation = this.resolvePermissionOperation('deleteMany')
|
||||
this.checkPermission(entity, permissionOperation)
|
||||
|
||||
const listResult = await this.baseAdapter.list(entity, { filter })
|
||||
for (const item of listResult.data) {
|
||||
this.checkRowLevelAccess(entity, permissionOperation, item as Record<string, unknown>)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.baseAdapter.deleteMany(entity, filter)
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'deleteMany', true)
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
if (this.auditLog) {
|
||||
this.logAudit(entity, 'deleteMany', false, error instanceof Error ? error.message : 'Unknown error')
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getCapabilities(): Promise<AdapterCapabilities> {
|
||||
return this.baseAdapter.getCapabilities()
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.baseAdapter.close()
|
||||
}
|
||||
}
|
||||
29
dbal/development/src/adapters/acl/audit-logger.ts
Normal file
29
dbal/development/src/adapters/acl/audit-logger.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* @file audit-logger.ts
|
||||
* @description Audit logging for ACL operations
|
||||
*/
|
||||
|
||||
import type { User } from './types'
|
||||
|
||||
/**
|
||||
* Log audit entry for ACL operation
|
||||
*/
|
||||
export const logAudit = (
|
||||
entity: string,
|
||||
operation: string,
|
||||
success: boolean,
|
||||
user: User,
|
||||
message?: string
|
||||
): void => {
|
||||
const logEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
user: user.username,
|
||||
userId: user.id,
|
||||
role: user.role,
|
||||
entity,
|
||||
operation,
|
||||
success,
|
||||
message
|
||||
}
|
||||
console.log('[DBAL Audit]', JSON.stringify(logEntry))
|
||||
}
|
||||
34
dbal/development/src/adapters/acl/check-permission.ts
Normal file
34
dbal/development/src/adapters/acl/check-permission.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* @file check-permission.ts
|
||||
* @description Check if user has permission for entity operation
|
||||
*/
|
||||
|
||||
import { DBALError } from '../../core/foundation/errors'
|
||||
import type { User, ACLRule } from './types'
|
||||
|
||||
/**
|
||||
* Check if user has permission to perform operation on entity
|
||||
* @throws DBALError.forbidden if permission denied
|
||||
*/
|
||||
export const checkPermission = (
|
||||
entity: string,
|
||||
operation: string,
|
||||
user: User,
|
||||
rules: ACLRule[],
|
||||
logFn?: (entity: string, operation: string, success: boolean, message?: string) => void
|
||||
): void => {
|
||||
const matchingRules = rules.filter(rule =>
|
||||
rule.entity === entity &&
|
||||
rule.roles.includes(user.role) &&
|
||||
rule.operations.includes(operation)
|
||||
)
|
||||
|
||||
if (matchingRules.length === 0) {
|
||||
if (logFn) {
|
||||
logFn(entity, operation, false, 'Permission denied')
|
||||
}
|
||||
throw DBALError.forbidden(
|
||||
`User ${user.username} (${user.role}) cannot ${operation} ${entity}`
|
||||
)
|
||||
}
|
||||
}
|
||||
38
dbal/development/src/adapters/acl/check-row-level-access.ts
Normal file
38
dbal/development/src/adapters/acl/check-row-level-access.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @file check-row-level-access.ts
|
||||
* @description Check row-level access permissions
|
||||
*/
|
||||
|
||||
import { DBALError } from '../../core/foundation/errors'
|
||||
import type { User, ACLRule } from './types'
|
||||
|
||||
/**
|
||||
* Check row-level access for specific data
|
||||
* @throws DBALError.forbidden if row-level access denied
|
||||
*/
|
||||
export const checkRowLevelAccess = (
|
||||
entity: string,
|
||||
operation: string,
|
||||
data: Record<string, unknown>,
|
||||
user: User,
|
||||
rules: ACLRule[],
|
||||
logFn?: (entity: string, operation: string, success: boolean, message?: string) => void
|
||||
): void => {
|
||||
const matchingRules = rules.filter(rule =>
|
||||
rule.entity === entity &&
|
||||
rule.roles.includes(user.role) &&
|
||||
rule.operations.includes(operation) &&
|
||||
rule.rowLevelFilter
|
||||
)
|
||||
|
||||
for (const rule of matchingRules) {
|
||||
if (rule.rowLevelFilter && !rule.rowLevelFilter(user, data)) {
|
||||
if (logFn) {
|
||||
logFn(entity, operation, false, 'Row-level access denied')
|
||||
}
|
||||
throw DBALError.forbidden(
|
||||
`Row-level access denied for ${entity}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
55
dbal/development/src/adapters/acl/default-rules.ts
Normal file
55
dbal/development/src/adapters/acl/default-rules.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @file default-rules.ts
|
||||
* @description Default ACL rules for entities
|
||||
*/
|
||||
|
||||
import type { ACLRule } from './types'
|
||||
|
||||
export const defaultACLRules: ACLRule[] = [
|
||||
{
|
||||
entity: 'User',
|
||||
roles: ['user'],
|
||||
operations: ['read', 'update'],
|
||||
rowLevelFilter: (user, data) => data.id === user.id
|
||||
},
|
||||
{
|
||||
entity: 'User',
|
||||
roles: ['admin', 'god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'PageView',
|
||||
roles: ['user', 'admin', 'god', 'supergod'],
|
||||
operations: ['read', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'PageView',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'update', 'delete']
|
||||
},
|
||||
{
|
||||
entity: 'ComponentHierarchy',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Workflow',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'LuaScript',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'read', 'update', 'delete', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Package',
|
||||
roles: ['admin', 'god', 'supergod'],
|
||||
operations: ['read', 'list']
|
||||
},
|
||||
{
|
||||
entity: 'Package',
|
||||
roles: ['god', 'supergod'],
|
||||
operations: ['create', 'update', 'delete']
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @file resolve-permission-operation.ts
|
||||
* @description Resolve DBAL operation to ACL permission operation
|
||||
*/
|
||||
|
||||
/**
|
||||
* Maps complex DBAL operations to their base permission operations
|
||||
*/
|
||||
export const resolvePermissionOperation = (operation: string): string => {
|
||||
switch (operation) {
|
||||
case 'findFirst':
|
||||
case 'findByField':
|
||||
return 'read'
|
||||
case 'createMany':
|
||||
return 'create'
|
||||
case 'updateByField':
|
||||
case 'updateMany':
|
||||
return 'update'
|
||||
case 'deleteByField':
|
||||
case 'deleteMany':
|
||||
return 'delete'
|
||||
default:
|
||||
return operation
|
||||
}
|
||||
}
|
||||
17
dbal/development/src/adapters/acl/types.ts
Normal file
17
dbal/development/src/adapters/acl/types.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @file types.ts
|
||||
* @description Type definitions for ACL adapter
|
||||
*/
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
username: string
|
||||
role: 'user' | 'admin' | 'god' | 'supergod'
|
||||
}
|
||||
|
||||
export interface ACLRule {
|
||||
entity: string
|
||||
roles: string[]
|
||||
operations: string[]
|
||||
rowLevelFilter?: (user: User, data: Record<string, unknown>) => boolean
|
||||
}
|
||||
Reference in New Issue
Block a user