feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,831 @@
import {
REVOKED_API_CREDENTIAL_DIGEST,
type ApiCredentialAdministrationOperation,
} from '@qinglong/runtime-core/api-credential-administration';
import {
assertApiCredentialId,
assertApiCredentialPepperKeyId,
} from '@qinglong/runtime-core/api-credential';
import {
IDENTITY_ADMINISTRATION_OPERATIONS,
type IdentityAdministrationOperation,
} from '@qinglong/runtime-core/identity-administration';
import {
type AppendAuthorizedLocalApiCredentialResult,
type AppendAuthorizedLocalCredentialDeliveryAcknowledgementResult,
type AppendAuthorizedLocalIdentityResult,
type InspectAuthorizedLocalApiCredentialResult,
type InspectAuthorizedLocalIdentityResult,
type LocalIdentityCredentialAdministrationRepository,
} from '@qinglong/runtime-core/local-identity-credential-administration';
import {
ProjectPolicyEngine,
assertProjectPolicyProjectId,
normalizeProjectPolicySubject,
type ProjectPolicyRepository,
} from '@qinglong/runtime-core/project-policy';
import {
normalizeSecurityPrincipal,
type SecurityPolicyFence,
type SecurityPrincipal,
type SecuritySubject,
} from '@qinglong/runtime-core/security';
import {
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
} from '@qinglong/runtime-core/security-audit';
const UUID_V4_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const MAX_VERSION = 2_147_483_647;
const MAX_CREDENTIAL_LIFETIME_MS = 2 * 365 * 24 * 60 * 60 * 1000;
const STRONG_USER_ASSURANCES = new Set([
'multi_factor',
'hardware',
'local_console',
]);
interface BaseAdministrationRequest {
readonly projectId: string;
readonly mutationId: string;
readonly requestId: string;
readonly principal: SecurityPrincipal;
}
interface BaseInspectionRequest {
readonly projectId: string;
readonly auditEventId: string;
readonly requestId: string;
readonly principal: SecurityPrincipal;
}
export interface LocalIdentityAdministrationRequest
extends BaseAdministrationRequest {
readonly operation: IdentityAdministrationOperation;
readonly target: SecuritySubject;
readonly expectedCurrentVersion: number;
}
export interface LocalApiCredentialAdministrationRequest
extends BaseAdministrationRequest {
readonly operation: ApiCredentialAdministrationOperation;
readonly credentialId: string;
readonly target: SecuritySubject;
readonly expectedCurrentVersion: number;
readonly pepperKeyId: string;
readonly secretDigest?: string;
readonly deliveryDigest?: string;
readonly notBeforeAtMs?: number;
readonly expiresAtMs?: number;
}
export interface LocalCredentialDeliveryAcknowledgementRequest
extends BaseAdministrationRequest {
readonly credentialMutationId: string;
readonly expectedDeliveryDigest: string;
}
export interface LocalIdentityInspectionRequest extends BaseInspectionRequest {
readonly target: SecuritySubject;
}
export interface LocalApiCredentialInspectionRequest
extends BaseInspectionRequest {
readonly credentialId: string;
}
export interface LocalIdentityCredentialAdministrationService {
inspectIdentity(
request: LocalIdentityInspectionRequest,
): Promise<InspectAuthorizedLocalIdentityResult>;
inspectCredential(
request: LocalApiCredentialInspectionRequest,
): Promise<InspectAuthorizedLocalApiCredentialResult>;
changeIdentity(
request: LocalIdentityAdministrationRequest,
): Promise<AppendAuthorizedLocalIdentityResult>;
changeCredential(
request: LocalApiCredentialAdministrationRequest,
): Promise<AppendAuthorizedLocalApiCredentialResult>;
acknowledgeCredentialDelivery(
request: LocalCredentialDeliveryAcknowledgementRequest,
): Promise<AppendAuthorizedLocalCredentialDeliveryAcknowledgementResult>;
}
export class LocalIdentityCredentialAdministrationConfigurationError extends TypeError {
readonly code = 'LOCAL_IDENTITY_CREDENTIAL_ADMINISTRATION_INVALID';
constructor(message: string) {
super(`Local Identity credential administration is invalid: ${message}`);
this.name = 'LocalIdentityCredentialAdministrationConfigurationError';
}
}
export class LocalIdentityCredentialAdministrationAuthenticationError extends Error {
readonly code =
'LOCAL_IDENTITY_CREDENTIAL_ADMINISTRATION_AUTHENTICATION_REQUIRED';
constructor() {
super('Local Identity credential administration requires a strong User');
this.name = 'LocalIdentityCredentialAdministrationAuthenticationError';
}
}
export class LocalIdentityCredentialAdministrationAuthorizationError extends Error {
readonly code = 'LOCAL_IDENTITY_CREDENTIAL_ADMINISTRATION_FORBIDDEN';
constructor() {
super('Local Identity credential administration is not authorized');
this.name = 'LocalIdentityCredentialAdministrationAuthorizationError';
}
}
export class LocalIdentityCredentialAdministrationServiceUnavailableError extends Error {
readonly code =
'LOCAL_IDENTITY_CREDENTIAL_ADMINISTRATION_SERVICE_UNAVAILABLE';
constructor() {
super('Local Identity credential administration service is unavailable');
this.name = 'LocalIdentityCredentialAdministrationServiceUnavailableError';
}
}
function exactObject(
value: unknown,
expectedKeys: readonly string[],
label: string,
): void {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new LocalIdentityCredentialAdministrationConfigurationError(
`${label} must be an object`,
);
}
const actual = Object.keys(value).sort();
const expected = [...expectedKeys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
throw new LocalIdentityCredentialAdministrationConfigurationError(
`${label} shape is invalid`,
);
}
}
function safeNow(now: () => number): number {
const value = now();
if (!Number.isSafeInteger(value) || value < 0) {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'clock is invalid',
);
}
return value;
}
function common(
request: BaseAdministrationRequest,
nowMs: number,
): Readonly<BaseAdministrationRequest> {
try {
assertProjectPolicyProjectId(request.projectId);
} catch {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'projectId is invalid',
);
}
if (
typeof request.mutationId !== 'string' ||
!UUID_V4_PATTERN.test(request.mutationId) ||
typeof request.requestId !== 'string' ||
!REQUEST_ID_PATTERN.test(request.requestId)
) {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'mutationId or requestId is invalid',
);
}
let principal: Readonly<SecurityPrincipal>;
try {
principal = normalizeSecurityPrincipal(request.principal, nowMs);
} catch {
throw new LocalIdentityCredentialAdministrationAuthenticationError();
}
if (
principal.subject.type !== 'user' ||
!STRONG_USER_ASSURANCES.has(principal.assurance)
) {
throw new LocalIdentityCredentialAdministrationAuthenticationError();
}
return Object.freeze({ ...request, principal });
}
function inspectionCommon(
request: BaseInspectionRequest,
nowMs: number,
): Readonly<BaseInspectionRequest> {
try {
assertProjectPolicyProjectId(request.projectId);
} catch {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'projectId is invalid',
);
}
if (
typeof request.auditEventId !== 'string' ||
!UUID_V4_PATTERN.test(request.auditEventId) ||
typeof request.requestId !== 'string' ||
!REQUEST_ID_PATTERN.test(request.requestId)
) {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'auditEventId or requestId is invalid',
);
}
let principal: Readonly<SecurityPrincipal>;
try {
principal = normalizeSecurityPrincipal(request.principal, nowMs);
} catch {
throw new LocalIdentityCredentialAdministrationAuthenticationError();
}
if (
principal.subject.type !== 'user' ||
!STRONG_USER_ASSURANCES.has(principal.assurance)
) {
throw new LocalIdentityCredentialAdministrationAuthenticationError();
}
return Object.freeze({ ...request, principal });
}
function expectedVersion(value: number): number {
if (!Number.isSafeInteger(value) || value < 0 || value >= MAX_VERSION) {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'expectedCurrentVersion is invalid',
);
}
return value;
}
function target(value: SecuritySubject): Readonly<SecuritySubject> {
let normalized: Readonly<SecuritySubject>;
try {
normalized = normalizeProjectPolicySubject(value);
} catch {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'target is invalid',
);
}
if (
normalized.type !== 'user' &&
normalized.type !== 'api_app' &&
normalized.type !== 'mcp_client' &&
normalized.type !== 'agent'
) {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'target type is not locally administrable',
);
}
return normalized;
}
function sameSubject(
left: Readonly<SecuritySubject>,
right: Readonly<SecuritySubject>,
): boolean {
return left.type === right.type && left.id === right.id;
}
function audit(options: {
readonly eventId: string;
readonly requestId: string;
readonly operationId: string;
readonly projectId: string;
readonly principal: Readonly<SecurityPrincipal>;
readonly outcome: SecurityAuditRecord['outcome'];
readonly reasons: readonly string[];
readonly fence: SecurityPolicyFence | null;
readonly occurredAtMs: number;
}): Readonly<SecurityAuditRecord> {
return normalizeSecurityAuditRecord({
eventId: options.eventId,
requestId: options.requestId,
operationId: options.operationId,
projectId: options.projectId,
subject: options.principal.subject,
authenticationId: options.principal.authenticationId,
outcome: options.outcome,
reasons: options.reasons,
fence: options.fence,
occurredAtMs: options.occurredAtMs,
});
}
export function createLocalIdentityCredentialAdministrationService(
projectPolicy: ProjectPolicyRepository,
repository: LocalIdentityCredentialAdministrationRepository,
options: { readonly now?: () => number } = {},
): LocalIdentityCredentialAdministrationService {
if (
!projectPolicy ||
typeof projectPolicy.resolve !== 'function' ||
!repository ||
typeof repository.resolveAuthorityProjectId !== 'function' ||
typeof repository.resolveIdentity !== 'function' ||
typeof repository.resolveIdentityMutation !== 'function' ||
typeof repository.appendAuthorizedIdentity !== 'function' ||
typeof repository.inspectAuthorizedIdentity !== 'function' ||
typeof repository.resolveCredentialMutation !== 'function' ||
typeof repository.appendAuthorizedCredential !== 'function' ||
typeof repository.inspectAuthorizedCredential !== 'function' ||
typeof repository.resolveDeliveryAcknowledgement !== 'function' ||
typeof repository.appendAuthorizedDeliveryAcknowledgement !== 'function' ||
typeof repository.record !== 'function' ||
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).some((key) => key !== 'now') ||
(options.now !== undefined && typeof options.now !== 'function')
) {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'dependencies are invalid',
);
}
const now = options.now ?? Date.now;
const policy = new ProjectPolicyEngine(projectPolicy);
async function authorize(
request: Readonly<BaseAdministrationRequest | BaseInspectionRequest>,
eventId: string,
operationId: string,
occurredAtMs: number,
): Promise<Readonly<SecurityPolicyFence>> {
let decision;
try {
decision = await policy.authorize(
request.principal,
request.projectId,
'project.manage',
);
} catch {
try {
await repository.record(
audit({
eventId,
requestId: request.requestId,
operationId,
projectId: request.projectId,
principal: request.principal,
outcome: 'authorization_unavailable',
reasons: ['policy_unavailable'],
fence: null,
occurredAtMs,
}),
);
} catch {
throw new LocalIdentityCredentialAdministrationServiceUnavailableError();
}
throw new LocalIdentityCredentialAdministrationServiceUnavailableError();
}
if (decision.effect !== 'allow' || !decision.fence?.bindingVersion) {
try {
await repository.record(
audit({
eventId,
requestId: request.requestId,
operationId,
projectId: request.projectId,
principal: request.principal,
outcome:
decision.effect === 'require_approval'
? 'approval_required'
: 'denied',
reasons: decision.reasons,
fence: decision.fence,
occurredAtMs,
}),
);
} catch {
throw new LocalIdentityCredentialAdministrationServiceUnavailableError();
}
throw new LocalIdentityCredentialAdministrationAuthorizationError();
}
let authorityProjectId: string | null;
try {
authorityProjectId = await repository.resolveAuthorityProjectId();
if (authorityProjectId !== null) {
assertProjectPolicyProjectId(authorityProjectId);
}
} catch {
try {
await repository.record(
audit({
eventId,
requestId: request.requestId,
operationId,
projectId: request.projectId,
principal: request.principal,
outcome: 'authorization_unavailable',
reasons: ['instance_authority_project_unavailable'],
fence: decision.fence,
occurredAtMs,
}),
);
} catch {
throw new LocalIdentityCredentialAdministrationServiceUnavailableError();
}
throw new LocalIdentityCredentialAdministrationServiceUnavailableError();
}
if (authorityProjectId !== request.projectId) {
try {
await repository.record(
audit({
eventId,
requestId: request.requestId,
operationId,
projectId: request.projectId,
principal: request.principal,
outcome: 'denied',
reasons: ['instance_authority_project_required'],
fence: decision.fence,
occurredAtMs,
}),
);
} catch {
throw new LocalIdentityCredentialAdministrationServiceUnavailableError();
}
throw new LocalIdentityCredentialAdministrationAuthorizationError();
}
return decision.fence;
}
return Object.freeze({
async inspectIdentity(input: LocalIdentityInspectionRequest) {
exactObject(
input,
['projectId', 'target', 'auditEventId', 'requestId', 'principal'],
'identity inspection request',
);
const occurredAtMs = safeNow(now);
const request = inspectionCommon(input, occurredAtMs);
const subject = target(input.target);
const operationId = 'identity.inspect';
const fence = await authorize(
request,
request.auditEventId,
operationId,
occurredAtMs,
);
return repository.inspectAuthorizedIdentity({
target: subject,
authorization: {
projectId: request.projectId,
actor: request.principal.subject,
fence,
},
audit: audit({
eventId: request.auditEventId,
requestId: request.requestId,
operationId,
projectId: request.projectId,
principal: request.principal,
outcome: 'allowed',
reasons: ['owner_identity_inspect'],
fence,
occurredAtMs,
}),
});
},
async inspectCredential(input: LocalApiCredentialInspectionRequest) {
exactObject(
input,
['projectId', 'credentialId', 'auditEventId', 'requestId', 'principal'],
'credential inspection request',
);
const occurredAtMs = safeNow(now);
const request = inspectionCommon(input, occurredAtMs);
try {
assertApiCredentialId(input.credentialId);
} catch {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'credentialId is invalid',
);
}
const operationId = 'credential.inspect';
const fence = await authorize(
request,
request.auditEventId,
operationId,
occurredAtMs,
);
return repository.inspectAuthorizedCredential({
credentialId: input.credentialId,
authorization: {
projectId: request.projectId,
actor: request.principal.subject,
fence,
},
audit: audit({
eventId: request.auditEventId,
requestId: request.requestId,
operationId,
projectId: request.projectId,
principal: request.principal,
outcome: 'allowed',
reasons: ['owner_credential_inspect'],
fence,
occurredAtMs,
}),
});
},
async changeIdentity(input: LocalIdentityAdministrationRequest) {
exactObject(
input,
[
'projectId',
'operation',
'target',
'expectedCurrentVersion',
'mutationId',
'requestId',
'principal',
],
'identity request',
);
const occurredAtMs = safeNow(now);
const request = common(input, occurredAtMs);
if (!IDENTITY_ADMINISTRATION_OPERATIONS.includes(input.operation)) {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'identity operation is invalid',
);
}
const currentVersion = expectedVersion(input.expectedCurrentVersion);
const subject = target(input.target);
const operationId = `identity.${input.operation}`;
const fence = await authorize(
request,
request.mutationId,
operationId,
occurredAtMs,
);
const replay = await repository.resolveIdentityMutation(
request.mutationId,
);
const mutationTime = replay?.mutation.createdAtMs ?? occurredAtMs;
return repository.appendAuthorizedIdentity({
expectedCurrentVersion: currentVersion,
mutation: {
mutationId: request.mutationId,
operation: input.operation,
subject,
subjectVersion: currentVersion + 1,
expectedPreviousVersion: currentVersion,
status: input.operation === 'disable' ? 'disabled' : 'active',
changedBy: request.principal.subject,
createdAtMs: mutationTime,
},
authorization: {
projectId: request.projectId,
actor: request.principal.subject,
fence,
},
audit: audit({
eventId: request.mutationId,
requestId: request.requestId,
operationId,
projectId: request.projectId,
principal: request.principal,
outcome: 'allowed',
reasons: ['owner_identity_admin'],
fence,
occurredAtMs: mutationTime,
}),
});
},
async changeCredential(input: LocalApiCredentialAdministrationRequest) {
const active = input.operation !== 'revoke';
exactObject(
input,
[
'projectId',
'operation',
'credentialId',
'target',
'expectedCurrentVersion',
'pepperKeyId',
...(active
? ['secretDigest', 'deliveryDigest', 'notBeforeAtMs', 'expiresAtMs']
: []),
'mutationId',
'requestId',
'principal',
],
'credential request',
);
const occurredAtMs = safeNow(now);
const request = common(input, occurredAtMs);
if (
input.operation !== 'issue' &&
input.operation !== 'rotate' &&
input.operation !== 'revoke'
) {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'credential operation is invalid',
);
}
try {
assertApiCredentialId(input.credentialId);
assertApiCredentialPepperKeyId(input.pepperKeyId);
} catch {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'credentialId or pepperKeyId is invalid',
);
}
const currentVersion = expectedVersion(input.expectedCurrentVersion);
const subject = target(input.target);
let secretDigest = REVOKED_API_CREDENTIAL_DIGEST;
let deliveryDigest: string | null = null;
let notBeforeAtMs = occurredAtMs;
let expiresAtMs = occurredAtMs + 1;
if (active) {
if (
typeof input.secretDigest !== 'string' ||
!DIGEST_PATTERN.test(input.secretDigest) ||
typeof input.deliveryDigest !== 'string' ||
!DIGEST_PATTERN.test(input.deliveryDigest) ||
!Number.isSafeInteger(input.notBeforeAtMs) ||
!Number.isSafeInteger(input.expiresAtMs) ||
(input.expiresAtMs as number) <= (input.notBeforeAtMs as number)
) {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'credential material or lifetime is invalid',
);
}
secretDigest = input.secretDigest;
deliveryDigest = input.deliveryDigest;
notBeforeAtMs = input.notBeforeAtMs as number;
expiresAtMs = input.expiresAtMs as number;
}
const operationId = `credential.${input.operation}`;
const fence = await authorize(
request,
request.mutationId,
operationId,
occurredAtMs,
);
const replay = await repository.resolveCredentialMutation(
request.mutationId,
);
const mutationTime = replay?.mutation.createdAtMs ?? occurredAtMs;
if (
active &&
(notBeforeAtMs < mutationTime ||
expiresAtMs - mutationTime > MAX_CREDENTIAL_LIFETIME_MS)
) {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'credential material or lifetime is invalid',
);
}
if (
replay &&
(replay.mutation.operation !== input.operation ||
replay.mutation.credentialId !== input.credentialId ||
replay.mutation.expectedPreviousVersion !== currentVersion ||
!sameSubject(replay.credential.subject, subject) ||
replay.credential.pepperKeyId !== input.pepperKeyId ||
(active &&
(replay.credential.secretDigest !== secretDigest ||
replay.delivery?.digest !== deliveryDigest ||
replay.credential.notBeforeAtMs !== notBeforeAtMs ||
replay.credential.expiresAtMs !== expiresAtMs)))
) {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'credential replay conflicts with request',
);
}
const identity = replay
? null
: await repository.resolveIdentity(subject);
if (!replay && (!identity || (active && identity.status !== 'active'))) {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'target Identity is unavailable',
);
}
if (replay && !active) {
secretDigest = replay.credential.secretDigest;
notBeforeAtMs = replay.credential.notBeforeAtMs;
expiresAtMs = replay.credential.expiresAtMs;
}
return repository.appendAuthorizedCredential({
expectedCurrentVersion: currentVersion,
credential: {
credentialId: input.credentialId,
version: currentVersion + 1,
pepperKeyId: input.pepperKeyId,
state: active ? 'active' : 'revoked',
subject,
subjectStatus: replay?.credential.subjectStatus ?? identity!.status,
secretDigest,
createdAtMs: mutationTime,
notBeforeAtMs,
expiresAtMs,
},
mutation: {
mutationId: request.mutationId,
operation: input.operation,
credentialId: input.credentialId,
credentialVersion: currentVersion + 1,
expectedPreviousVersion: currentVersion,
changedBy: request.principal.subject,
createdAtMs: mutationTime,
},
authorization: {
projectId: request.projectId,
actor: request.principal.subject,
fence,
},
delivery:
deliveryDigest === null
? null
: Object.freeze({ digest: deliveryDigest }),
audit: audit({
eventId: request.mutationId,
requestId: request.requestId,
operationId,
projectId: request.projectId,
principal: request.principal,
outcome: 'allowed',
reasons: ['owner_credential_admin'],
fence,
occurredAtMs: mutationTime,
}),
});
},
async acknowledgeCredentialDelivery(
input: LocalCredentialDeliveryAcknowledgementRequest,
) {
exactObject(
input,
[
'projectId',
'credentialMutationId',
'expectedDeliveryDigest',
'mutationId',
'requestId',
'principal',
],
'delivery acknowledgement request',
);
const occurredAtMs = safeNow(now);
const request = common(input, occurredAtMs);
if (
!UUID_V4_PATTERN.test(input.credentialMutationId) ||
input.credentialMutationId === request.mutationId ||
!DIGEST_PATTERN.test(input.expectedDeliveryDigest)
) {
throw new LocalIdentityCredentialAdministrationConfigurationError(
'delivery acknowledgement value is invalid',
);
}
const operationId = 'credential.delivery.acknowledge';
const fence = await authorize(
request,
request.mutationId,
operationId,
occurredAtMs,
);
const existing = await repository.resolveDeliveryAcknowledgement(
input.credentialMutationId,
);
const acknowledgementTime = existing?.acknowledgedAtMs ?? occurredAtMs;
return repository.appendAuthorizedDeliveryAcknowledgement({
acknowledgement: {
credentialMutationId: input.credentialMutationId,
acknowledgementMutationId: request.mutationId,
projectId: request.projectId,
deliveryDigest: input.expectedDeliveryDigest,
acknowledgedBy: request.principal.subject,
acknowledgedAtMs: acknowledgementTime,
},
authorization: {
projectId: request.projectId,
actor: request.principal.subject,
fence,
},
audit: audit({
eventId: request.mutationId,
requestId: request.requestId,
operationId,
projectId: request.projectId,
principal: request.principal,
outcome: 'allowed',
reasons: ['owner_credential_delivery_acknowledged'],
fence,
occurredAtMs: acknowledgementTime,
}),
});
},
});
}
@@ -0,0 +1,485 @@
import {
LOCAL_SECRET_ALGORITHM,
LocalSecretMutationConflictError,
LocalSecretUnavailableError,
LocalSecretVersionConflictError,
assertLocalSecretExpectedVersion,
assertLocalSecretMutationId,
assertLocalSecretName,
assertLocalSecretPlaintext,
assertLocalSecretProjectId,
createLocalSecretRef,
type LocalSecretEnvelope,
type LocalSecretKeyProvider,
type PutEncryptedLocalSecretResult,
} from '@qinglong/runtime-core/local-secret';
import {
LocalSecretAuthorizationFenceConflictError,
type LocalSecretAdministrationMutation,
type LocalSecretAdministrationRepository,
} from '@qinglong/runtime-core/local-secret-administration';
import {
ProjectPolicyEngine,
ProjectPolicyUnavailableError,
type ProjectPolicyRepository,
} from '@qinglong/runtime-core/project-policy';
import {
normalizeSecurityPrincipal,
type SecurityPolicyDecision,
type SecurityPrincipal,
} from '@qinglong/runtime-core/security';
import {
SecurityAuditUnavailableError,
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
type SecurityAuditSink,
} from '@qinglong/runtime-core/security-audit';
import {
encryptLocalSecretEnvelope,
localSecretPlaintextMatches,
ownedLocalSecretKeyMaterial,
type LocalSecretNonceFactory,
} from '@qinglong/local-secret';
const UUID_V4_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const STRONG_USER_ASSURANCES = new Set([
'multi_factor',
'hardware',
'local_console',
]);
export interface LocalSecretAdministrationRequest {
readonly projectId: string;
readonly name: string;
readonly plaintext: string;
readonly mutationId: string;
readonly requestId: string;
readonly expectedCurrentVersion: number;
readonly principal: SecurityPrincipal;
}
export interface LocalSecretAdministrationOptions {
readonly now?: () => number;
readonly nonceFactory?: LocalSecretNonceFactory;
}
export interface LocalSecretAdministrationService {
put(
request: LocalSecretAdministrationRequest,
): Promise<PutEncryptedLocalSecretResult>;
}
export class LocalSecretAdministrationConfigurationError extends TypeError {
constructor(message: string) {
super(`Local Secret administration configuration is invalid: ${message}`);
this.name = 'LocalSecretAdministrationConfigurationError';
}
}
export class LocalSecretAdministrationAuthenticationError extends Error {
readonly code = 'LOCAL_SECRET_ADMINISTRATION_AUTHENTICATION_REQUIRED';
constructor() {
super('Local Secret administration requires a strong principal');
this.name = 'LocalSecretAdministrationAuthenticationError';
}
}
export class LocalSecretAdministrationAuthorizationError extends Error {
readonly code = 'LOCAL_SECRET_ADMINISTRATION_FORBIDDEN';
constructor() {
super('Local Secret administration is not authorized');
this.name = 'LocalSecretAdministrationAuthorizationError';
}
}
export class LocalSecretAdministrationUnavailableError extends Error {
readonly code = 'LOCAL_SECRET_ADMINISTRATION_UNAVAILABLE';
constructor() {
super('Local Secret administration is unavailable');
this.name = 'LocalSecretAdministrationUnavailableError';
}
}
function exactKeys(value: object, expected: readonly string[]): boolean {
const actual = Object.keys(value).sort();
const canonical = [...expected].sort();
return (
actual.length === canonical.length &&
actual.every((key, index) => key === canonical[index])
);
}
function assertRequest(request: LocalSecretAdministrationRequest): void {
if (
!request ||
typeof request !== 'object' ||
Array.isArray(request) ||
!exactKeys(request, [
'projectId',
'name',
'plaintext',
'mutationId',
'requestId',
'expectedCurrentVersion',
'principal',
])
) {
throw new LocalSecretAdministrationConfigurationError(
'request shape is invalid',
);
}
try {
assertLocalSecretProjectId(request.projectId);
assertLocalSecretName(request.name);
assertLocalSecretPlaintext(request.plaintext);
assertLocalSecretMutationId(request.mutationId);
assertLocalSecretExpectedVersion(request.expectedCurrentVersion);
} catch {
throw new LocalSecretAdministrationConfigurationError(
'request value is invalid',
);
}
if (
!UUID_V4_PATTERN.test(request.mutationId) ||
!REQUEST_ID_PATTERN.test(request.requestId)
) {
throw new LocalSecretAdministrationConfigurationError(
'request identity is invalid',
);
}
}
function administrationPrincipal(
value: SecurityPrincipal,
nowMs: number,
): Readonly<SecurityPrincipal> {
let principal: Readonly<SecurityPrincipal>;
try {
principal = normalizeSecurityPrincipal(value, nowMs);
} catch {
throw new LocalSecretAdministrationAuthenticationError();
}
const human =
principal.subject.type === 'user' &&
STRONG_USER_ASSURANCES.has(principal.assurance);
const system =
principal.subject.type === 'system' && principal.assurance === 'service';
if (!human && !system) {
throw new LocalSecretAdministrationAuthenticationError();
}
return principal;
}
function auditRecord(options: {
readonly request: LocalSecretAdministrationRequest;
readonly principal: Readonly<SecurityPrincipal> | null;
readonly operationId: 'secret.create' | 'secret.manage' | 'secret.rotate';
readonly outcome: SecurityAuditRecord['outcome'];
readonly reasons: readonly string[];
readonly fence: SecurityPolicyDecision['fence'];
readonly occurredAtMs: number;
}): Readonly<SecurityAuditRecord> {
return normalizeSecurityAuditRecord({
eventId: options.request.mutationId,
requestId: options.request.requestId,
operationId: options.operationId,
projectId: options.request.projectId,
subject: options.principal?.subject ?? null,
authenticationId: options.principal?.authenticationId ?? null,
outcome: options.outcome,
reasons: options.reasons,
fence: options.fence,
occurredAtMs: options.occurredAtMs,
});
}
function sameAuditSemantic(
left: Readonly<SecurityAuditRecord>,
right: Readonly<SecurityAuditRecord>,
): boolean {
const { occurredAtMs: _leftTime, ...leftSemantic } = left;
const { occurredAtMs: _rightTime, ...rightSemantic } = right;
return JSON.stringify(leftSemantic) === JSON.stringify(rightSemantic);
}
function result(
status: PutEncryptedLocalSecretResult['status'],
envelope: LocalSecretEnvelope,
): PutEncryptedLocalSecretResult {
return Object.freeze({
status,
version: envelope.version,
secretRef: createLocalSecretRef({
projectId: envelope.projectId,
name: envelope.name,
version: envelope.version,
}),
});
}
async function matchesExisting(
existing: Readonly<LocalSecretAdministrationMutation>,
expectedAudit: Readonly<SecurityAuditRecord>,
request: LocalSecretAdministrationRequest,
keys: LocalSecretKeyProvider,
): Promise<boolean> {
if (
existing.envelope.version !== request.expectedCurrentVersion + 1 ||
!sameAuditSemantic(existing.audit, expectedAudit)
) {
return false;
}
const material = ownedLocalSecretKeyMaterial(
await keys.resolve(existing.envelope.keyId),
existing.envelope.keyId,
);
try {
return localSecretPlaintextMatches(
existing.envelope,
material.key,
request.plaintext,
);
} finally {
material.key.fill(0);
}
}
export function createLocalSecretAdministrationService(
projectPolicy: ProjectPolicyRepository,
mutations: LocalSecretAdministrationRepository,
audit: SecurityAuditSink,
keys: LocalSecretKeyProvider,
options: LocalSecretAdministrationOptions = {},
): LocalSecretAdministrationService {
if (
!projectPolicy ||
typeof projectPolicy.resolve !== 'function' ||
typeof projectPolicy.append !== 'function'
) {
throw new LocalSecretAdministrationConfigurationError(
'Project Policy repository is invalid',
);
}
if (
!mutations ||
typeof mutations.resolveLocalSecretAdministrationMutation !== 'function' ||
typeof mutations.appendAuthorizedLocalSecretEnvelope !== 'function'
) {
throw new LocalSecretAdministrationConfigurationError(
'mutation repository is invalid',
);
}
if (!audit || typeof audit.record !== 'function') {
throw new LocalSecretAdministrationConfigurationError(
'audit sink is invalid',
);
}
if (
!keys ||
typeof keys.active !== 'function' ||
typeof keys.resolve !== 'function'
) {
throw new LocalSecretAdministrationConfigurationError(
'key provider is invalid',
);
}
if (
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
!exactKeys(
options,
options.nonceFactory === undefined
? options.now === undefined
? []
: ['now']
: options.now === undefined
? ['nonceFactory']
: ['now', 'nonceFactory'],
) ||
(options.now !== undefined && typeof options.now !== 'function') ||
(options.nonceFactory !== undefined &&
typeof options.nonceFactory !== 'function')
) {
throw new LocalSecretAdministrationConfigurationError(
'options are invalid',
);
}
const policy = new ProjectPolicyEngine(projectPolicy);
const now = options.now ?? Date.now;
return Object.freeze({
async put(
request: LocalSecretAdministrationRequest,
): Promise<PutEncryptedLocalSecretResult> {
assertRequest(request);
const nowMs = now();
if (!Number.isSafeInteger(nowMs) || nowMs < 0) {
throw new LocalSecretAdministrationConfigurationError(
'clock is invalid',
);
}
let principal: Readonly<SecurityPrincipal>;
try {
principal = administrationPrincipal(request.principal, nowMs);
} catch (error) {
try {
await audit.record(
auditRecord({
request,
principal: null,
operationId: 'secret.manage',
outcome: 'authentication_rejected',
reasons: ['strong_authentication_required'],
fence: null,
occurredAtMs: nowMs,
}),
);
} catch {
throw new LocalSecretAdministrationUnavailableError();
}
throw error;
}
let decision: Readonly<SecurityPolicyDecision>;
try {
decision = await policy.authorize(
principal,
request.projectId,
'secret.manage',
);
} catch (error) {
if (!(error instanceof ProjectPolicyUnavailableError)) {
throw new LocalSecretAdministrationUnavailableError();
}
try {
await audit.record(
auditRecord({
request,
principal,
operationId: 'secret.manage',
outcome: 'authorization_unavailable',
reasons: ['policy_unavailable'],
fence: null,
occurredAtMs: nowMs,
}),
);
} catch {
throw new LocalSecretAdministrationUnavailableError();
}
throw new LocalSecretAdministrationUnavailableError();
}
if (decision.effect !== 'allow') {
try {
await audit.record(
auditRecord({
request,
principal,
operationId: 'secret.manage',
outcome:
decision.effect === 'require_approval'
? 'approval_required'
: 'denied',
reasons: decision.reasons,
fence: decision.fence,
occurredAtMs: nowMs,
}),
);
} catch {
throw new LocalSecretAdministrationUnavailableError();
}
throw new LocalSecretAdministrationAuthorizationError();
}
if (!decision.fence || decision.fence.bindingVersion === null) {
throw new LocalSecretAdministrationUnavailableError();
}
const operationId =
request.expectedCurrentVersion === 0
? ('secret.create' as const)
: ('secret.rotate' as const);
const allowedAudit = auditRecord({
request,
principal,
operationId,
outcome: 'allowed',
reasons: decision.reasons,
fence: decision.fence,
occurredAtMs: nowMs,
});
try {
const existing =
await mutations.resolveLocalSecretAdministrationMutation(
request.projectId,
request.name,
request.mutationId,
);
if (existing) {
if (!(await matchesExisting(existing, allowedAudit, request, keys))) {
throw new LocalSecretMutationConflictError();
}
return result('existing', existing.envelope);
}
const material = ownedLocalSecretKeyMaterial(await keys.active());
try {
const envelope = encryptLocalSecretEnvelope(
{
projectId: request.projectId,
name: request.name,
version: request.expectedCurrentVersion + 1,
mutationId: request.mutationId,
keyId: material.keyId,
algorithm: LOCAL_SECRET_ALGORITHM,
createdAtMs: nowMs,
},
request.plaintext,
material.key,
options.nonceFactory,
);
const appended = await mutations.appendAuthorizedLocalSecretEnvelope({
expectedCurrentVersion: request.expectedCurrentVersion,
envelope,
subject: principal.subject,
fence: decision.fence,
audit: allowedAudit,
});
if (
appended.status === 'existing' &&
!(await matchesExisting(
{ envelope: appended.envelope, audit: appended.audit },
allowedAudit,
request,
keys,
))
) {
throw new LocalSecretMutationConflictError();
}
return result(appended.status, appended.envelope);
} finally {
material.key.fill(0);
}
} catch (error) {
if (
error instanceof LocalSecretVersionConflictError ||
error instanceof LocalSecretMutationConflictError ||
error instanceof LocalSecretAuthorizationFenceConflictError ||
error instanceof LocalSecretUnavailableError
) {
throw error;
}
if (error instanceof SecurityAuditUnavailableError) {
throw new LocalSecretAdministrationUnavailableError();
}
throw new LocalSecretAdministrationUnavailableError();
}
},
});
}
@@ -0,0 +1,276 @@
import {
LocalSecurityAuditQueryAuthorizationFenceConflictError,
LocalSecurityAuditQueryUnavailableError,
MAX_LOCAL_SECURITY_AUDIT_QUERY_PAGE_SIZE,
type ListAuthorizedLocalSecurityAuditResult,
type LocalSecurityAuditQueryRepository,
} from '@qinglong/runtime-core/local-security-audit-query';
import {
ProjectPolicyEngine,
assertProjectPolicyProjectId,
type ProjectPolicyRepository,
} from '@qinglong/runtime-core/project-policy';
import {
normalizeSecurityPrincipal,
type SecurityPrincipal,
} from '@qinglong/runtime-core/security';
import {
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
} from '@qinglong/runtime-core/security-audit';
import {
normalizeSecurityAuditQuery,
type SecurityAuditQuery,
} from '@qinglong/runtime-core/security-audit-query';
const UUID_V4_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const STRONG_USER_ASSURANCES = new Set([
'multi_factor',
'hardware',
'local_console',
]);
export interface ListLocalSecurityAuditRequest {
readonly authorityProjectId: string;
readonly query: SecurityAuditQuery;
readonly auditEventId: string;
readonly requestId: string;
readonly principal: SecurityPrincipal;
}
export interface LocalSecurityAuditQueryService {
list(
request: ListLocalSecurityAuditRequest,
): Promise<ListAuthorizedLocalSecurityAuditResult>;
}
export class LocalSecurityAuditQueryConfigurationError extends TypeError {
readonly code = 'LOCAL_SECURITY_AUDIT_QUERY_INVALID';
constructor(message: string) {
super(`Local security audit query is invalid: ${message}`);
this.name = 'LocalSecurityAuditQueryConfigurationError';
}
}
export class LocalSecurityAuditQueryAuthenticationError extends Error {
readonly code = 'LOCAL_SECURITY_AUDIT_QUERY_AUTHENTICATION_REQUIRED';
constructor() {
super('Local security audit query requires a strong User');
this.name = 'LocalSecurityAuditQueryAuthenticationError';
}
}
export class LocalSecurityAuditQueryAuthorizationError extends Error {
readonly code = 'LOCAL_SECURITY_AUDIT_QUERY_FORBIDDEN';
constructor() {
super('Local security audit query is not authorized');
this.name = 'LocalSecurityAuditQueryAuthorizationError';
}
}
function exactKeys(
value: object,
expected: readonly string[],
label: string,
): void {
const actual = Object.keys(value).sort();
const canonical = [...expected].sort();
if (
actual.length !== canonical.length ||
actual.some((key, index) => key !== canonical[index])
) {
throw new LocalSecurityAuditQueryConfigurationError(
`${label} shape is invalid`,
);
}
}
function normalizeRequest(
value: ListLocalSecurityAuditRequest,
): Readonly<ListLocalSecurityAuditRequest> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new LocalSecurityAuditQueryConfigurationError(
'request must be an object',
);
}
exactKeys(
value,
['authorityProjectId', 'query', 'auditEventId', 'requestId', 'principal'],
'request',
);
try {
assertProjectPolicyProjectId(value.authorityProjectId);
} catch {
throw new LocalSecurityAuditQueryConfigurationError(
'authority Project identity is invalid',
);
}
if (
typeof value.auditEventId !== 'string' ||
!UUID_V4_PATTERN.test(value.auditEventId) ||
typeof value.requestId !== 'string' ||
!REQUEST_ID_PATTERN.test(value.requestId)
) {
throw new LocalSecurityAuditQueryConfigurationError(
'audit or request identity is invalid',
);
}
let query: Readonly<SecurityAuditQuery>;
try {
query = normalizeSecurityAuditQuery(value.query);
} catch {
throw new LocalSecurityAuditQueryConfigurationError(
'filter, cursor, or limit is invalid',
);
}
if (query.limit > MAX_LOCAL_SECURITY_AUDIT_QUERY_PAGE_SIZE) {
throw new LocalSecurityAuditQueryConfigurationError(
'limit exceeds the local maximum of 64',
);
}
return Object.freeze({ ...value, query });
}
function strongUser(
value: SecurityPrincipal,
nowMs: number,
): Readonly<SecurityPrincipal> {
let principal: Readonly<SecurityPrincipal>;
try {
principal = normalizeSecurityPrincipal(value, nowMs);
} catch {
throw new LocalSecurityAuditQueryAuthenticationError();
}
if (
principal.subject.type !== 'user' ||
!STRONG_USER_ASSURANCES.has(principal.assurance)
) {
throw new LocalSecurityAuditQueryAuthenticationError();
}
return principal;
}
function auditRecord(options: {
readonly request: Readonly<ListLocalSecurityAuditRequest>;
readonly principal: Readonly<SecurityPrincipal>;
readonly outcome: SecurityAuditRecord['outcome'];
readonly reasons: readonly string[];
readonly fence: SecurityAuditRecord['fence'];
readonly occurredAtMs: number;
}): Readonly<SecurityAuditRecord> {
return normalizeSecurityAuditRecord({
eventId: options.request.auditEventId,
requestId: options.request.requestId,
operationId: 'security.audit.list',
projectId: options.request.authorityProjectId,
subject: options.principal.subject,
authenticationId: options.principal.authenticationId,
outcome: options.outcome,
reasons: options.reasons,
fence: options.fence,
occurredAtMs: options.occurredAtMs,
});
}
export function createLocalSecurityAuditQueryService(
projectPolicy: ProjectPolicyRepository,
repository: LocalSecurityAuditQueryRepository,
options: { readonly now?: () => number } = {},
): LocalSecurityAuditQueryService {
if (
!projectPolicy ||
typeof projectPolicy.resolve !== 'function' ||
!repository ||
typeof repository.listAuthorized !== 'function' ||
typeof repository.record !== 'function' ||
(options.now !== undefined && typeof options.now !== 'function')
) {
throw new LocalSecurityAuditQueryConfigurationError(
'dependencies are invalid',
);
}
const now = options.now ?? Date.now;
const policy = new ProjectPolicyEngine(projectPolicy);
return Object.freeze({
async list(input: ListLocalSecurityAuditRequest) {
const request = normalizeRequest(input);
const occurredAtMs = now();
const principal = strongUser(request.principal, occurredAtMs);
let decision;
try {
decision = await policy.authorize(
principal,
request.authorityProjectId,
'project.manage',
);
} catch {
try {
await repository.record(
auditRecord({
request,
principal,
outcome: 'authorization_unavailable',
reasons: ['policy_unavailable'],
fence: null,
occurredAtMs,
}),
);
} catch {
throw new LocalSecurityAuditQueryUnavailableError();
}
throw new LocalSecurityAuditQueryUnavailableError();
}
if (decision.effect !== 'allow' || !decision.fence?.bindingVersion) {
try {
await repository.record(
auditRecord({
request,
principal,
outcome:
decision.effect === 'require_approval'
? 'approval_required'
: 'denied',
reasons: decision.reasons,
fence: decision.fence,
occurredAtMs,
}),
);
} catch {
throw new LocalSecurityAuditQueryUnavailableError();
}
throw new LocalSecurityAuditQueryAuthorizationError();
}
try {
return await repository.listAuthorized({
query: request.query,
authorization: {
authorityProjectId: request.authorityProjectId,
actor: principal.subject,
fence: decision.fence,
},
audit: auditRecord({
request,
principal,
outcome: 'allowed',
reasons: ['instance_authority_security_audit_query'],
fence: decision.fence,
occurredAtMs,
}),
});
} catch (error) {
if (
error instanceof
LocalSecurityAuditQueryAuthorizationFenceConflictError
) {
throw error;
}
throw new LocalSecurityAuditQueryUnavailableError();
}
},
});
}
@@ -0,0 +1,299 @@
import {
LocalSecurityAuditCompactionMutationConflictError,
LocalSecurityAuditRetentionAuthorizationFenceConflictError,
LocalSecurityAuditRetentionUnavailableError,
MAX_LOCAL_SECURITY_AUDIT_RETENTION_MS,
MAX_STANDALONE_SECURITY_AUDIT_COMPACTION_BATCH_SIZE,
MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS,
type CompactAuthorizedLocalSecurityAuditResult,
type LocalSecurityAuditRetentionRepository,
} from '@qinglong/runtime-core/local-security-audit-retention';
import {
ProjectPolicyEngine,
assertProjectPolicyProjectId,
type ProjectPolicyRepository,
} from '@qinglong/runtime-core/project-policy';
import {
normalizeSecurityPrincipal,
type SecurityPrincipal,
} from '@qinglong/runtime-core/security';
import {
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
} from '@qinglong/runtime-core/security-audit';
const UUID_V4_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const STRONG_USER_ASSURANCES = new Set([
'multi_factor',
'hardware',
'local_console',
]);
export interface CompactLocalSecurityAuditRequest {
readonly authorityProjectId: string;
readonly retentionMs: number;
readonly eligibleBeforeMs: number;
readonly limit: number;
readonly mutationId: string;
readonly requestId: string;
readonly failureAuditEventId: string;
readonly principal: SecurityPrincipal;
}
export interface LocalSecurityAuditRetentionService {
compact(
request: CompactLocalSecurityAuditRequest,
): Promise<CompactAuthorizedLocalSecurityAuditResult>;
}
export class LocalSecurityAuditRetentionConfigurationError extends TypeError {
readonly code = 'LOCAL_SECURITY_AUDIT_RETENTION_INVALID';
constructor(message: string) {
super(`Local security audit retention is invalid: ${message}`);
this.name = 'LocalSecurityAuditRetentionConfigurationError';
}
}
export class LocalSecurityAuditRetentionAuthenticationError extends Error {
readonly code = 'LOCAL_SECURITY_AUDIT_RETENTION_AUTHENTICATION_REQUIRED';
constructor() {
super('Local security audit retention requires a strong User');
this.name = 'LocalSecurityAuditRetentionAuthenticationError';
}
}
export class LocalSecurityAuditRetentionAuthorizationError extends Error {
readonly code = 'LOCAL_SECURITY_AUDIT_RETENTION_FORBIDDEN';
constructor() {
super('Local security audit retention is not authorized');
this.name = 'LocalSecurityAuditRetentionAuthorizationError';
}
}
function exactKeys(
value: object,
expected: readonly string[],
label: string,
): void {
const actual = Object.keys(value).sort();
const canonical = [...expected].sort();
if (
actual.length !== canonical.length ||
actual.some((key, index) => key !== canonical[index])
) {
throw new LocalSecurityAuditRetentionConfigurationError(
`${label} shape is invalid`,
);
}
}
function request(
value: CompactLocalSecurityAuditRequest,
nowMs: number,
): Readonly<CompactLocalSecurityAuditRequest> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new LocalSecurityAuditRetentionConfigurationError(
'request must be an object',
);
}
exactKeys(
value,
[
'authorityProjectId',
'retentionMs',
'eligibleBeforeMs',
'limit',
'mutationId',
'requestId',
'failureAuditEventId',
'principal',
],
'request',
);
try {
assertProjectPolicyProjectId(value.authorityProjectId);
} catch {
throw new LocalSecurityAuditRetentionConfigurationError(
'authority Project identity is invalid',
);
}
if (
!UUID_V4_PATTERN.test(value.mutationId) ||
!UUID_V4_PATTERN.test(value.failureAuditEventId) ||
value.mutationId === value.failureAuditEventId ||
!REQUEST_ID_PATTERN.test(value.requestId) ||
!Number.isSafeInteger(value.retentionMs) ||
value.retentionMs < MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS ||
value.retentionMs > MAX_LOCAL_SECURITY_AUDIT_RETENTION_MS ||
!Number.isSafeInteger(value.eligibleBeforeMs) ||
value.eligibleBeforeMs < 0 ||
value.eligibleBeforeMs + value.retentionMs > nowMs ||
!Number.isSafeInteger(value.limit) ||
value.limit < 1 ||
value.limit > MAX_STANDALONE_SECURITY_AUDIT_COMPACTION_BATCH_SIZE
) {
throw new LocalSecurityAuditRetentionConfigurationError(
'identity, retention fence, or limit is invalid',
);
}
return Object.freeze({ ...value });
}
function strongUser(
value: SecurityPrincipal,
nowMs: number,
): Readonly<SecurityPrincipal> {
let principal: Readonly<SecurityPrincipal>;
try {
principal = normalizeSecurityPrincipal(value, nowMs);
} catch {
throw new LocalSecurityAuditRetentionAuthenticationError();
}
if (
principal.subject.type !== 'user' ||
!STRONG_USER_ASSURANCES.has(principal.assurance)
) {
throw new LocalSecurityAuditRetentionAuthenticationError();
}
return principal;
}
function auditRecord(options: {
readonly eventId: string;
readonly request: Readonly<CompactLocalSecurityAuditRequest>;
readonly principal: Readonly<SecurityPrincipal>;
readonly outcome: SecurityAuditRecord['outcome'];
readonly reasons: readonly string[];
readonly fence: SecurityAuditRecord['fence'];
readonly occurredAtMs: number;
}): Readonly<SecurityAuditRecord> {
return normalizeSecurityAuditRecord({
eventId: options.eventId,
requestId: options.request.requestId,
operationId: 'security.audit.compact',
projectId: options.request.authorityProjectId,
subject: options.principal.subject,
authenticationId: options.principal.authenticationId,
outcome: options.outcome,
reasons: options.reasons,
fence: options.fence,
occurredAtMs: options.occurredAtMs,
});
}
export function createLocalSecurityAuditRetentionService(
projectPolicy: ProjectPolicyRepository,
repository: LocalSecurityAuditRetentionRepository,
options: { readonly now?: () => number } = {},
): LocalSecurityAuditRetentionService {
if (
!projectPolicy ||
typeof projectPolicy.resolve !== 'function' ||
!repository ||
typeof repository.resolveCompaction !== 'function' ||
typeof repository.compactAuthorized !== 'function' ||
typeof repository.record !== 'function' ||
(options.now !== undefined && typeof options.now !== 'function')
) {
throw new LocalSecurityAuditRetentionConfigurationError(
'dependencies are invalid',
);
}
const now = options.now ?? Date.now;
const policy = new ProjectPolicyEngine(projectPolicy);
return Object.freeze({
async compact(input: CompactLocalSecurityAuditRequest) {
const occurredAtMs = now();
if (!Number.isSafeInteger(occurredAtMs) || occurredAtMs < 0) {
throw new LocalSecurityAuditRetentionConfigurationError(
'trusted clock is invalid',
);
}
const command = request(input, occurredAtMs);
const principal = strongUser(command.principal, occurredAtMs);
let decision;
try {
decision = await policy.authorize(
principal,
command.authorityProjectId,
'project.manage',
);
} catch {
try {
await repository.record(
auditRecord({
eventId: command.failureAuditEventId,
request: command,
principal,
outcome: 'authorization_unavailable',
reasons: ['policy_unavailable'],
fence: null,
occurredAtMs,
}),
);
} catch {
throw new LocalSecurityAuditRetentionUnavailableError();
}
throw new LocalSecurityAuditRetentionUnavailableError();
}
if (decision.effect !== 'allow' || !decision.fence?.bindingVersion) {
try {
await repository.record(
auditRecord({
eventId: command.failureAuditEventId,
request: command,
principal,
outcome:
decision.effect === 'require_approval'
? 'approval_required'
: 'denied',
reasons: decision.reasons,
fence: decision.fence,
occurredAtMs,
}),
);
} catch {
throw new LocalSecurityAuditRetentionUnavailableError();
}
throw new LocalSecurityAuditRetentionAuthorizationError();
}
try {
return await repository.compactAuthorized({
mutationId: command.mutationId,
requestId: command.requestId,
retentionMs: command.retentionMs,
eligibleBeforeMs: command.eligibleBeforeMs,
limit: command.limit,
authorization: {
authorityProjectId: command.authorityProjectId,
actor: principal.subject,
fence: decision.fence,
},
audit: auditRecord({
eventId: command.mutationId,
request: command,
principal,
outcome: 'allowed',
reasons: ['instance_authority_security_audit_compaction'],
fence: decision.fence,
occurredAtMs,
}),
});
} catch (error) {
if (
error instanceof
LocalSecurityAuditRetentionAuthorizationFenceConflictError ||
error instanceof LocalSecurityAuditCompactionMutationConflictError
) {
throw error;
}
throw new LocalSecurityAuditRetentionUnavailableError();
}
},
});
}