feat(phase-2): workstream 5 — OPA Policy Engine
- policies/authz.rego: Rego policy with path normalisation and scope enforcement - policies/data/scopes.json: all 13 endpoint → scope mappings - src/middleware/opa.ts: OpaMiddleware with Wasm primary path + scopes.json fallback; exports createOpaMiddleware() and reloadOpaPolicy() for SIGHUP hot-reload - All four route files: opaMiddleware wired after authMiddleware - AuditController, OAuth2Service: manual scope checks removed (now centralised in OPA) - src/server.ts: SIGHUP handler calls reloadOpaPolicy() - docs/devops/environment-variables.md: POLICY_DIR documented - 38 new tests; 302/302 passing; opa.ts coverage 98.66% statements Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,6 @@ import { ITokenPayload, IAuditEvent } from '../../../src/types/index';
|
||||
import {
|
||||
ValidationError,
|
||||
AuthenticationError,
|
||||
InsufficientScopeError,
|
||||
AuditEventNotFoundError,
|
||||
} from '../../../src/utils/errors';
|
||||
|
||||
@@ -103,13 +102,18 @@ describe('AuditController', () => {
|
||||
expect(next).toHaveBeenCalledWith(expect.any(AuthenticationError));
|
||||
});
|
||||
|
||||
it('should call next(InsufficientScopeError) when scope does not include audit:read', async () => {
|
||||
it('should call auditService.queryEvents regardless of scope (scope enforced by OPA middleware)', async () => {
|
||||
// Scope enforcement has been moved to OpaMiddleware; the controller delegates
|
||||
// to the service for all authenticated requests that reach it.
|
||||
const { req, res, next } = buildMocks('agents:read');
|
||||
req.query = {};
|
||||
const emptyResponse = { data: [], total: 0, page: 1, limit: 50 };
|
||||
auditService.queryEvents.mockResolvedValue(emptyResponse);
|
||||
|
||||
await controller.queryAuditLog(req as Request, res as Response, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(expect.any(InsufficientScopeError));
|
||||
expect(auditService.queryEvents).not.toHaveBeenCalled();
|
||||
expect(auditService.queryEvents).toHaveBeenCalledTimes(1);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
});
|
||||
|
||||
it('should call next(ValidationError) when query params are invalid', async () => {
|
||||
@@ -190,14 +194,17 @@ describe('AuditController', () => {
|
||||
expect(next).toHaveBeenCalledWith(expect.any(AuthenticationError));
|
||||
});
|
||||
|
||||
it('should call next(InsufficientScopeError) when scope does not include audit:read', async () => {
|
||||
it('should call auditService.getEventById regardless of scope (scope enforced by OPA middleware)', async () => {
|
||||
// Scope enforcement has been moved to OpaMiddleware; the controller delegates
|
||||
// to the service for all authenticated requests that reach it.
|
||||
const { req, res, next } = buildMocks('agents:read');
|
||||
req.params = { eventId: MOCK_AUDIT_EVENT.eventId };
|
||||
auditService.getEventById.mockResolvedValue(MOCK_AUDIT_EVENT);
|
||||
|
||||
await controller.getAuditEventById(req as Request, res as Response, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(expect.any(InsufficientScopeError));
|
||||
expect(auditService.getEventById).not.toHaveBeenCalled();
|
||||
expect(auditService.getEventById).toHaveBeenCalledTimes(1);
|
||||
expect(res.status).toHaveBeenCalledWith(200);
|
||||
});
|
||||
|
||||
it('should forward AuditEventNotFoundError to next', async () => {
|
||||
|
||||
Reference in New Issue
Block a user