Skip to content

Kernel Upgrade: ExecutionContext propagation, per-object hooks, middleware chain, plugin-security#594

Merged
hotlong merged 5 commits into
mainfrom
copilot/update-code-based-on-requirements
Feb 10, 2026
Merged

Kernel Upgrade: ExecutionContext propagation, per-object hooks, middleware chain, plugin-security#594
hotlong merged 5 commits into
mainfrom
copilot/update-code-based-on-requirements

Conversation

Copilot AI commented Feb 10, 2026

Copy link
Copy Markdown
Contributor

Implements the full execution context propagation chain specified in docs/objectql_roadmap.md — bridging the gap between plugin-auth (authentication) and data operations (authorization/context). Previously, HookContextSchema.session, PermissionSetSchema, and RowLevelSecurityPolicySchema were defined in spec but had zero runtime wiring.

Spec: ExecutionContext + Data Engine Context

  • New ExecutionContextSchema (packages/spec/src/kernel/execution-context.zod.ts) — userId, tenantId, roles, permissions, isSystem, accessToken, transaction, traceId
  • BaseEngineOptionsSchema with optional context field; all 6 DataEngine option schemas extend it
  • Added ./security subpath export to @objectstack/spec

ObjectQL Engine: Hooks, Middleware, Context

  • Per-object hooks with priority: registerHook(event, handler, { object?, priority? }) — replaces flat Record<string, HookHandler[]> with Map<string, HookEntry[]>, sorted by priority, with object-match filtering
  • Middleware chain (onion model): registerMiddleware(fn, { object? }) wraps all CRUD operations
  • Context propagation: options.contextHookContext.session + HookContext.transaction in all methods
  • MetadataFacade: injectable wrapper over SchemaRegistry, registered as metadata service
  • ObjectRepository + ScopedContext: engine.createContext({ userId, tenantId }).object('user').find()
// Context flows from HTTP layer to data operations
const results = await engine.find('account', {
  filter: { status: 'active' },
  context: { userId: 'u1', tenantId: 'org1', roles: ['sales'] }
});

// Or via scoped context
const ctx = engine.createContext({ userId: 'u1', tenantId: 'org1' });
const accounts = ctx.object('account');
await accounts.find({ filter: { status: 'active' } });

ObjectQLPlugin: Audit Hooks + Tenant Middleware

  • Auto-stamp created_by/modified_by/created_at/modified_at/space_id on insert/update (priority 10)
  • Auto-fetch previousData for update/delete hooks (priority 5)
  • Tenant isolation middleware: auto-inject space_id filter for non-system operations with tenantId

AuthPlugin

  • Registers auth middleware on ObjectQL engine in start() (placeholder for session resolution via AsyncLocalStorage)

New Package: @objectstack/plugin-security

  • PermissionEvaluator — resolves PermissionSets from roles, checks CRUD permissions (most-permissive merge)
  • RLSCompiler — compiles using expressions (field = current_user.prop, field IN (current_user.arr)) into ObjectQL filters
  • FieldMasker — strips non-readable fields from results, strips non-editable fields from writes
  • SecurityPlugin — registers as engine middleware: RBAC check → RLS filter injection → field masking

All changes are backward compatible — context is optional everywhere, existing tests unmodified and passing.

Original prompt

On objectql_roadmap.md, 参考此文件中的要求,修改代码


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@vercel

vercel Bot commented Feb 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
objectstack-play Ready Ready Preview, Comment Feb 10, 2026 4:19pm
spec Error Error Feb 10, 2026 4:19pm

Request Review

Copilot AI and others added 2 commits February 10, 2026 15:41
…ine options

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
…ry, audit hooks, tenant middleware

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI and others added 2 commits February 10, 2026 15:54
…field masker

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
… error handling

Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Copilot AI changed the title [WIP] Update code according to objectql_roadmap.md requirements Kernel Upgrade: ExecutionContext propagation, per-object hooks, middleware chain, plugin-security Feb 10, 2026
Copilot AI requested a review from hotlong February 10, 2026 16:02
@hotlong hotlong marked this pull request as ready for review February 10, 2026 16:03
Copilot AI review requested due to automatic review settings February 10, 2026 16:03
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file protocol:data tests tooling size/xl labels Feb 10, 2026
@hotlong hotlong merged commit 5b6fc1c into main Feb 10, 2026
16 of 18 checks passed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a comprehensive kernel upgrade that introduces execution context propagation, per-object hooks with priority, middleware chain (onion model), and a new security plugin. It bridges the authentication layer (plugin-auth) with data operations by flowing user identity, tenant context, and permissions through the entire stack.

