diff --git a/dbal/development/src/adapters/acl-adapter.ts b/dbal/development/src/adapters/acl-adapter.ts index a5bfa02e5..9fbcd7d83 100644 --- a/dbal/development/src/adapters/acl-adapter.ts +++ b/dbal/development/src/adapters/acl-adapter.ts @@ -1,3 +1,3 @@ -export { ACLAdapter } from './acl-adapter/index' -export type { User, ACLRule } from './acl/types' +export { ACLAdapter } from './acl-adapter' +export type { ACLAdapterOptions, ACLContext, ACLRule, User } from './acl-adapter/types' export { defaultACLRules } from './acl/default-rules' diff --git a/dbal/development/src/adapters/acl-adapter/acl-adapter.ts b/dbal/development/src/adapters/acl-adapter/acl-adapter.ts new file mode 100644 index 000000000..9d2492451 --- /dev/null +++ b/dbal/development/src/adapters/acl-adapter/acl-adapter.ts @@ -0,0 +1,86 @@ +import type { AdapterCapabilities, DBALAdapter } from '../adapter' +import type { ListOptions, ListResult } from '../../core/foundation/types' +import { createContext } from './context' +import { createReadStrategy } from './read-strategy' +import { createWriteStrategy } from './write-strategy' +import type { ACLAdapterOptions, ACLContext, ACLRule, User } from './types' + +export class ACLAdapter implements DBALAdapter { + private readonly context: ACLContext + private readonly readStrategy: ReturnType + private readonly writeStrategy: ReturnType + + constructor(baseAdapter: DBALAdapter, user: User, options?: ACLAdapterOptions) { + this.context = createContext(baseAdapter, user, options) + this.readStrategy = createReadStrategy(this.context) + this.writeStrategy = createWriteStrategy(this.context) + } + + async create(entity: string, data: Record): Promise { + return this.writeStrategy.create(entity, data) + } + + async read(entity: string, id: string): Promise { + return this.readStrategy.read(entity, id) + } + + async update(entity: string, id: string, data: Record): Promise { + return this.writeStrategy.update(entity, id, data) + } + + async delete(entity: string, id: string): Promise { + return this.writeStrategy.delete(entity, id) + } + + async list(entity: string, options?: ListOptions): Promise> { + return this.readStrategy.list(entity, options) + } + + async findFirst(entity: string, filter?: Record): Promise { + return this.readStrategy.findFirst(entity, filter) + } + + async findByField(entity: string, field: string, value: unknown): Promise { + return this.readStrategy.findByField(entity, field, value) + } + + async upsert( + entity: string, + filter: Record, + createData: Record, + updateData: Record, + ): Promise { + return this.writeStrategy.upsert(entity, filter, createData, updateData) + } + + async updateByField(entity: string, field: string, value: unknown, data: Record): Promise { + return this.writeStrategy.updateByField(entity, field, value, data) + } + + async deleteByField(entity: string, field: string, value: unknown): Promise { + return this.writeStrategy.deleteByField(entity, field, value) + } + + async createMany(entity: string, data: Record[]): Promise { + return this.writeStrategy.createMany(entity, data) + } + + async updateMany(entity: string, filter: Record, data: Record): Promise { + return this.writeStrategy.updateMany(entity, filter, data) + } + + async deleteMany(entity: string, filter?: Record): Promise { + return this.writeStrategy.deleteMany(entity, filter) + } + + async getCapabilities(): Promise { + return this.context.baseAdapter.getCapabilities() + } + + async close(): Promise { + await this.context.baseAdapter.close() + } +} + +export type { ACLAdapterOptions, ACLContext, ACLRule, User } +export { defaultACLRules } from '../acl/default-rules' diff --git a/dbal/development/src/adapters/acl-adapter/context.ts b/dbal/development/src/adapters/acl-adapter/context.ts index 9262dd64d..8213926b9 100644 --- a/dbal/development/src/adapters/acl-adapter/context.ts +++ b/dbal/development/src/adapters/acl-adapter/context.ts @@ -1,20 +1,12 @@ import type { DBALAdapter } from '../adapter' -import type { User, ACLRule } from '../acl/types' +import type { ACLAdapterOptions, ACLContext, ACLRule, User } from './types' import { logAudit } from '../acl/audit-logger' import { defaultACLRules } from '../acl/default-rules' -export interface ACLContext { - baseAdapter: DBALAdapter - user: User - rules: ACLRule[] - auditLog: boolean - logger: (entity: string, operation: string, success: boolean, message?: string) => void -} - export const createContext = ( baseAdapter: DBALAdapter, user: User, - options?: { rules?: ACLRule[]; auditLog?: boolean }, + options?: ACLAdapterOptions, ): ACLContext => { const auditLog = options?.auditLog ?? true const rules = options?.rules || defaultACLRules diff --git a/dbal/development/src/adapters/acl-adapter/guards.ts b/dbal/development/src/adapters/acl-adapter/guards.ts index 8da05a011..be5171354 100644 --- a/dbal/development/src/adapters/acl-adapter/guards.ts +++ b/dbal/development/src/adapters/acl-adapter/guards.ts @@ -1,7 +1,7 @@ import { checkPermission } from '../acl/check-permission' import { checkRowLevelAccess } from '../acl/check-row-level-access' import { resolvePermissionOperation } from '../acl/resolve-permission-operation' -import type { ACLContext } from './context' +import type { ACLContext } from './types' export const enforcePermission = (context: ACLContext, entity: string, operation: string) => { checkPermission(entity, operation, context.user, context.rules, context.logger) diff --git a/dbal/development/src/adapters/acl-adapter/index.ts b/dbal/development/src/adapters/acl-adapter/index.ts index 354fe2a58..b356927a7 100644 --- a/dbal/development/src/adapters/acl-adapter/index.ts +++ b/dbal/development/src/adapters/acl-adapter/index.ts @@ -1,92 +1,3 @@ -import type { AdapterCapabilities, DBALAdapter } from '../adapter' -import type { ListOptions, ListResult } from '../../core/foundation/types' -import type { User, ACLRule } from '../acl/types' -import type { ACLContext } from './context' -import { createContext } from './context' -import { createEntity, deleteEntity, listEntities, readEntity, updateEntity } from './crud' -import { - createMany, - deleteByField, - deleteMany, - findByField, - findFirst, - updateByField, - updateMany, - upsert, -} from './bulk' - -export class ACLAdapter implements DBALAdapter { - private readonly context: ACLContext - - constructor(baseAdapter: DBALAdapter, user: User, options?: { rules?: ACLRule[]; auditLog?: boolean }) { - this.context = createContext(baseAdapter, user, options) - } - - async create(entity: string, data: Record): Promise { - return createEntity(this.context)(entity, data) - } - - async read(entity: string, id: string): Promise { - return readEntity(this.context)(entity, id) - } - - async update(entity: string, id: string, data: Record): Promise { - return updateEntity(this.context)(entity, id, data) - } - - async delete(entity: string, id: string): Promise { - return deleteEntity(this.context)(entity, id) - } - - async list(entity: string, options?: ListOptions): Promise> { - return listEntities(this.context)(entity, options) - } - - async findFirst(entity: string, filter?: Record): Promise { - return findFirst(this.context)(entity, filter) - } - - async findByField(entity: string, field: string, value: unknown): Promise { - return findByField(this.context)(entity, field, value) - } - - async upsert( - entity: string, - filter: Record, - createData: Record, - updateData: Record, - ): Promise { - return upsert(this.context)(entity, filter, createData, updateData) - } - - async updateByField(entity: string, field: string, value: unknown, data: Record): Promise { - return updateByField(this.context)(entity, field, value, data) - } - - async deleteByField(entity: string, field: string, value: unknown): Promise { - return deleteByField(this.context)(entity, field, value) - } - - async createMany(entity: string, data: Record[]): Promise { - return createMany(this.context)(entity, data) - } - - async updateMany(entity: string, filter: Record, data: Record): Promise { - return updateMany(this.context)(entity, filter, data) - } - - async deleteMany(entity: string, filter?: Record): Promise { - return deleteMany(this.context)(entity, filter) - } - - async getCapabilities(): Promise { - return this.context.baseAdapter.getCapabilities() - } - - async close(): Promise { - await this.context.baseAdapter.close() - } -} - -export type { User, ACLRule } from './acl/types' -export { defaultACLRules } from './acl/default-rules' +export { ACLAdapter } from './acl-adapter' +export type { ACLAdapterOptions, ACLContext, ACLRule, User } from './types' +export { defaultACLRules } from '../acl/default-rules' diff --git a/dbal/development/src/adapters/acl-adapter/read-strategy.ts b/dbal/development/src/adapters/acl-adapter/read-strategy.ts new file mode 100644 index 000000000..da2742e26 --- /dev/null +++ b/dbal/development/src/adapters/acl-adapter/read-strategy.ts @@ -0,0 +1,48 @@ +import type { ListOptions, ListResult } from '../../core/foundation/types' +import { enforceRowAccess, resolveOperation, withAudit } from './guards' +import type { ACLContext } from './types' + +export const createReadStrategy = (context: ACLContext) => { + const read = async (entity: string, id: string): Promise => { + return withAudit(context, entity, 'read', async () => { + const result = await context.baseAdapter.read(entity, id) + if (result) { + enforceRowAccess(context, entity, 'read', result as Record) + } + return result + }) + } + + const list = async (entity: string, options?: ListOptions): Promise> => { + return withAudit(context, entity, 'list', () => context.baseAdapter.list(entity, options)) + } + + const findFirst = async (entity: string, filter?: Record): Promise => { + const operation = resolveOperation('findFirst') + return withAudit(context, entity, operation, async () => { + const result = await context.baseAdapter.findFirst(entity, filter) + if (result) { + enforceRowAccess(context, entity, operation, result as Record) + } + return result + }) + } + + const findByField = async (entity: string, field: string, value: unknown): Promise => { + const operation = resolveOperation('findByField') + return withAudit(context, entity, operation, async () => { + const result = await context.baseAdapter.findByField(entity, field, value) + if (result) { + enforceRowAccess(context, entity, operation, result as Record) + } + return result + }) + } + + return { + read, + list, + findFirst, + findByField, + } +} diff --git a/dbal/development/src/adapters/acl-adapter/types.ts b/dbal/development/src/adapters/acl-adapter/types.ts new file mode 100644 index 000000000..ea4cf1857 --- /dev/null +++ b/dbal/development/src/adapters/acl-adapter/types.ts @@ -0,0 +1,27 @@ +import type { DBALAdapter } from '../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) => boolean +} + +export interface ACLAdapterOptions { + rules?: ACLRule[] + auditLog?: boolean +} + +export interface ACLContext { + baseAdapter: DBALAdapter + user: User + rules: ACLRule[] + auditLog: boolean + logger: (entity: string, operation: string, success: boolean, message?: string) => void +} diff --git a/dbal/development/src/adapters/acl-adapter/write-strategy.ts b/dbal/development/src/adapters/acl-adapter/write-strategy.ts new file mode 100644 index 000000000..cf8b6aff5 --- /dev/null +++ b/dbal/development/src/adapters/acl-adapter/write-strategy.ts @@ -0,0 +1,83 @@ +import { enforceRowAccess, resolveOperation, withAudit } from './guards' +import type { ACLContext } from './types' + +export const createWriteStrategy = (context: ACLContext) => { + const create = async (entity: string, data: Record): Promise => { + return withAudit(context, entity, 'create', () => context.baseAdapter.create(entity, data)) + } + + const update = async (entity: string, id: string, data: Record): Promise => { + return withAudit(context, entity, 'update', async () => { + const existing = await context.baseAdapter.read(entity, id) + if (existing) { + enforceRowAccess(context, entity, 'update', existing as Record) + } + return context.baseAdapter.update(entity, id, data) + }) + } + + const remove = async (entity: string, id: string): Promise => { + return withAudit(context, entity, 'delete', async () => { + const existing = await context.baseAdapter.read(entity, id) + if (existing) { + enforceRowAccess(context, entity, 'delete', existing as Record) + } + return context.baseAdapter.delete(entity, id) + }) + } + + const upsert = async ( + entity: string, + filter: Record, + createData: Record, + updateData: Record, + ): Promise => { + return withAudit(context, entity, 'upsert', () => context.baseAdapter.upsert(entity, filter, createData, updateData)) + } + + const updateByField = async ( + entity: string, + field: string, + value: unknown, + data: Record, + ): Promise => { + const operation = resolveOperation('updateByField') + return withAudit(context, entity, operation, () => context.baseAdapter.updateByField(entity, field, value, data)) + } + + const deleteByField = async (entity: string, field: string, value: unknown): Promise => { + const operation = resolveOperation('deleteByField') + return withAudit(context, entity, operation, () => context.baseAdapter.deleteByField(entity, field, value)) + } + + const createMany = async (entity: string, data: Record[]): Promise => { + const operation = resolveOperation('createMany') + return withAudit(context, entity, operation, () => context.baseAdapter.createMany(entity, data)) + } + + const updateMany = async ( + entity: string, + filter: Record, + data: Record, + ): Promise => { + const operation = resolveOperation('updateMany') + return withAudit(context, entity, operation, () => context.baseAdapter.updateMany(entity, filter, data)) + } + + const deleteMany = async (entity: string, filter?: Record): Promise => { + const operation = resolveOperation('deleteMany') + return withAudit(context, entity, operation, () => context.baseAdapter.deleteMany(entity, filter)) + } + + return { + create, + update, + delete: remove, + upsert, + updateByField, + deleteByField, + createMany, + updateMany, + deleteMany, + } +} diff --git a/dbal/development/src/adapters/acl/audit-logger.ts b/dbal/development/src/adapters/acl/audit-logger.ts index f67a2736d..864c181e4 100644 --- a/dbal/development/src/adapters/acl/audit-logger.ts +++ b/dbal/development/src/adapters/acl/audit-logger.ts @@ -3,7 +3,7 @@ * @description Audit logging for ACL operations */ -import type { User } from './types' +import type { User } from '../acl-adapter/types' /** * Log audit entry for ACL operation diff --git a/dbal/development/src/adapters/acl/check-permission.ts b/dbal/development/src/adapters/acl/check-permission.ts index 3f1fd4a1b..b27a7b12d 100644 --- a/dbal/development/src/adapters/acl/check-permission.ts +++ b/dbal/development/src/adapters/acl/check-permission.ts @@ -4,7 +4,7 @@ */ import { DBALError } from '../../core/foundation/errors' -import type { User, ACLRule } from './types' +import type { ACLRule, User } from '../acl-adapter/types' /** * Check if user has permission to perform operation on entity diff --git a/dbal/development/src/adapters/acl/check-row-level-access.ts b/dbal/development/src/adapters/acl/check-row-level-access.ts index 3b3403205..70ea72fc7 100644 --- a/dbal/development/src/adapters/acl/check-row-level-access.ts +++ b/dbal/development/src/adapters/acl/check-row-level-access.ts @@ -4,7 +4,7 @@ */ import { DBALError } from '../../core/foundation/errors' -import type { User, ACLRule } from './types' +import type { ACLRule, User } from '../acl-adapter/types' /** * Check row-level access for specific data diff --git a/dbal/development/src/adapters/acl/default-rules.ts b/dbal/development/src/adapters/acl/default-rules.ts index a5ff3f3b0..25a6b803e 100644 --- a/dbal/development/src/adapters/acl/default-rules.ts +++ b/dbal/development/src/adapters/acl/default-rules.ts @@ -3,7 +3,7 @@ * @description Default ACL rules for entities */ -import type { ACLRule } from './types' +import type { ACLRule } from '../acl-adapter/types' export const defaultACLRules: ACLRule[] = [ {