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 { Module } from '@nestjs/common';
|
||||||
import { PrismaModule } from './prisma/prisma.module';
|
import { PrismaModule } from './prisma/prisma.module';
|
||||||
|
import { PlatformModule } from './platform/platform.module';
|
||||||
import { HealthController } from './health.controller';
|
import { HealthController } from './health.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule, PlatformModule],
|
||||||
controllers: [HealthController],
|
controllers: [HealthController],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
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