Changes:

  • Adds ExecutionContext protocol to spec package with userId, tenantId, roles, permissions, and transaction support
  • Refactors ObjectQL engine to support per-object hooks with priority and middleware chain wrapping all CRUD operations
  • Introduces MetadataFacade as injectable wrapper over SchemaRegistry
  • Adds ObjectRepository and ScopedContext for ergonomic context-bound data access
  • Implements SecurityPlugin with RBAC evaluation, RLS filter compilation, and field-level security
  • Adds built-in audit hooks for auto-stamping created_by/modified_by and tenant isolation middleware

Reviewed changes

Copilot reviewed 19 out of 20 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
packages/spec/src/kernel/execution-context.zod.ts Defines ExecutionContext schema with user/tenant/permissions/transaction fields
packages/spec/src/data/data-engine.zod.ts Adds BaseEngineOptions with optional context to all 6 CRUD operation schemas
packages/spec/package.json Exports new ./security subpath for permission/RLS schemas
packages/objectql/src/engine.ts Refactors hooks to Map-based with priority, adds middleware chain, propagates context through all operations, adds ObjectRepository/ScopedContext
packages/objectql/src/metadata-facade.ts Provides injectable interface over SchemaRegistry for metadata service
packages/objectql/src/plugin.ts Registers MetadataFacade, adds audit hooks and tenant isolation middleware
packages/plugins/plugin-auth/src/auth-plugin.ts Registers placeholder auth middleware on ObjectQL engine
packages/plugins/plugin-security/* New plugin with PermissionEvaluator, RLSCompiler, FieldMasker, and SecurityPlugin
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment on lines +139 to +152
this.ql.registerHook('beforeInsert', async (hookCtx) => {
if (hookCtx.session?.userId && hookCtx.input?.data) {
const data = hookCtx.input.data as Record<string, any>;
if (typeof data === 'object' && data !== null) {
data.created_by = data.created_by ?? hookCtx.session.userId;
data.modified_by = hookCtx.session.userId;
data.created_at = data.created_at ?? new Date().toISOString();
data.modified_at = new Date().toISOString();
if (hookCtx.session.tenantId) {
data.space_id = data.space_id ?? hookCtx.session.tenantId;
}
}
}
}, { object: '*', priority: 10 });

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The audit hook for beforeInsert does not handle array data correctly. When hookCtx.input.data is an array (bulk insert), the code treats it as a single object and tries to stamp audit fields on the array itself rather than on each element.

This will fail to add audit fields (created_by, modified_by, etc.) to bulk insert operations. The hook should check if data is an array and iterate over each element to apply the audit stamps.

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +101
compileExpression(
expression: string,
userCtx: RLSUserContext
): Record<string, unknown> | null {
if (!expression) return null;

// Handle simple equality: "field = current_user.property"
const eqMatch = expression.match(/^\s*(\w+)\s*=\s*current_user\.(\w+)\s*$/);
if (eqMatch) {
const [, field, prop] = eqMatch;
const value = userCtx[prop];
if (value === undefined) return null;
return { [field]: value };
}

// Handle literal equality: "field = 'value'"
const litMatch = expression.match(/^\s*(\w+)\s*=\s*'([^']*)'\s*$/);
if (litMatch) {
const [, field, value] = litMatch;
return { [field]: value };
}

// Handle IN: "field IN (current_user.array_property)"
const inMatch = expression.match(/^\s*(\w+)\s+IN\s+\(\s*current_user\.(\w+)\s*\)\s*$/i);
if (inMatch) {
const [, field, prop] = inMatch;
const value = userCtx[prop];
if (!Array.isArray(value)) return null;
return { [field]: { $in: value } };
}

// Unsupported expression: return null (no additional RLS filter applied).
// Note: callers should treat absence of RLS policies as "allow all" only when
// no policies are defined. If policies exist but cannot be compiled, the caller
// may want to deny access as a safety measure.
return null;
}

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The RLS compiler's expression parsing is overly simplistic and could lead to security vulnerabilities or incorrect access control.

Issues:

  1. Only supports basic patterns (field = current_user.property, field IN (...), field = 'literal')
  2. No validation that the expression is safe (could contain malicious SQL if extended)
  3. Returns null for unsupported expressions without logging or error - this could silently bypass security policies

Consider:

  • Adding validation to reject potentially unsafe expressions
  • Logging warnings when policies cannot be compiled
  • Documenting the subset of RLS expressions currently supported
  • Adding comprehensive test coverage for expression parsing edge cases

Copilot uses AI. Check for mistakes.
Comment on lines +204 to +229
private registerTenantMiddleware(ctx: PluginContext) {
if (!this.ql) return;

this.ql.registerMiddleware(async (opCtx, next) => {
// Only apply to operations with tenantId that are not system-level
if (!opCtx.context?.tenantId || opCtx.context?.isSystem) {
return next();
}

// Read operations: inject space_id filter into AST
if (['find', 'findOne', 'count', 'aggregate'].includes(opCtx.operation)) {
if (opCtx.ast) {
const tenantFilter = { space_id: opCtx.context.tenantId };
if (opCtx.ast.where) {
opCtx.ast.where = { $and: [opCtx.ast.where, tenantFilter] };
} else {
opCtx.ast.where = tenantFilter;
}
}
}

await next();
});

ctx.logger.debug('Tenant isolation middleware registered');
}

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tenant isolation middleware only applies filters to read operations (find, findOne, count, aggregate) but does NOT enforce tenant isolation on write operations (insert, update, delete).

This is a critical multi-tenant security gap:

  1. Users can insert records with arbitrary space_id values, potentially creating records in other tenants
  2. Users can update/delete records in other tenants by manipulating the filter to bypass space_id checks
  3. The audit hook stamps space_id on insert (line 148), but this occurs AFTER middleware, so malicious users could override it

Consider:

  • Adding tenant validation to update/delete operations to prevent cross-tenant modifications
  • Validating that inserted records have the correct space_id AFTER the audit hook runs
  • Moving tenant enforcement to middleware that runs BEFORE hooks to prevent bypass

Copilot uses AI. Check for mistakes.
Comment on lines +934 to +980
export class ObjectRepository {
constructor(
private objectName: string,
private context: ExecutionContext,
private engine: IDataEngine
) {}

async find(query: any = {}): Promise<any[]> {
return this.engine.find(this.objectName, {
...query,
context: this.context,
});
}

async findOne(query: any = {}): Promise<any> {
return this.engine.findOne(this.objectName, {
...query,
context: this.context,
});
}

async insert(data: any): Promise<any> {
return this.engine.insert(this.objectName, data, {
context: this.context,
});
}

async update(data: any, options: any = {}): Promise<any> {
return this.engine.update(this.objectName, data, {
...options,
context: this.context,
});
}

async delete(options: any = {}): Promise<any> {
return this.engine.delete(this.objectName, {
...options,
context: this.context,
});
}

async count(query: any = {}): Promise<number> {
return this.engine.count(this.objectName, {
...query,
context: this.context,
});
}

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ObjectRepository methods don't preserve all query options when merging with context. For example, in the find method (line 941-946), the spread operator ...query comes BEFORE context: this.context, which means if a caller passes a context in the query parameter, it will be overwritten by the repository's context.

This could lead to confusion where a developer explicitly passes context but it gets silently ignored. Consider either:

  1. Throwing an error if query contains a context (to prevent silent overwrites)
  2. Merging contexts explicitly with clear precedence rules
  3. Documenting that the repository's context always takes precedence

Copilot uses AI. Check for mistakes.
Comment on lines +914 to 1008

/**
* Create a scoped execution context bound to this engine.
*
* Usage:
* const ctx = engine.createContext({ userId: '...', tenantId: '...' });
* const users = ctx.object('user');
* await users.find({ filter: { status: 'active' } });
*/
createContext(ctx: Partial<ExecutionContext>): ScopedContext {
return new ScopedContext(
ExecutionContextSchema.parse(ctx),
this
);
}
}

