feat(service): PlatformGates seam + fail-closed decideOrThrow (P1.3)
LocalDevPorts = permissive in-service default (no testkit at runtime); decideOrThrow turns OPA deny/throw/timeout into PolicyDeniedError. Tests drive deny/timeout via testkit fakes. 12 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { PlatformModule } from './platform/platform.module';
|
||||
import { HealthController } from './health.controller';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
imports: [PrismaModule, PlatformModule],
|
||||
controllers: [HealthController],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { makeFakePorts } from '@insignia/iios-testkit';
|
||||
import { PolicyDeniedError } from '@insignia/iios-contracts';
|
||||
import { decideOrThrow } from './fail-closed';
|
||||
|
||||
describe('decideOrThrow (fail-closed gate)', () => {
|
||||
it('returns the decision when OPA allows', async () => {
|
||||
const ports = makeFakePorts();
|
||||
const decision = await decideOrThrow(ports, {});
|
||||
expect(decision.allow).toBe(true);
|
||||
});
|
||||
|
||||
it('throws PolicyDeniedError when OPA denies', async () => {
|
||||
const ports = makeFakePorts();
|
||||
ports.opa.deny('blocked');
|
||||
await expect(decideOrThrow(ports, {})).rejects.toBeInstanceOf(PolicyDeniedError);
|
||||
});
|
||||
|
||||
it('throws PolicyDeniedError when OPA times out (never implicit-allow)', async () => {
|
||||
const ports = makeFakePorts();
|
||||
ports.opa.timeout();
|
||||
await expect(decideOrThrow(ports, {})).rejects.toBeInstanceOf(PolicyDeniedError);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { IiosPlatformPorts, PolicyDecision, PolicyDeniedError } from '@insignia/iios-contracts';
|
||||
|
||||
/**
|
||||
* Fail-closed authorization (Bottom-Up hard gate; Critics KG-03).
|
||||
*
|
||||
* Calls the OPA port. The kernel proceeds ONLY on an explicit allow. A denial,
|
||||
* a thrown error, or a timeout all become a PolicyDeniedError — never an
|
||||
* implicit allow. Callers must let this propagate so nothing is written.
|
||||
*/
|
||||
export async function decideOrThrow(ports: IiosPlatformPorts, input: unknown): Promise<PolicyDecision> {
|
||||
let decision: PolicyDecision;
|
||||
try {
|
||||
decision = await ports.opa.decide(input);
|
||||
} catch (err) {
|
||||
throw new PolicyDeniedError(`policy port unavailable: ${(err as Error).message}`);
|
||||
}
|
||||
if (!decision.allow) {
|
||||
throw new PolicyDeniedError(decision.obligations[0]?.reason ?? 'policy denied');
|
||||
}
|
||||
return decision;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import type {
|
||||
IiosPlatformPorts,
|
||||
PlatformPrincipal,
|
||||
PolicyDecision,
|
||||
ContextDecisionBundle,
|
||||
SourceHandleResolution,
|
||||
} from '@insignia/iios-contracts';
|
||||
|
||||
/** DI token for the platform ports (Session/OPA/CMP/MDM/CRRE/SAS/Capability). */
|
||||
export const PLATFORM_PORTS = Symbol('PLATFORM_PORTS');
|
||||
|
||||
/**
|
||||
* P1 default binding: a permissive, in-service implementation so `nest start`
|
||||
* runs without the real platform fabric (which other teams own) and without a
|
||||
* runtime dependency on the testkit. Tests inject @insignia/iios-testkit fakes
|
||||
* to exercise deny / timeout / ambiguous paths. Real adapters replace this from
|
||||
* P5 onward.
|
||||
*/
|
||||
export class LocalDevPorts implements IiosPlatformPorts {
|
||||
session = {
|
||||
async verify(_token: string): Promise<PlatformPrincipal> {
|
||||
return { principalRef: 'local-dev', orgId: 'org_demo', appId: 'portal-demo', assurance: 'service' };
|
||||
},
|
||||
};
|
||||
opa = {
|
||||
async decide(_input: unknown): Promise<PolicyDecision> {
|
||||
return { decisionRef: 'local-allow', allow: true, obligations: [], ttlSeconds: 60 };
|
||||
},
|
||||
};
|
||||
cmp = {
|
||||
async checkPurpose(_input: unknown): Promise<{ receiptRef?: string; status: 'ALLOW' | 'DENY' | 'NOT_REQUIRED' }> {
|
||||
return { status: 'NOT_REQUIRED' };
|
||||
},
|
||||
};
|
||||
mdm = {
|
||||
async resolveSourceHandle(_input: unknown): Promise<SourceHandleResolution> {
|
||||
return { confidence: 0, status: 'UNRESOLVED' };
|
||||
},
|
||||
};
|
||||
crre = {
|
||||
async resolve(_input: unknown): Promise<ContextDecisionBundle> {
|
||||
return { bundleRef: 'local', scopeHash: 'local', resourceSet: [], constraints: [] };
|
||||
},
|
||||
};
|
||||
sas = {
|
||||
async tokenizeParts(parts: unknown[]): Promise<unknown[]> {
|
||||
return parts;
|
||||
},
|
||||
};
|
||||
capability = {
|
||||
async invoke(_input: unknown): Promise<{ providerRef: string; outcome: string }> {
|
||||
return { providerRef: 'local', outcome: 'SENT' };
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { PLATFORM_PORTS, LocalDevPorts } from './platform-ports';
|
||||
|
||||
/** Binds the platform ports (P1: the permissive in-service default). */
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [{ provide: PLATFORM_PORTS, useClass: LocalDevPorts }],
|
||||
exports: [PLATFORM_PORTS],
|
||||
})
|
||||
export class PlatformModule {}
|
||||
Reference in New Issue
Block a user