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:
copilot-swe-agent[bot]
2025-12-27 14:36:54 +00:00
parent aac7d1f4d4
commit 672038938b
8 changed files with 737 additions and 281 deletions

View File

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

View 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()
}
}

View 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))
}

View 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}`
)
}
}

View 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}`
)
}
}
}

View 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']
},
]

View File

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

View 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
}