/**
* Repository scoped to a single object, bound to an execution context.
*/
export class ObjectRepository {
constructor(
private objectName: string,
private context: ExecutionContext,
private engine: IDataEngine
) {}

async find(query: any = {}): Promise<any[]> {
return this.engine.find(this.objectName, {
...query,
context: this.context,
});
}

async findOne(query: any = {}): Promise<any> {
return this.engine.findOne(this.objectName, {
...query,
context: this.context,
});
}

async insert(data: any): Promise<any> {
return this.engine.insert(this.objectName, data, {
context: this.context,
});
}

async update(data: any, options: any = {}): Promise<any> {
return this.engine.update(this.objectName, data, {
...options,
context: this.context,
});
}

async delete(options: any = {}): Promise<any> {
return this.engine.delete(this.objectName, {
...options,
context: this.context,
});
}

async count(query: any = {}): Promise<number> {
return this.engine.count(this.objectName, {
...query,
context: this.context,
});
}
}

/**
* Scoped execution context with object() accessor.
*/
export class ScopedContext {
constructor(
private executionContext: ExecutionContext,
private engine: IDataEngine
) {}

/** Get a repository scoped to this context */
object(name: string): ObjectRepository {
return new ObjectRepository(name, this.executionContext, this.engine);
}

/** Create an elevated (system) context */
sudo(): ScopedContext {
return new ScopedContext(
{ ...this.executionContext, isSystem: true },
this.engine
);
}

get userId() { return this.executionContext.userId; }
get tenantId() { return this.executionContext.tenantId; }
get roles() { return this.executionContext.roles; }
}

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new ExecutionContext propagation, middleware chain, per-object hooks, and ScopedContext/ObjectRepository features lack test coverage in the ObjectQL engine tests.

While ExecutionContextSchema has unit tests in the spec package, there are no integration tests verifying:

  1. Context propagation through find/insert/update/delete operations
  2. Middleware chain execution order and context passing
  3. Per-object hook filtering and priority ordering
  4. ScopedContext and ObjectRepository API behavior
  5. Interaction between middleware and hooks with contexts

These are fundamental architectural changes that should have comprehensive test coverage before being merged. Consider adding integration tests to verify the execution context flows correctly through the entire stack.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +153
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.

import { Plugin, PluginContext } from '@objectstack/core';
import type { PermissionSet, RowLevelSecurityPolicy } from '@objectstack/spec/security';
import { PermissionEvaluator } from './permission-evaluator.js';
import { RLSCompiler } from './rls-compiler.js';
import { FieldMasker } from './field-masker.js';

/**
* SecurityPlugin
*
* Provides RBAC, Row-Level Security, and Field-Level Security runtime.
* Registers as an engine middleware on the ObjectQL engine.
*
* This plugin is fully optional — without it, the system operates
* without permission checks (same as current behavior).
*
* Dependencies:
* - objectql service (ObjectQL engine with middleware support)
* - metadata service (MetadataFacade for reading permission sets and RLS policies)
*/
export class SecurityPlugin implements Plugin {
name = 'com.objectstack.security';
type = 'standard';
version = '1.0.0';
dependencies = ['com.objectstack.engine.objectql'];

private permissionEvaluator = new PermissionEvaluator();
private rlsCompiler = new RLSCompiler();
private fieldMasker = new FieldMasker();

async init(ctx: PluginContext): Promise<void> {
ctx.logger.info('Initializing Security Plugin...');

// Register security services
ctx.registerService('security.permissions', this.permissionEvaluator);
ctx.registerService('security.rls', this.rlsCompiler);
ctx.registerService('security.fieldMasker', this.fieldMasker);

ctx.logger.info('Security Plugin initialized');
}

async start(ctx: PluginContext): Promise<void> {
ctx.logger.info('Starting Security Plugin...');

// Get required services
let ql: any;
let metadata: any;

try {
ql = ctx.getService('objectql');
metadata = ctx.getService('metadata');
} catch (e) {
ctx.logger.warn('ObjectQL or metadata service not available, security middleware not registered');
return;
}

if (!ql || typeof ql.registerMiddleware !== 'function') {
ctx.logger.warn('ObjectQL engine does not support middleware, security middleware not registered');
return;
}

// Register security middleware
ql.registerMiddleware(async (opCtx: any, next: () => Promise<void>) => {
// System operations bypass security
if (opCtx.context?.isSystem) {
return next();
}

const roles = opCtx.context?.roles ?? [];

// Skip security checks if no roles (anonymous/unauthenticated)
// The auth middleware should handle authentication separately
if (roles.length === 0 && !opCtx.context?.userId) {
return next();
}

// 1. Resolve permission sets for the user's roles
let permissionSets: PermissionSet[] = [];
try {
permissionSets = this.permissionEvaluator.resolvePermissionSets(roles, metadata);
} catch (e) {
// If metadata service is misconfigured, log and continue without permission checks
// rather than blocking all operations
return next();
}

// 2. CRUD permission check
if (permissionSets.length > 0) {
const allowed = this.permissionEvaluator.checkObjectPermission(
opCtx.operation,
opCtx.object,
permissionSets
);

if (!allowed) {
throw new Error(
`[Security] Access denied: operation '${opCtx.operation}' on object '${opCtx.object}' ` +
`is not permitted for roles [${roles.join(', ')}]`
);
}
}

// 3. RLS filter injection
const allRlsPolicies = this.collectRLSPolicies(permissionSets, opCtx.object, opCtx.operation);
if (allRlsPolicies.length > 0 && opCtx.ast) {
const rlsFilter = this.rlsCompiler.compileFilter(allRlsPolicies, opCtx.context);
if (rlsFilter) {
if (opCtx.ast.where) {
opCtx.ast.where = { $and: [opCtx.ast.where, rlsFilter] };
} else {
opCtx.ast.where = rlsFilter;
}
}
}

await next();

// 4. Field-level security: mask restricted fields in read results
if (opCtx.result && ['find', 'findOne'].includes(opCtx.operation)) {
const fieldPerms = this.permissionEvaluator.getFieldPermissions(opCtx.object, permissionSets);
if (Object.keys(fieldPerms).length > 0) {
opCtx.result = this.fieldMasker.maskResults(opCtx.result, fieldPerms, opCtx.object);
}
}
});

ctx.logger.info('Security middleware registered on ObjectQL engine');
}

async destroy(): Promise<void> {
// No cleanup needed
}

/**
* Collect all RLS policies from permission sets applicable to the given object/operation.
*/
private collectRLSPolicies(
permissionSets: PermissionSet[],
objectName: string,
operation: string
): RowLevelSecurityPolicy[] {
const allPolicies: RowLevelSecurityPolicy[] = [];

for (const ps of permissionSets) {
if (ps.rowLevelSecurity) {
allPolicies.push(...ps.rowLevelSecurity);
}
}

return this.rlsCompiler.getApplicablePolicies(objectName, operation, allPolicies);
}
}

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SecurityPlugin lacks test coverage. A new plugin implementing critical security features (RBAC, RLS, field masking) should have comprehensive unit tests to ensure:

  1. Permission evaluation works correctly with different permission sets
  2. RLS filter compilation produces correct query filters
  3. Field masking properly removes non-readable fields
  4. Edge cases like empty permission sets, missing metadata, and malformed policies are handled

Consider adding tests to verify the security guarantees before this code reaches production.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +84
// If metadata service is misconfigured, log and continue without permission checks
// rather than blocking all operations

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling in the security middleware silently swallows exceptions when resolving permission sets. On line 82-85, if an error occurs during resolvePermissionSets, the code logs nothing and continues without security checks.

This is a critical security issue: if the metadata service throws an error, all subsequent operations bypass permission checks entirely. An attacker could potentially trigger metadata errors to gain unauthorized access.

Consider:

  • Logging a warning when permission resolution fails
  • Making this behavior configurable (fail-open vs fail-closed)
  • In production environments, default to fail-closed (deny access on errors) rather than fail-open
Suggested change
// If metadata service is misconfigured, log and continue without permission checks
// rather than blocking all operations
// If metadata service is misconfigured, decide whether to fail-open or fail-closed.
// In production (NODE_ENV === 'production'), we default to fail-closed (deny access).
// In non-production environments, we log and continue without permission checks.
const isProduction = process.env.NODE_ENV === 'production';
ctx.logger?.warn?.(
`[Security] Failed to resolve permission sets for roles [${roles.join(', ')}] ` +
`on operation '${opCtx.operation}' for object '${opCtx.object}'. ` +
(isProduction
? 'Denying access (fail-closed).'
: 'Continuing without permission checks (fail-open).'),
{ error: e }
);
if (isProduction) {
throw new Error('[Security] Unable to resolve permission sets; access denied.');
}

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +76
// Skip security checks if no roles (anonymous/unauthenticated)
// The auth middleware should handle authentication separately
if (roles.length === 0 && !opCtx.context?.userId) {
return next();
}

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The security middleware skips permission checks when roles.length === 0 && !opCtx.context?.userId. This creates a potential security bypass: unauthenticated users with no roles can access data without permission checks.

While the comment mentions "the auth middleware should handle authentication separately", relying on another middleware for security is risky:

  1. If auth middleware is not installed or misconfigured, this allows unauthorized access
  2. Middleware execution order matters - if security runs before auth, this bypass activates
  3. Anonymous access should be an explicit, documented feature, not a fallback behavior

Consider:

  • Making anonymous access behavior configurable (with secure defaults)
  • Logging when operations proceed without authentication
  • Documenting the dependency on auth middleware and middleware ordering requirements

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +111
resolvePermissionSets(
roles: string[],
metadataService: any
): PermissionSet[] {
const result: PermissionSet[] = [];

// Get all permission sets from metadata
const allPermSets = metadataService.list?.('permissions') || [];

for (const ps of allPermSets) {
// A permission set is relevant if it's a profile assigned to any of the user's roles,
// or if the role name matches the permission set name
if (roles.includes(ps.name)) {
result.push(ps);
}
}

return result;
}

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The permission resolution logic is fragile and could fail to load permission sets correctly. On line 100, the code calls metadataService.list?.('permissions') which:

  1. Uses optional chaining (?.) suggesting the method might not exist
  2. Passes 'permissions' as the type, but there's no guarantee this type exists in the metadata registry
  3. Returns an empty array on failure without logging, making debugging difficult
  4. Assumes permission sets have a name field that matches role names exactly

This makes the permission system brittle:

  • If permission sets are registered under a different metadata type, they won't be found
  • No validation that permission sets conform to the expected schema
  • Silent failures make it hard to diagnose permission issues in production

Consider adding validation, error logging, and documentation about the expected metadata structure.

Copilot uses AI. Check for mistakes.
Comment on lines +635 to +660
await this.executeWithMiddleware(opCtx, async () => {
const hookContext: HookContext = {
object,
event: 'beforeFind',
input: { ast: opCtx.ast, options: opCtx.options },
session: this.buildSession(opCtx.context),
transaction: opCtx.context?.transaction,
ql: this
};
await this.triggerHooks('beforeFind', hookContext);

try {
const result = await driver.find(object, hookContext.input.ast as QueryAST, hookContext.input.options as any);

hookContext.event = 'afterFind';
hookContext.result = result;
await this.triggerHooks('afterFind', hookContext);

return hookContext.result;
} catch (e) {
this.logger.error('Find operation failed', e as Error, { object });
throw e;
}
});

return opCtx.result as any[];

Copilot AI Feb 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The middleware execution happens OUTSIDE the hook execution in the find/insert/update/delete/count/aggregate operations. Looking at the structure:

executeWithMiddleware(opCtx, async () => {
  // Hooks run here
  triggerHooks('beforeX', hookContext)
  // Driver call
  result = await driver.operation(...)
  // After hooks
  triggerHooks('afterX', hookContext)
})

This means middlewares wrap hooks. However, the SecurityPlugin modifies opCtx.ast (the query AST) in middleware, while hooks receive hookContext.input.ast. If a hook modifies the AST, those changes might not be seen by security middleware that already ran, or vice versa.

Additionally, the audit hooks (lines 139-195 in plugin.ts) stamp fields on hookCtx.input.data which happens INSIDE the middleware executor, so tenant validation middleware cannot validate the stamped values since it runs OUTSIDE.

This creates potential race conditions and bypass opportunities. Consider documenting the middleware/hook execution order and ensuring security-critical validations happen at the right layer.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file protocol:data size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants