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,108 @@
import {
ApiCredentialUnavailableError,
assertApiCredentialId,
normalizeApiCredentialRecord,
type ApiCredentialRecord,
type ApiCredentialRepository,
} from '@qinglong/runtime-core/api-credential';
import type { DatabaseSync } from 'node:sqlite';
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
type CredentialRow = Record<string, unknown>;
function text(row: CredentialRow, name: string): string {
const value = row[name];
if (typeof value !== 'string') throw new ApiCredentialUnavailableError();
return value;
}
function integer(row: CredentialRow, name: string): number {
const value = row[name];
if (!Number.isSafeInteger(value)) {
throw new ApiCredentialUnavailableError();
}
return value as number;
}
function record(row: CredentialRow): Readonly<ApiCredentialRecord> {
try {
return normalizeApiCredentialRecord({
credentialId: text(row, 'credentialId'),
version: integer(row, 'version'),
pepperKeyId: text(row, 'pepperKeyId'),
state: text(row, 'state') as ApiCredentialRecord['state'],
subject: {
type: text(
row,
'subjectType',
) as ApiCredentialRecord['subject']['type'],
id: text(row, 'subjectId'),
},
subjectStatus: text(
row,
'subjectStatus',
) as ApiCredentialRecord['subjectStatus'],
secretDigest: text(row, 'secretDigest'),
createdAtMs: integer(row, 'createdAtMs'),
notBeforeAtMs: integer(row, 'notBeforeAtMs'),
expiresAtMs: integer(row, 'expiresAtMs'),
});
} catch (error) {
if (error instanceof ApiCredentialUnavailableError) throw error;
throw new ApiCredentialUnavailableError();
}
}
export class LocalSqliteApiCredentialRepository
implements ApiCredentialRepository
{
private readonly authority: LocalSqliteOperationAuthority;
constructor(authority: LocalSqliteOperationAuthority | DatabaseSync) {
this.authority =
authority instanceof LocalSqliteOperationAuthority
? authority
: new LocalSqliteOperationAuthority(authority);
}
resolve(credentialId: string): Promise<Readonly<ApiCredentialRecord> | null> {
assertApiCredentialId(credentialId);
return this.authority.enqueue(
async () => {
try {
const row = this.authority.client
.prepare(
`SELECT
credential."credential_id" AS "credentialId",
credential."version" AS "version",
pepper."pepper_key_id" AS "pepperKeyId",
credential."state" AS "state",
credential."subject_type" AS "subjectType",
credential."subject_id" AS "subjectId",
identity."status" AS "subjectStatus",
credential."secret_digest" AS "secretDigest",
credential."created_at_ms" AS "createdAtMs",
credential."not_before_at_ms" AS "notBeforeAtMs",
credential."expires_at_ms" AS "expiresAtMs"
FROM "QingLong3ApiCredentials" AS credential
JOIN "QingLong3IdentitySubjects" AS identity
ON identity."subject_type" = credential."subject_type"
AND identity."subject_id" = credential."subject_id"
LEFT JOIN "QingLong3ApiCredentialPepperBindings" AS pepper
ON pepper."credential_id" = credential."credential_id"
AND pepper."credential_version" = credential."version"
WHERE credential."credential_id" = ?
ORDER BY credential."version" DESC
LIMIT 1`,
)
.get(credentialId) as CredentialRow | undefined;
return row ? record(row) : null;
} catch (error) {
if (error instanceof ApiCredentialUnavailableError) throw error;
throw new ApiCredentialUnavailableError();
}
},
() => new ApiCredentialUnavailableError(),
);
}
}
@@ -0,0 +1,115 @@
import {
LocalIdentityCredentialAuthorizationFenceConflictError,
type LocalIdentityAdministrationAuthorization,
} from '@qinglong/runtime-core/local-identity-credential-administration';
import type { SecuritySubject } from '@qinglong/runtime-core/security';
import { LocalSqliteOperationAuthority } from '../../authority/operationAuthority';
import { resolveLocalInstanceAuthorityProjectId } from '../../authority/instanceAuthorityProject';
import {
LOCAL_ROLE_BINDING_SELECT,
localRoleBindingFromRow,
} from '../securityPersistence';
import { integer, type Row } from './codec';
export function assertAuthorizationInTransaction(
authority: LocalSqliteOperationAuthority,
auth: Readonly<LocalIdentityAdministrationAuthorization>,
beforeMutation: () => void,
): void {
try {
beforeMutation();
} catch {
throw new LocalIdentityCredentialAuthorizationFenceConflictError();
}
if (
resolveLocalInstanceAuthorityProjectId(authority.client) !== auth.projectId
) {
throw new LocalIdentityCredentialAuthorizationFenceConflictError();
}
const project = authority.client
.prepare(
`SELECT "status" AS "status", "version" AS "version"
FROM "QingLong3Projects" WHERE "id" = ?`,
)
.get(auth.projectId) as Row | undefined;
const actorRow = authority.client
.prepare(
`SELECT ${LOCAL_ROLE_BINDING_SELECT}
FROM "QingLong3ProjectRoleBindings"
WHERE "project_id" = ? AND "subject_type" = ?
AND "subject_id" = ?
ORDER BY "version" DESC LIMIT 1`,
)
.get(auth.projectId, auth.actor.type, auth.actor.id) as Row | undefined;
if (
!project ||
project.status !== 'active' ||
integer(project, 'version') !== auth.fence.projectVersion ||
!actorRow
) {
throw new LocalIdentityCredentialAuthorizationFenceConflictError();
}
const binding = localRoleBindingFromRow(actorRow);
if (
binding.version !== auth.fence.bindingVersion ||
binding.state !== 'active' ||
binding.role !== 'owner'
) {
throw new LocalIdentityCredentialAuthorizationFenceConflictError();
}
}
export function activeOwnerBindingExists(
authority: LocalSqliteOperationAuthority,
subject: Readonly<SecuritySubject>,
): boolean {
return !!authority.client
.prepare(
`SELECT 1 AS "present"
FROM "QingLong3ProjectRoleBindings" AS binding
WHERE binding."subject_type" = ? AND binding."subject_id" = ?
AND binding."state" = 'active' AND binding."role" = 'owner'
AND binding."version" = (
SELECT max(latest."version")
FROM "QingLong3ProjectRoleBindings" AS latest
WHERE latest."project_id" = binding."project_id"
AND latest."subject_type" = binding."subject_type"
AND latest."subject_id" = binding."subject_id"
)
LIMIT 1`,
)
.get(subject.type, subject.id);
}
export function anotherActiveCredentialExists(
authority: LocalSqliteOperationAuthority,
subject: Readonly<SecuritySubject>,
excludedCredentialId: string,
nowMs: number,
): boolean {
return !!authority.client
.prepare(
`SELECT 1 AS "present"
FROM "QingLong3ApiCredentials" AS credential
JOIN "QingLong3ApiCredentialPepperBindings" AS binding
ON binding."credential_id" = credential."credential_id"
AND binding."credential_version" = credential."version"
JOIN "QingLong3LocalOwnerPepperKeys" AS pepper
ON pepper."pepper_key_id" = binding."pepper_key_id"
WHERE credential."subject_type" = ?
AND credential."subject_id" = ?
AND credential."credential_id" <> ?
AND credential."state" = 'active'
AND credential."not_before_at_ms" <= ?
AND credential."expires_at_ms" > ?
AND pepper."state" IN ('active','retired')
AND credential."version" = (
SELECT max(latest."version")
FROM "QingLong3ApiCredentials" AS latest
WHERE latest."credential_id" = credential."credential_id"
)
LIMIT 1`,
)
.get(subject.type, subject.id, excludedCredentialId, nowMs, nowMs);
}
@@ -0,0 +1,342 @@
import { type ApiCredentialMutationRecord } from '@qinglong/runtime-core/api-credential-administration';
import {
normalizeApiCredentialRecord,
type ApiCredentialRecord,
} from '@qinglong/runtime-core/api-credential';
import {
normalizeIdentitySubjectRecord,
type IdentitySubjectMutationRecord,
type IdentitySubjectRecord,
} from '@qinglong/runtime-core/identity-administration';
import {
LocalIdentityCredentialAdministrationUnavailableError,
type LocalIdentityAdministrationAuthorization,
type ResolvedLocalApiCredentialMutation,
type ResolvedLocalIdentitySubjectMutation,
} from '@qinglong/runtime-core/local-identity-credential-administration';
import {
assertProjectPolicyProjectId,
normalizeProjectPolicySubject,
} from '@qinglong/runtime-core/project-policy';
import type {
SecurityPolicyFence,
SecuritySubject,
} from '@qinglong/runtime-core/security';
import {
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
} from '@qinglong/runtime-core/security-audit';
import { sameSecurityAuditSemantic } from '../securityPersistence';
export type Row = Record<string, unknown>;
export const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
export const ADMINISTRABLE_SUBJECT_TYPES = new Set([
'user',
'api_app',
'mcp_client',
'agent',
]);
export const IDENTITY_MUTATION_SELECT = `
mutation."mutation_id" AS "mutationId",
mutation."project_id" AS "mutationProjectId",
mutation."operation" AS "operation",
mutation."subject_type" AS "targetSubjectType",
mutation."subject_id" AS "targetSubjectId",
mutation."subject_version" AS "subjectVersion",
mutation."expected_previous_version" AS "expectedPreviousVersion",
mutation."status" AS "status",
mutation."changed_by_type" AS "changedByType",
mutation."changed_by_id" AS "changedById",
mutation."identity_created_at_ms" AS "identityCreatedAtMs",
mutation."created_at_ms" AS "createdAtMs"
`;
export const CREDENTIAL_MUTATION_SELECT = `
mutation."mutation_id" AS "mutationId",
mutation."project_id" AS "mutationProjectId",
mutation."operation" AS "operation",
mutation."credential_id" AS "credentialId",
mutation."credential_version" AS "credentialVersion",
mutation."expected_previous_version" AS "expectedPreviousVersion",
mutation."subject_type" AS "targetSubjectType",
mutation."subject_id" AS "targetSubjectId",
mutation."subject_status" AS "subjectStatus",
mutation."state" AS "state",
mutation."pepper_key_id" AS "pepperKeyId",
mutation."secret_digest" AS "secretDigest",
mutation."not_before_at_ms" AS "notBeforeAtMs",
mutation."expires_at_ms" AS "expiresAtMs",
mutation."delivery_digest" AS "deliveryDigest",
mutation."changed_by_type" AS "changedByType",
mutation."changed_by_id" AS "changedById",
mutation."created_at_ms" AS "createdAtMs"
`;
export const ADMIN_AUDIT_SELECT = `
audit."event_id" AS "eventId",
audit."request_id" AS "requestId",
audit."operation_id" AS "operationId",
audit."project_id" AS "projectId",
audit."subject_type" AS "subjectType",
audit."subject_id" AS "subjectId",
audit."authentication_id" AS "authenticationId",
audit."outcome" AS "outcome",
audit."reasons_json" AS "reasonsJson",
audit."fence_project_version" AS "fenceProjectVersion",
audit."fence_binding_version" AS "fenceBindingVersion",
audit."occurred_at_ms" AS "occurredAtMs"
`;
export function integer(row: Row, key: string): number {
const value = row[key];
if (!Number.isSafeInteger(value)) {
throw new LocalIdentityCredentialAdministrationUnavailableError();
}
return value as number;
}
export function text(row: Row, key: string): string {
const value = row[key];
if (typeof value !== 'string') {
throw new LocalIdentityCredentialAdministrationUnavailableError();
}
return value;
}
export function optionalText(row: Row, key: string): string | null {
const value = row[key];
if (value !== null && typeof value !== 'string') {
throw new LocalIdentityCredentialAdministrationUnavailableError();
}
return value as string | null;
}
export function sameSubject(
left: Readonly<SecuritySubject>,
right: Readonly<SecuritySubject>,
): boolean {
return left.type === right.type && left.id === right.id;
}
export function exactFence(
value: SecurityPolicyFence,
): Readonly<SecurityPolicyFence> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Object.keys(value).sort().join(',') !== 'bindingVersion,projectVersion' ||
!Number.isSafeInteger(value.projectVersion) ||
value.projectVersion < 1 ||
!Number.isSafeInteger(value.bindingVersion) ||
(value.bindingVersion as number) < 1
) {
throw new TypeError('Local Identity administration fence is invalid');
}
return Object.freeze({
projectVersion: value.projectVersion,
bindingVersion: value.bindingVersion,
});
}
export function authorization(
value: LocalIdentityAdministrationAuthorization,
): Readonly<LocalIdentityAdministrationAuthorization> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Object.keys(value).sort().join(',') !== 'actor,fence,projectId'
) {
throw new TypeError(
'Local Identity administration authorization is invalid',
);
}
assertProjectPolicyProjectId(value.projectId);
const actor = normalizeProjectPolicySubject(value.actor);
if (actor.type !== 'user') {
throw new TypeError('Local Identity administration actor is invalid');
}
return Object.freeze({
projectId: value.projectId,
actor,
fence: exactFence(value.fence),
});
}
export function assertAllowedAudit(
input: SecurityAuditRecord,
operationId: string,
mutationId: string,
auth: Readonly<LocalIdentityAdministrationAuthorization>,
): Readonly<SecurityAuditRecord> {
const audit = normalizeSecurityAuditRecord(input);
if (
audit.eventId !== mutationId ||
audit.operationId !== operationId ||
audit.projectId !== auth.projectId ||
!audit.subject ||
!sameSubject(audit.subject, auth.actor) ||
audit.outcome !== 'allowed' ||
audit.fence?.projectVersion !== auth.fence.projectVersion ||
audit.fence.bindingVersion !== auth.fence.bindingVersion
) {
throw new TypeError('Local Identity administration audit is invalid');
}
return audit;
}
export function identityFromRow(row: Row): Readonly<IdentitySubjectRecord> {
return normalizeIdentitySubjectRecord({
subject: {
type: text(row, 'subjectType') as SecuritySubject['type'],
id: text(row, 'subjectId'),
},
status: text(row, 'status') as IdentitySubjectRecord['status'],
version: integer(row, 'version'),
createdAtMs: integer(row, 'createdAtMs'),
updatedAtMs: integer(row, 'updatedAtMs'),
});
}
export function identityMutationFromRow(
row: Row,
): Readonly<IdentitySubjectMutationRecord> {
return Object.freeze({
mutationId: text(row, 'mutationId'),
operation: text(
row,
'operation',
) as IdentitySubjectMutationRecord['operation'],
subject: Object.freeze({
type: text(row, 'targetSubjectType') as SecuritySubject['type'],
id: text(row, 'targetSubjectId'),
}),
subjectVersion: integer(row, 'subjectVersion'),
expectedPreviousVersion: integer(row, 'expectedPreviousVersion'),
status: text(row, 'status') as IdentitySubjectMutationRecord['status'],
changedBy: Object.freeze({
type: text(row, 'changedByType') as SecuritySubject['type'],
id: text(row, 'changedById'),
}),
createdAtMs: integer(row, 'createdAtMs'),
});
}
export function identityResultFromMutationRow(
row: Row,
): Readonly<IdentitySubjectRecord> {
return normalizeIdentitySubjectRecord({
subject: {
type: text(row, 'targetSubjectType') as SecuritySubject['type'],
id: text(row, 'targetSubjectId'),
},
status: text(row, 'status') as IdentitySubjectRecord['status'],
version: integer(row, 'subjectVersion'),
createdAtMs: integer(row, 'identityCreatedAtMs'),
updatedAtMs: integer(row, 'createdAtMs'),
});
}
export function credentialFromMutationRow(
row: Row,
): Readonly<ApiCredentialRecord> {
return normalizeApiCredentialRecord({
credentialId: text(row, 'credentialId'),
version: integer(row, 'credentialVersion'),
pepperKeyId: text(row, 'pepperKeyId'),
state: text(row, 'state') as ApiCredentialRecord['state'],
subject: {
type: text(row, 'targetSubjectType') as SecuritySubject['type'],
id: text(row, 'targetSubjectId'),
},
subjectStatus: text(
row,
'subjectStatus',
) as ApiCredentialRecord['subjectStatus'],
secretDigest: text(row, 'secretDigest'),
createdAtMs: integer(row, 'createdAtMs'),
notBeforeAtMs: integer(row, 'notBeforeAtMs'),
expiresAtMs: integer(row, 'expiresAtMs'),
});
}
export function credentialMutationFromRow(
row: Row,
): Readonly<ApiCredentialMutationRecord> {
return Object.freeze({
mutationId: text(row, 'mutationId'),
operation: text(
row,
'operation',
) as ApiCredentialMutationRecord['operation'],
credentialId: text(row, 'credentialId'),
credentialVersion: integer(row, 'credentialVersion'),
expectedPreviousVersion: integer(row, 'expectedPreviousVersion'),
changedBy: Object.freeze({
type: text(row, 'changedByType') as SecuritySubject['type'],
id: text(row, 'changedById'),
}),
createdAtMs: integer(row, 'createdAtMs'),
});
}
export function sameIdentitySemantic(
existing: Readonly<ResolvedLocalIdentitySubjectMutation>,
expected: Readonly<{
projectId: string;
identity: IdentitySubjectRecord;
mutation: IdentitySubjectMutationRecord;
audit: SecurityAuditRecord;
}>,
): boolean {
return (
existing.projectId === expected.projectId &&
sameSubject(existing.identity.subject, expected.identity.subject) &&
existing.identity.status === expected.identity.status &&
existing.identity.version === expected.identity.version &&
existing.mutation.operation === expected.mutation.operation &&
existing.mutation.subjectVersion === expected.mutation.subjectVersion &&
existing.mutation.expectedPreviousVersion ===
expected.mutation.expectedPreviousVersion &&
sameSubject(existing.mutation.changedBy, expected.mutation.changedBy) &&
sameSecurityAuditSemantic(existing.audit, expected.audit)
);
}
export function sameCredentialSemantic(
existing: Readonly<ResolvedLocalApiCredentialMutation>,
expected: Readonly<{
projectId: string;
credential: ApiCredentialRecord;
mutation: ApiCredentialMutationRecord;
deliveryDigest: string | null;
audit: SecurityAuditRecord;
}>,
): boolean {
const left = existing.credential;
const right = expected.credential;
return (
existing.projectId === expected.projectId &&
existing.mutation.operation === expected.mutation.operation &&
existing.mutation.credentialId === expected.mutation.credentialId &&
existing.mutation.credentialVersion ===
expected.mutation.credentialVersion &&
existing.mutation.expectedPreviousVersion ===
expected.mutation.expectedPreviousVersion &&
sameSubject(existing.mutation.changedBy, expected.mutation.changedBy) &&
left.credentialId === right.credentialId &&
left.version === right.version &&
left.pepperKeyId === right.pepperKeyId &&
left.state === right.state &&
sameSubject(left.subject, right.subject) &&
left.subjectStatus === right.subjectStatus &&
left.secretDigest === right.secretDigest &&
left.notBeforeAtMs === right.notBeforeAtMs &&
left.expiresAtMs === right.expiresAtMs &&
(existing.delivery?.digest ?? null) === expected.deliveryDigest &&
sameSecurityAuditSemantic(existing.audit, expected.audit)
);
}
@@ -0,0 +1,21 @@
import { LocalIdentityCredentialAdministrationUnavailableError } from '@qinglong/runtime-core/local-identity-credential-administration';
import { type SecurityAuditRecord } from '@qinglong/runtime-core/security-audit';
import { LocalSqliteOperationAuthority } from '../../authority/operationAuthority';
import { resolveLocalInstanceAuthorityProjectId } from '../../authority/instanceAuthorityProject';
import { LocalSqliteSecurityAuthorityStore } from '../securityAuthorityStore';
export function record(
authority: LocalSqliteOperationAuthority,
audit: SecurityAuditRecord,
): Promise<void> {
return new LocalSqliteSecurityAuthorityStore(authority).record(audit);
}
export function resolveAuthorityProjectId(
authority: LocalSqliteOperationAuthority,
): Promise<string | null> {
return authority.enqueue(
async () => resolveLocalInstanceAuthorityProjectId(authority.client),
() => new LocalIdentityCredentialAdministrationUnavailableError(),
);
}
@@ -0,0 +1,402 @@
import {
ApiCredentialAdministrationMutationConflictError,
ApiCredentialAdministrationSubjectNotFoundError,
ApiCredentialAdministrationVersionConflictError,
REVOKED_API_CREDENTIAL_DIGEST,
normalizeApiCredentialAdministrationMutationId,
} from '@qinglong/runtime-core/api-credential-administration';
import {
assertApiCredentialId,
normalizeApiCredentialRecord,
} from '@qinglong/runtime-core/api-credential';
import {
LocalCredentialOwnerContinuityError,
LocalIdentityCredentialAdministrationUnavailableError,
LocalIdentityCredentialAuthorizationFenceConflictError,
type AppendAuthorizedLocalApiCredentialCommand,
type AppendAuthorizedLocalApiCredentialResult,
type InspectAuthorizedLocalApiCredentialCommand,
type InspectAuthorizedLocalApiCredentialResult,
type ResolvedLocalApiCredentialMutation,
} from '@qinglong/runtime-core/local-identity-credential-administration';
import { SecurityAuditUnavailableError } from '@qinglong/runtime-core/security-audit';
import { LocalSqliteOperationAuthority } from '../../authority/operationAuthority';
import {
insertLocalSecurityAudit,
localSecurityAuditFromRow,
} from '../securityPersistence';
import {
ADMIN_AUDIT_SELECT,
ADMINISTRABLE_SUBJECT_TYPES,
CREDENTIAL_MUTATION_SELECT,
DIGEST_PATTERN,
assertAllowedAudit,
authorization,
credentialFromMutationRow,
credentialMutationFromRow,
identityFromRow,
integer,
optionalText,
sameCredentialSemantic,
sameSubject,
text,
type Row,
} from './codec';
import {
activeOwnerBindingExists,
anotherActiveCredentialExists,
assertAuthorizationInTransaction,
} from './authorization';
export function resolveCredentialMutation(
authority: LocalSqliteOperationAuthority,
requestedMutationId: string,
): Promise<Readonly<ResolvedLocalApiCredentialMutation> | null> {
normalizeApiCredentialAdministrationMutationId(requestedMutationId);
return authority.enqueue(
async () => {
const row = authority.client
.prepare(
`SELECT ${CREDENTIAL_MUTATION_SELECT},
${ADMIN_AUDIT_SELECT}
FROM "QingLong3ApiCredentialAdministrationMutations" AS mutation
JOIN "QingLong3SecurityAuditEvents" AS audit
ON audit."event_id" = mutation."audit_event_id"
WHERE mutation."mutation_id" = ?`,
)
.get(requestedMutationId) as Row | undefined;
if (!row) return null;
const deliveryDigest = optionalText(row, 'deliveryDigest');
return Object.freeze({
projectId: text(row, 'mutationProjectId'),
credential: credentialFromMutationRow(row),
mutation: credentialMutationFromRow(row),
delivery:
deliveryDigest === null
? null
: Object.freeze({ digest: deliveryDigest }),
audit: localSecurityAuditFromRow(row),
});
},
() => new LocalIdentityCredentialAdministrationUnavailableError(),
);
}
export function inspectAuthorizedCredential(
authority: LocalSqliteOperationAuthority,
beforeMutation: () => void,
input: InspectAuthorizedLocalApiCredentialCommand,
): Promise<InspectAuthorizedLocalApiCredentialResult> {
assertApiCredentialId(input.credentialId);
const auth = authorization(input.authorization);
const audit = assertAllowedAudit(
input.audit,
'credential.inspect',
input.audit.eventId,
auth,
);
return authority.enqueue(
async () => {
const client = authority.client;
client.exec('BEGIN IMMEDIATE');
try {
assertAuthorizationInTransaction(authority, auth, beforeMutation);
const row = client
.prepare(
`SELECT credential."credential_id" AS "credentialId",
credential."version" AS "credentialVersion",
pepper."pepper_key_id" AS "pepperKeyId",
credential."state" AS "state",
credential."subject_type" AS "targetSubjectType",
credential."subject_id" AS "targetSubjectId",
identity."status" AS "subjectStatus",
credential."secret_digest" AS "secretDigest",
credential."created_at_ms" AS "createdAtMs",
credential."not_before_at_ms" AS "notBeforeAtMs",
credential."expires_at_ms" AS "expiresAtMs"
FROM "QingLong3ApiCredentials" AS credential
JOIN "QingLong3IdentitySubjects" AS identity
ON identity."subject_type" = credential."subject_type"
AND identity."subject_id" = credential."subject_id"
LEFT JOIN "QingLong3ApiCredentialPepperBindings" AS pepper
ON pepper."credential_id" = credential."credential_id"
AND pepper."credential_version" = credential."version"
WHERE credential."credential_id" = ?
ORDER BY credential."version" DESC
LIMIT 1`,
)
.get(input.credentialId) as Row | undefined;
insertLocalSecurityAudit(client, audit);
client.exec('COMMIT');
return Object.freeze({
credential: row ? credentialFromMutationRow(row) : null,
audit,
});
} catch (error) {
if (client.isTransaction) client.exec('ROLLBACK');
if (
error instanceof
LocalIdentityCredentialAuthorizationFenceConflictError
) {
throw error;
}
if (error instanceof SecurityAuditUnavailableError) throw error;
throw new LocalIdentityCredentialAdministrationUnavailableError();
}
},
() => new LocalIdentityCredentialAdministrationUnavailableError(),
);
}
export function appendAuthorizedCredential(
authority: LocalSqliteOperationAuthority,
beforeMutation: () => void,
input: AppendAuthorizedLocalApiCredentialCommand,
): Promise<AppendAuthorizedLocalApiCredentialResult> {
const auth = authorization(input.authorization);
const credential = normalizeApiCredentialRecord(input.credential);
const mutation = input.mutation;
normalizeApiCredentialAdministrationMutationId(mutation.mutationId);
const deliveryDigest = input.delivery?.digest ?? null;
if (
!ADMINISTRABLE_SUBJECT_TYPES.has(credential.subject.type) ||
!Number.isSafeInteger(input.expectedCurrentVersion) ||
input.expectedCurrentVersion < 0 ||
mutation.credentialId !== credential.credentialId ||
mutation.credentialVersion !== input.expectedCurrentVersion + 1 ||
mutation.expectedPreviousVersion !== input.expectedCurrentVersion ||
credential.version !== mutation.credentialVersion ||
!sameSubject(mutation.changedBy, auth.actor) ||
(mutation.operation === 'issue'
? input.expectedCurrentVersion !== 0 ||
credential.state !== 'active' ||
deliveryDigest === null
: mutation.operation === 'rotate'
? input.expectedCurrentVersion < 1 ||
credential.state !== 'active' ||
deliveryDigest === null
: input.expectedCurrentVersion < 1 ||
credential.state !== 'revoked' ||
credential.secretDigest !== REVOKED_API_CREDENTIAL_DIGEST ||
deliveryDigest !== null) ||
(deliveryDigest !== null && !DIGEST_PATTERN.test(deliveryDigest))
) {
throw new TypeError(
'Local API credential administration command is invalid',
);
}
const audit = assertAllowedAudit(
input.audit,
`credential.${mutation.operation}`,
mutation.mutationId,
auth,
);
return authority.enqueue(
async () => {
const client = authority.client;
client.exec('BEGIN IMMEDIATE');
try {
assertAuthorizationInTransaction(authority, auth, beforeMutation);
const replayRow = client
.prepare(
`SELECT ${CREDENTIAL_MUTATION_SELECT},
${ADMIN_AUDIT_SELECT}
FROM "QingLong3ApiCredentialAdministrationMutations" AS mutation
JOIN "QingLong3SecurityAuditEvents" AS audit
ON audit."event_id" = mutation."audit_event_id"
WHERE mutation."mutation_id" = ?`,
)
.get(mutation.mutationId) as Row | undefined;
if (replayRow) {
const replayDelivery = optionalText(replayRow, 'deliveryDigest');
const existing = Object.freeze({
projectId: text(replayRow, 'mutationProjectId'),
credential: credentialFromMutationRow(replayRow),
mutation: credentialMutationFromRow(replayRow),
delivery:
replayDelivery === null
? null
: Object.freeze({ digest: replayDelivery }),
audit: localSecurityAuditFromRow(replayRow),
});
if (
!sameCredentialSemantic(existing, {
projectId: auth.projectId,
credential,
mutation,
deliveryDigest,
audit,
})
) {
throw new ApiCredentialAdministrationMutationConflictError();
}
client.exec('COMMIT');
return Object.freeze({
status: 'existing' as const,
credential: existing.credential,
mutation: existing.mutation,
delivery: existing.delivery,
audit: existing.audit,
});
}
const identityRow = client
.prepare(
`SELECT "subject_type" AS "subjectType",
"subject_id" AS "subjectId",
"status" AS "status", "version" AS "version",
"created_at_ms" AS "createdAtMs",
"updated_at_ms" AS "updatedAtMs"
FROM "QingLong3IdentitySubjects"
WHERE "subject_type" = ? AND "subject_id" = ?`,
)
.get(credential.subject.type, credential.subject.id) as
| Row
| undefined;
const identity = identityRow ? identityFromRow(identityRow) : null;
if (
!identity ||
(mutation.operation !== 'revoke' && identity.status !== 'active') ||
identity.status !== credential.subjectStatus
) {
throw new ApiCredentialAdministrationSubjectNotFoundError();
}
const currentRow = client
.prepare(
`SELECT "credential_id" AS "credentialId",
"version" AS "version", "state" AS "state",
"subject_type" AS "subjectType",
"subject_id" AS "subjectId"
FROM "QingLong3ApiCredentials"
WHERE "credential_id" = ?
ORDER BY "version" DESC LIMIT 1`,
)
.get(credential.credentialId) as Row | undefined;
const currentVersion = currentRow ? integer(currentRow, 'version') : 0;
if (
currentVersion !== input.expectedCurrentVersion ||
(currentRow &&
(text(currentRow, 'subjectType') !== credential.subject.type ||
text(currentRow, 'subjectId') !== credential.subject.id))
) {
throw new ApiCredentialAdministrationVersionConflictError();
}
if (
mutation.operation === 'revoke' &&
credential.subject.type === 'user' &&
activeOwnerBindingExists(authority, credential.subject) &&
!anotherActiveCredentialExists(
authority,
credential.subject,
credential.credentialId,
mutation.createdAtMs,
)
) {
throw new LocalCredentialOwnerContinuityError();
}
if (mutation.operation !== 'revoke') {
const pepper = client
.prepare(
`SELECT "state" AS "state"
FROM "QingLong3LocalOwnerPepperKeys"
WHERE "pepper_key_id" = ?`,
)
.get(credential.pepperKeyId) as Row | undefined;
if (!pepper || pepper.state !== 'active') {
throw new LocalIdentityCredentialAuthorizationFenceConflictError();
}
}
client
.prepare(
`INSERT INTO "QingLong3ApiCredentials" (
"credential_id", "version", "state", "subject_type",
"subject_id", "secret_digest", "created_at_ms",
"not_before_at_ms", "expires_at_ms"
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
credential.credentialId,
credential.version,
credential.state,
credential.subject.type,
credential.subject.id,
credential.secretDigest,
credential.createdAtMs,
credential.notBeforeAtMs,
credential.expiresAtMs,
);
client
.prepare(
`INSERT INTO "QingLong3ApiCredentialPepperBindings" (
"credential_id", "credential_version", "pepper_key_id"
) VALUES (?, ?, ?)`,
)
.run(
credential.credentialId,
credential.version,
credential.pepperKeyId,
);
insertLocalSecurityAudit(client, audit);
client
.prepare(
`INSERT INTO "QingLong3ApiCredentialAdministrationMutations" (
"mutation_id", "project_id", "operation", "credential_id",
"credential_version", "expected_previous_version",
"subject_type", "subject_id", "subject_status", "state",
"pepper_key_id", "secret_digest", "not_before_at_ms",
"expires_at_ms", "delivery_digest", "changed_by_type",
"changed_by_id", "audit_event_id", "created_at_ms"
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
mutation.mutationId,
auth.projectId,
mutation.operation,
credential.credentialId,
credential.version,
mutation.expectedPreviousVersion,
credential.subject.type,
credential.subject.id,
credential.subjectStatus,
credential.state,
credential.pepperKeyId,
credential.secretDigest,
credential.notBeforeAtMs,
credential.expiresAtMs,
deliveryDigest,
auth.actor.type,
auth.actor.id,
audit.eventId,
credential.createdAtMs,
);
client.exec('COMMIT');
return Object.freeze({
status: 'inserted' as const,
credential,
mutation: Object.freeze({ ...mutation }),
delivery:
deliveryDigest === null
? null
: Object.freeze({ digest: deliveryDigest }),
audit,
});
} catch (error) {
if (client.isTransaction) client.exec('ROLLBACK');
if (
error instanceof
LocalIdentityCredentialAuthorizationFenceConflictError ||
error instanceof LocalCredentialOwnerContinuityError ||
error instanceof ApiCredentialAdministrationSubjectNotFoundError ||
error instanceof ApiCredentialAdministrationVersionConflictError ||
error instanceof ApiCredentialAdministrationMutationConflictError
) {
throw error;
}
if (error instanceof SecurityAuditUnavailableError) throw error;
throw new LocalIdentityCredentialAdministrationUnavailableError();
}
},
() => new LocalIdentityCredentialAdministrationUnavailableError(),
);
}
@@ -0,0 +1,127 @@
import { type ApiCredentialRepository } from '@qinglong/runtime-core/api-credential';
import {
LocalIdentityCredentialAdministrationUnavailableError,
type LocalIdentityCredentialAdministrationRepository,
} from '@qinglong/runtime-core/local-identity-credential-administration';
import { type ProjectPolicyRepository } from '@qinglong/runtime-core/project-policy';
import type { LocalOwnerPepperRepository } from '@qinglong/runtime-core/local-owner-pepper';
import { LocalSqliteApiCredentialRepository } from '../apiCredentialRepository';
import {
assertLocalSqliteOptions,
assertLocalSqlitePathBoundary,
openLocalSqliteClient,
type LocalSqliteDatabaseOptions,
type LocalSqliteProfile,
} from '../../storage/config';
import { LocalSqliteOperationAuthority } from '../../authority/operationAuthority';
import { LocalSqliteOwnerPepperRepository } from '../../local-owner/ownerPepperRepository';
import {
confirmLocalSqliteAuthenticatedUserCredentialFence,
LocalSqliteAuthenticatedManagementFenceError,
type LocalSqliteAuthenticatedUserCredentialFence,
} from '../../administration/packageManagement';
import {
auditLocalSqliteReadiness,
type LocalSqliteReadinessEvidence,
} from '../../readiness/readiness';
import { LocalSqliteSecurityAuthorityStore } from '../securityAuthorityStore';
import { LocalSqliteIdentityCredentialAdministrationRepository } from './repository';
export interface LocalSqliteIdentityCredentialAdministrationDatabase {
readonly profile: LocalSqliteProfile;
readonly readiness: LocalSqliteReadinessEvidence;
readonly apiCredentials: ApiCredentialRepository;
readonly ownerPepper: Pick<
LocalOwnerPepperRepository,
'resolveActive' | 'resolveKey'
>;
readonly projectPolicy: ProjectPolicyRepository;
readonly identityCredentialAdministration: LocalIdentityCredentialAdministrationRepository;
activateUserCredentialFence(
fence: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
): void;
close(): Promise<void>;
}
function sameCredentialFence(
left: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
right: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
): boolean {
return (
left.credentialId === right.credentialId &&
left.credentialVersion === right.credentialVersion &&
left.pepperKeyId === right.pepperKeyId &&
left.materialDigest === right.materialDigest &&
left.subjectType === right.subjectType &&
left.subjectId === right.subjectId &&
left.secretDigest === right.secretDigest &&
left.notBeforeAtMs === right.notBeforeAtMs &&
left.expiresAtMs === right.expiresAtMs
);
}
export async function openLocalSqliteIdentityCredentialAdministrationDatabase(
options: LocalSqliteDatabaseOptions,
): Promise<LocalSqliteIdentityCredentialAdministrationDatabase> {
assertLocalSqliteOptions(options);
assertLocalSqlitePathBoundary(options.databasePath, false);
const client = openLocalSqliteClient(options, false);
try {
const readiness = await auditLocalSqliteReadiness(client);
const authority = new LocalSqliteOperationAuthority(client);
let activeFence:
| Readonly<LocalSqliteAuthenticatedUserCredentialFence>
| undefined;
const securityAuthority = new LocalSqliteSecurityAuthorityStore(authority);
const projectPolicy: ProjectPolicyRepository = Object.freeze({
resolve: (
...[projectId, subject]: Parameters<ProjectPolicyRepository['resolve']>
) => securityAuthority.resolve(projectId, subject),
append: (...[command]: Parameters<ProjectPolicyRepository['append']>) =>
securityAuthority.append(command),
});
const identityCredentialAdministration =
new LocalSqliteIdentityCredentialAdministrationRepository(
authority,
() => {
if (!activeFence) {
throw new LocalSqliteAuthenticatedManagementFenceError();
}
confirmLocalSqliteAuthenticatedUserCredentialFence(
authority,
activeFence,
);
},
);
let closePromise: Promise<void> | undefined;
return Object.freeze({
profile: options.profile,
readiness,
apiCredentials: new LocalSqliteApiCredentialRepository(authority),
ownerPepper: new LocalSqliteOwnerPepperRepository(authority),
projectPolicy,
identityCredentialAdministration,
activateUserCredentialFence(
fence: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
) {
confirmLocalSqliteAuthenticatedUserCredentialFence(authority, fence);
if (activeFence && !sameCredentialFence(activeFence, fence)) {
throw new LocalSqliteAuthenticatedManagementFenceError();
}
activeFence = Object.freeze({ ...fence });
},
close() {
if (!closePromise) {
closePromise = authority.close().catch(() => {
throw new LocalIdentityCredentialAdministrationUnavailableError();
});
}
return closePromise;
},
});
} catch (error) {
if (client.isOpen) client.close();
throw error;
}
}
@@ -0,0 +1,240 @@
import { normalizeApiCredentialAdministrationMutationId } from '@qinglong/runtime-core/api-credential-administration';
import {
LocalCredentialDeliveryMutationConflictError,
LocalIdentityCredentialAdministrationUnavailableError,
LocalIdentityCredentialAuthorizationFenceConflictError,
type AppendAuthorizedLocalCredentialDeliveryAcknowledgementCommand,
type AppendAuthorizedLocalCredentialDeliveryAcknowledgementResult,
type LocalCredentialDeliveryAcknowledgementRecord,
} from '@qinglong/runtime-core/local-identity-credential-administration';
import type { SecuritySubject } from '@qinglong/runtime-core/security';
import { SecurityAuditUnavailableError } from '@qinglong/runtime-core/security-audit';
import { LocalSqliteOperationAuthority } from '../../authority/operationAuthority';
import {
insertLocalSecurityAudit,
localSecurityAuditFromRow,
sameSecurityAuditSemantic,
} from '../securityPersistence';
import {
ADMIN_AUDIT_SELECT,
DIGEST_PATTERN,
assertAllowedAudit,
authorization,
integer,
optionalText,
sameSubject,
text,
type Row,
} from './codec';
import { assertAuthorizationInTransaction } from './authorization';
export function resolveDeliveryAcknowledgement(
authority: LocalSqliteOperationAuthority,
credentialMutationId: string,
): Promise<Readonly<LocalCredentialDeliveryAcknowledgementRecord> | null> {
normalizeApiCredentialAdministrationMutationId(credentialMutationId);
return authority.enqueue(
async () => {
const row = authority.client
.prepare(
`SELECT "credential_mutation_id" AS "credentialMutationId",
"acknowledgement_mutation_id" AS "acknowledgementMutationId",
"project_id" AS "projectId",
"delivery_digest" AS "deliveryDigest",
"acknowledged_by_type" AS "acknowledgedByType",
"acknowledged_by_id" AS "acknowledgedById",
"acknowledged_at_ms" AS "acknowledgedAtMs"
FROM "QingLong3ApiCredentialDeliveryAcknowledgements"
WHERE "credential_mutation_id" = ?`,
)
.get(credentialMutationId) as Row | undefined;
return row
? Object.freeze({
credentialMutationId: text(row, 'credentialMutationId'),
acknowledgementMutationId: text(row, 'acknowledgementMutationId'),
projectId: text(row, 'projectId'),
deliveryDigest: text(row, 'deliveryDigest'),
acknowledgedBy: Object.freeze({
type: text(row, 'acknowledgedByType') as SecuritySubject['type'],
id: text(row, 'acknowledgedById'),
}),
acknowledgedAtMs: integer(row, 'acknowledgedAtMs'),
})
: null;
},
() => new LocalIdentityCredentialAdministrationUnavailableError(),
);
}
export function appendAuthorizedDeliveryAcknowledgement(
authority: LocalSqliteOperationAuthority,
beforeMutation: () => void,
input: AppendAuthorizedLocalCredentialDeliveryAcknowledgementCommand,
): Promise<AppendAuthorizedLocalCredentialDeliveryAcknowledgementResult> {
const auth = authorization(input.authorization);
const acknowledgement = input.acknowledgement;
normalizeApiCredentialAdministrationMutationId(
acknowledgement.credentialMutationId,
);
normalizeApiCredentialAdministrationMutationId(
acknowledgement.acknowledgementMutationId,
);
if (
acknowledgement.credentialMutationId ===
acknowledgement.acknowledgementMutationId ||
acknowledgement.projectId !== auth.projectId ||
!DIGEST_PATTERN.test(acknowledgement.deliveryDigest) ||
!sameSubject(acknowledgement.acknowledgedBy, auth.actor) ||
!Number.isSafeInteger(acknowledgement.acknowledgedAtMs) ||
acknowledgement.acknowledgedAtMs < 0
) {
throw new TypeError('Local credential delivery acknowledgement is invalid');
}
const audit = assertAllowedAudit(
input.audit,
'credential.delivery.acknowledge',
acknowledgement.acknowledgementMutationId,
auth,
);
return authority.enqueue(
async () => {
const client = authority.client;
client.exec('BEGIN IMMEDIATE');
try {
assertAuthorizationInTransaction(authority, auth, beforeMutation);
const existing = client
.prepare(
`SELECT "credential_mutation_id" AS "credentialMutationId",
"acknowledgement_mutation_id" AS "acknowledgementMutationId",
"project_id" AS "projectId",
"delivery_digest" AS "deliveryDigest",
"acknowledged_by_type" AS "acknowledgedByType",
"acknowledged_by_id" AS "acknowledgedById",
"acknowledged_at_ms" AS "acknowledgedAtMs"
FROM "QingLong3ApiCredentialDeliveryAcknowledgements"
WHERE "credential_mutation_id" = ?
OR "acknowledgement_mutation_id" = ?
LIMIT 1`,
)
.get(
acknowledgement.credentialMutationId,
acknowledgement.acknowledgementMutationId,
) as Row | undefined;
if (existing) {
const stored = Object.freeze({
credentialMutationId: text(existing, 'credentialMutationId'),
acknowledgementMutationId: text(
existing,
'acknowledgementMutationId',
),
projectId: text(existing, 'projectId'),
deliveryDigest: text(existing, 'deliveryDigest'),
acknowledgedBy: Object.freeze({
type: text(
existing,
'acknowledgedByType',
) as SecuritySubject['type'],
id: text(existing, 'acknowledgedById'),
}),
acknowledgedAtMs: integer(existing, 'acknowledgedAtMs'),
});
const auditRow = client
.prepare(
`SELECT ${ADMIN_AUDIT_SELECT}
FROM "QingLong3SecurityAuditEvents" AS audit
WHERE "event_id" = ?`,
)
.get(stored.acknowledgementMutationId) as Row | undefined;
if (
stored.credentialMutationId !==
acknowledgement.credentialMutationId ||
stored.acknowledgementMutationId !==
acknowledgement.acknowledgementMutationId ||
stored.projectId !== acknowledgement.projectId ||
stored.deliveryDigest !== acknowledgement.deliveryDigest ||
!sameSubject(
stored.acknowledgedBy,
acknowledgement.acknowledgedBy,
) ||
!auditRow ||
!sameSecurityAuditSemantic(
localSecurityAuditFromRow(auditRow),
audit,
)
) {
throw new LocalCredentialDeliveryMutationConflictError();
}
client.exec('COMMIT');
return Object.freeze({
status: 'existing' as const,
acknowledgement: stored,
audit: localSecurityAuditFromRow(auditRow),
});
}
const credentialMutation = client
.prepare(
`SELECT "project_id" AS "projectId",
"delivery_digest" AS "deliveryDigest",
"operation" AS "operation"
FROM "QingLong3ApiCredentialAdministrationMutations"
WHERE "mutation_id" = ?`,
)
.get(acknowledgement.credentialMutationId) as Row | undefined;
if (
!credentialMutation ||
text(credentialMutation, 'projectId') !== auth.projectId ||
text(credentialMutation, 'operation') === 'revoke' ||
optionalText(credentialMutation, 'deliveryDigest') !==
acknowledgement.deliveryDigest
) {
throw new LocalCredentialDeliveryMutationConflictError();
}
insertLocalSecurityAudit(client, audit);
client
.prepare(
`INSERT INTO "QingLong3ApiCredentialDeliveryAcknowledgements" (
"credential_mutation_id", "acknowledgement_mutation_id",
"project_id", "delivery_digest", "acknowledged_by_type",
"acknowledged_by_id", "audit_event_id",
"acknowledged_at_ms"
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
acknowledgement.credentialMutationId,
acknowledgement.acknowledgementMutationId,
acknowledgement.projectId,
acknowledgement.deliveryDigest,
acknowledgement.acknowledgedBy.type,
acknowledgement.acknowledgedBy.id,
audit.eventId,
acknowledgement.acknowledgedAtMs,
);
client.exec('COMMIT');
return Object.freeze({
status: 'inserted' as const,
acknowledgement: Object.freeze({
...acknowledgement,
acknowledgedBy: Object.freeze({
...acknowledgement.acknowledgedBy,
}),
}),
audit,
});
} catch (error) {
if (client.isTransaction) client.exec('ROLLBACK');
if (
error instanceof
LocalIdentityCredentialAuthorizationFenceConflictError ||
error instanceof LocalCredentialDeliveryMutationConflictError
) {
throw error;
}
if (error instanceof SecurityAuditUnavailableError) throw error;
throw new LocalIdentityCredentialAdministrationUnavailableError();
}
},
() => new LocalIdentityCredentialAdministrationUnavailableError(),
);
}
@@ -0,0 +1,346 @@
import {
IdentityAdministrationMutationConflictError,
IdentityAdministrationVersionConflictError,
normalizeIdentityAdministrationMutationId,
normalizeIdentityAdministrationSubject,
normalizeIdentitySubjectRecord,
type IdentitySubjectRecord,
} from '@qinglong/runtime-core/identity-administration';
import {
LocalIdentityCredentialAdministrationUnavailableError,
LocalIdentityCredentialAuthorizationFenceConflictError,
LocalIdentityOwnerBindingConflictError,
type AppendAuthorizedLocalIdentityCommand,
type AppendAuthorizedLocalIdentityResult,
type InspectAuthorizedLocalIdentityCommand,
type InspectAuthorizedLocalIdentityResult,
type ResolvedLocalIdentitySubjectMutation,
} from '@qinglong/runtime-core/local-identity-credential-administration';
import type { SecuritySubject } from '@qinglong/runtime-core/security';
import { SecurityAuditUnavailableError } from '@qinglong/runtime-core/security-audit';
import { LocalSqliteOperationAuthority } from '../../authority/operationAuthority';
import {
insertLocalSecurityAudit,
localSecurityAuditFromRow,
} from '../securityPersistence';
import {
ADMIN_AUDIT_SELECT,
ADMINISTRABLE_SUBJECT_TYPES,
IDENTITY_MUTATION_SELECT,
assertAllowedAudit,
authorization,
identityFromRow,
identityMutationFromRow,
identityResultFromMutationRow,
sameIdentitySemantic,
sameSubject,
text,
type Row,
} from './codec';
import {
activeOwnerBindingExists,
assertAuthorizationInTransaction,
} from './authorization';
export function resolveIdentity(
authority: LocalSqliteOperationAuthority,
requested: SecuritySubject,
): Promise<Readonly<IdentitySubjectRecord> | null> {
const subject = normalizeIdentityAdministrationSubject(requested);
return authority.enqueue(
async () => {
const row = authority.client
.prepare(
`SELECT "subject_type" AS "subjectType",
"subject_id" AS "subjectId",
"status" AS "status", "version" AS "version",
"created_at_ms" AS "createdAtMs",
"updated_at_ms" AS "updatedAtMs"
FROM "QingLong3IdentitySubjects"
WHERE "subject_type" = ? AND "subject_id" = ?`,
)
.get(subject.type, subject.id) as Row | undefined;
return row ? identityFromRow(row) : null;
},
() => new LocalIdentityCredentialAdministrationUnavailableError(),
);
}
export function resolveIdentityMutation(
authority: LocalSqliteOperationAuthority,
requestedMutationId: string,
): Promise<Readonly<ResolvedLocalIdentitySubjectMutation> | null> {
normalizeIdentityAdministrationMutationId(requestedMutationId);
return authority.enqueue(
async () => {
const row = authority.client
.prepare(
`SELECT ${IDENTITY_MUTATION_SELECT},
${ADMIN_AUDIT_SELECT}
FROM "QingLong3IdentityAdministrationMutations" AS mutation
JOIN "QingLong3SecurityAuditEvents" AS audit
ON audit."event_id" = mutation."audit_event_id"
WHERE mutation."mutation_id" = ?`,
)
.get(requestedMutationId) as Row | undefined;
if (!row) return null;
return Object.freeze({
projectId: text(row, 'mutationProjectId'),
identity: identityResultFromMutationRow(row),
mutation: identityMutationFromRow(row),
audit: localSecurityAuditFromRow(row),
});
},
() => new LocalIdentityCredentialAdministrationUnavailableError(),
);
}
export function inspectAuthorizedIdentity(
authority: LocalSqliteOperationAuthority,
beforeMutation: () => void,
input: InspectAuthorizedLocalIdentityCommand,
): Promise<InspectAuthorizedLocalIdentityResult> {
const subject = normalizeIdentityAdministrationSubject(input.target);
const auth = authorization(input.authorization);
const audit = assertAllowedAudit(
input.audit,
'identity.inspect',
input.audit.eventId,
auth,
);
return authority.enqueue(
async () => {
const client = authority.client;
client.exec('BEGIN IMMEDIATE');
try {
assertAuthorizationInTransaction(authority, auth, beforeMutation);
const row = client
.prepare(
`SELECT "subject_type" AS "subjectType",
"subject_id" AS "subjectId",
"status" AS "status", "version" AS "version",
"created_at_ms" AS "createdAtMs",
"updated_at_ms" AS "updatedAtMs"
FROM "QingLong3IdentitySubjects"
WHERE "subject_type" = ? AND "subject_id" = ?`,
)
.get(subject.type, subject.id) as Row | undefined;
insertLocalSecurityAudit(client, audit);
client.exec('COMMIT');
return Object.freeze({
identity: row ? identityFromRow(row) : null,
audit,
});
} catch (error) {
if (client.isTransaction) client.exec('ROLLBACK');
if (
error instanceof
LocalIdentityCredentialAuthorizationFenceConflictError
) {
throw error;
}
if (error instanceof SecurityAuditUnavailableError) throw error;
throw new LocalIdentityCredentialAdministrationUnavailableError();
}
},
() => new LocalIdentityCredentialAdministrationUnavailableError(),
);
}
export function appendAuthorizedIdentity(
authority: LocalSqliteOperationAuthority,
beforeMutation: () => void,
input: AppendAuthorizedLocalIdentityCommand,
): Promise<AppendAuthorizedLocalIdentityResult> {
const auth = authorization(input.authorization);
const mutation = input.mutation;
normalizeIdentityAdministrationMutationId(mutation.mutationId);
const subject = normalizeIdentityAdministrationSubject(mutation.subject);
if (
!ADMINISTRABLE_SUBJECT_TYPES.has(subject.type) ||
!Number.isSafeInteger(input.expectedCurrentVersion) ||
input.expectedCurrentVersion < 0 ||
mutation.subjectVersion !== input.expectedCurrentVersion + 1 ||
mutation.expectedPreviousVersion !== input.expectedCurrentVersion ||
!sameSubject(mutation.changedBy, auth.actor) ||
(mutation.operation === 'register'
? input.expectedCurrentVersion !== 0 || mutation.status !== 'active'
: input.expectedCurrentVersion < 1 ||
mutation.status !==
(mutation.operation === 'disable' ? 'disabled' : 'active')) ||
!Number.isSafeInteger(mutation.createdAtMs) ||
mutation.createdAtMs < 0
) {
throw new TypeError('Local Identity administration command is invalid');
}
const audit = assertAllowedAudit(
input.audit,
`identity.${mutation.operation}`,
mutation.mutationId,
auth,
);
return authority.enqueue(
async () => {
const client = authority.client;
client.exec('BEGIN IMMEDIATE');
try {
assertAuthorizationInTransaction(authority, auth, beforeMutation);
const replayRow = client
.prepare(
`SELECT ${IDENTITY_MUTATION_SELECT},
${ADMIN_AUDIT_SELECT}
FROM "QingLong3IdentityAdministrationMutations" AS mutation
JOIN "QingLong3SecurityAuditEvents" AS audit
ON audit."event_id" = mutation."audit_event_id"
WHERE mutation."mutation_id" = ?`,
)
.get(mutation.mutationId) as Row | undefined;
if (replayRow) {
const existing = Object.freeze({
projectId: text(replayRow, 'mutationProjectId'),
identity: identityResultFromMutationRow(replayRow),
mutation: identityMutationFromRow(replayRow),
audit: localSecurityAuditFromRow(replayRow),
});
const expectedIdentity: IdentitySubjectRecord = {
subject,
status: mutation.status,
version: mutation.subjectVersion,
createdAtMs:
mutation.operation === 'register'
? mutation.createdAtMs
: existing.identity.createdAtMs,
updatedAtMs: mutation.createdAtMs,
};
if (
!sameIdentitySemantic(existing, {
projectId: auth.projectId,
identity: expectedIdentity,
mutation,
audit,
})
) {
throw new IdentityAdministrationMutationConflictError();
}
client.exec('COMMIT');
return Object.freeze({
status: 'existing' as const,
identity: existing.identity,
mutation: existing.mutation,
audit: existing.audit,
});
}
const currentRow = client
.prepare(
`SELECT "subject_type" AS "subjectType",
"subject_id" AS "subjectId",
"status" AS "status", "version" AS "version",
"created_at_ms" AS "createdAtMs",
"updated_at_ms" AS "updatedAtMs"
FROM "QingLong3IdentitySubjects"
WHERE "subject_type" = ? AND "subject_id" = ?`,
)
.get(subject.type, subject.id) as Row | undefined;
const current = currentRow ? identityFromRow(currentRow) : null;
if ((current?.version ?? 0) !== input.expectedCurrentVersion) {
throw new IdentityAdministrationVersionConflictError();
}
if (
mutation.operation === 'disable' &&
activeOwnerBindingExists(authority, subject)
) {
throw new LocalIdentityOwnerBindingConflictError();
}
const identity = normalizeIdentitySubjectRecord({
subject,
status: mutation.status,
version: mutation.subjectVersion,
createdAtMs: current?.createdAtMs ?? mutation.createdAtMs,
updatedAtMs: mutation.createdAtMs,
});
if (mutation.operation === 'register') {
client
.prepare(
`INSERT INTO "QingLong3IdentitySubjects" (
"subject_type", "subject_id", "status", "version",
"created_at_ms", "updated_at_ms"
) VALUES (?, ?, ?, ?, ?, ?)`,
)
.run(
subject.type,
subject.id,
identity.status,
identity.version,
identity.createdAtMs,
identity.updatedAtMs,
);
} else {
client
.prepare(
`UPDATE "QingLong3IdentitySubjects"
SET "status" = ?, "version" = ?, "updated_at_ms" = ?
WHERE "subject_type" = ? AND "subject_id" = ?
AND "version" = ?`,
)
.run(
identity.status,
identity.version,
identity.updatedAtMs,
subject.type,
subject.id,
input.expectedCurrentVersion,
);
}
insertLocalSecurityAudit(client, audit);
client
.prepare(
`INSERT INTO "QingLong3IdentityAdministrationMutations" (
"mutation_id", "project_id", "operation", "subject_type",
"subject_id", "subject_version",
"expected_previous_version", "status", "changed_by_type",
"changed_by_id", "audit_event_id",
"identity_created_at_ms", "created_at_ms"
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
mutation.mutationId,
auth.projectId,
mutation.operation,
subject.type,
subject.id,
mutation.subjectVersion,
mutation.expectedPreviousVersion,
mutation.status,
auth.actor.type,
auth.actor.id,
audit.eventId,
identity.createdAtMs,
mutation.createdAtMs,
);
client.exec('COMMIT');
return Object.freeze({
status: 'inserted' as const,
identity,
mutation: Object.freeze({ ...mutation, subject }),
audit,
});
} catch (error) {
if (client.isTransaction) client.exec('ROLLBACK');
if (
error instanceof
LocalIdentityCredentialAuthorizationFenceConflictError ||
error instanceof LocalIdentityOwnerBindingConflictError ||
error instanceof IdentityAdministrationVersionConflictError ||
error instanceof IdentityAdministrationMutationConflictError
) {
throw error;
}
if (error instanceof SecurityAuditUnavailableError) throw error;
throw new LocalIdentityCredentialAdministrationUnavailableError();
}
},
() => new LocalIdentityCredentialAdministrationUnavailableError(),
);
}
@@ -0,0 +1,137 @@
import { type IdentitySubjectRecord } from '@qinglong/runtime-core/identity-administration';
import {
type AppendAuthorizedLocalApiCredentialCommand,
type AppendAuthorizedLocalApiCredentialResult,
type AppendAuthorizedLocalCredentialDeliveryAcknowledgementCommand,
type AppendAuthorizedLocalCredentialDeliveryAcknowledgementResult,
type AppendAuthorizedLocalIdentityCommand,
type AppendAuthorizedLocalIdentityResult,
type InspectAuthorizedLocalApiCredentialCommand,
type InspectAuthorizedLocalApiCredentialResult,
type InspectAuthorizedLocalIdentityCommand,
type InspectAuthorizedLocalIdentityResult,
type LocalCredentialDeliveryAcknowledgementRecord,
type LocalIdentityCredentialAdministrationRepository,
type ResolvedLocalApiCredentialMutation,
type ResolvedLocalIdentitySubjectMutation,
} from '@qinglong/runtime-core/local-identity-credential-administration';
import type { SecuritySubject } from '@qinglong/runtime-core/security';
import { type SecurityAuditRecord } from '@qinglong/runtime-core/security-audit';
import { LocalSqliteOperationAuthority } from '../../authority/operationAuthority';
import * as commonOperations from './commonOperations';
import * as identityOperations from './identityOperations';
import * as credentialOperations from './credentialOperations';
import * as deliveryOperations from './deliveryOperations';
export class LocalSqliteIdentityCredentialAdministrationRepository
implements LocalIdentityCredentialAdministrationRepository
{
constructor(
private readonly authority: LocalSqliteOperationAuthority,
private readonly beforeMutation: () => void,
) {
if (
!(authority instanceof LocalSqliteOperationAuthority) ||
typeof beforeMutation !== 'function'
) {
throw new TypeError(
'Local SQLite Identity administration dependencies are invalid',
);
}
}
record(audit: SecurityAuditRecord): Promise<void> {
return commonOperations.record(this.authority, audit);
}
resolveAuthorityProjectId(): Promise<string | null> {
return commonOperations.resolveAuthorityProjectId(this.authority);
}
resolveIdentity(
requested: SecuritySubject,
): Promise<Readonly<IdentitySubjectRecord> | null> {
return identityOperations.resolveIdentity(this.authority, requested);
}
resolveIdentityMutation(
requestedMutationId: string,
): Promise<Readonly<ResolvedLocalIdentitySubjectMutation> | null> {
return identityOperations.resolveIdentityMutation(
this.authority,
requestedMutationId,
);
}
inspectAuthorizedIdentity(
input: InspectAuthorizedLocalIdentityCommand,
): Promise<InspectAuthorizedLocalIdentityResult> {
return identityOperations.inspectAuthorizedIdentity(
this.authority,
this.beforeMutation,
input,
);
}
appendAuthorizedIdentity(
input: AppendAuthorizedLocalIdentityCommand,
): Promise<AppendAuthorizedLocalIdentityResult> {
return identityOperations.appendAuthorizedIdentity(
this.authority,
this.beforeMutation,
input,
);
}
resolveCredentialMutation(
requestedMutationId: string,
): Promise<Readonly<ResolvedLocalApiCredentialMutation> | null> {
return credentialOperations.resolveCredentialMutation(
this.authority,
requestedMutationId,
);
}
inspectAuthorizedCredential(
input: InspectAuthorizedLocalApiCredentialCommand,
): Promise<InspectAuthorizedLocalApiCredentialResult> {
return credentialOperations.inspectAuthorizedCredential(
this.authority,
this.beforeMutation,
input,
);
}
appendAuthorizedCredential(
input: AppendAuthorizedLocalApiCredentialCommand,
): Promise<AppendAuthorizedLocalApiCredentialResult> {
return credentialOperations.appendAuthorizedCredential(
this.authority,
this.beforeMutation,
input,
);
}
resolveDeliveryAcknowledgement(
credentialMutationId: string,
): Promise<Readonly<LocalCredentialDeliveryAcknowledgementRecord> | null> {
return deliveryOperations.resolveDeliveryAcknowledgement(
this.authority,
credentialMutationId,
);
}
appendAuthorizedDeliveryAcknowledgement(
input: AppendAuthorizedLocalCredentialDeliveryAcknowledgementCommand,
): Promise<AppendAuthorizedLocalCredentialDeliveryAcknowledgementResult> {
return deliveryOperations.appendAuthorizedDeliveryAcknowledgement(
this.authority,
this.beforeMutation,
input,
);
}
}
@@ -0,0 +1,5 @@
export { LocalSqliteIdentityCredentialAdministrationRepository } from './identity-credential-administration/repository';
export {
openLocalSqliteIdentityCredentialAdministrationDatabase,
type LocalSqliteIdentityCredentialAdministrationDatabase,
} from './identity-credential-administration/database';
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,39 @@
import type { DatabaseSync } from 'node:sqlite';
import type {
AppendProjectRoleBindingCommand,
AppendProjectRoleBindingResult,
ProjectPolicyRepository,
ProjectPolicySnapshot,
} from '@qinglong/runtime-core/project-policy';
import type { SecuritySubject } from '@qinglong/runtime-core/security';
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
import { LocalSqliteSecurityAuthorityStore } from './securityAuthorityStore';
export class LocalSqliteProjectPolicyRepository
implements ProjectPolicyRepository
{
readonly #store: LocalSqliteSecurityAuthorityStore;
constructor(authority: LocalSqliteOperationAuthority | DatabaseSync) {
this.#store = new LocalSqliteSecurityAuthorityStore(
authority instanceof LocalSqliteOperationAuthority
? authority
: new LocalSqliteOperationAuthority(authority),
);
}
resolve(
projectId: string,
subject: Readonly<SecuritySubject>,
): Promise<Readonly<ProjectPolicySnapshot> | null> {
return this.#store.resolve(projectId, subject);
}
append(
command: AppendProjectRoleBindingCommand,
): Promise<AppendProjectRoleBindingResult> {
return this.#store.append(command);
}
}
@@ -0,0 +1,140 @@
import {
InvalidProjectPolicyValueError,
assertProjectPolicyProjectId,
normalizeProjectPolicySubject,
} from '@qinglong/runtime-core/project-policy';
import type {
SecurityPolicyFence,
SecuritySubject,
} from '@qinglong/runtime-core/security';
import { resolveLocalInstanceAuthorityProjectId } from '../authority/instanceAuthorityProject';
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
import type { LocalSqliteAuthenticatedUserCredentialFence } from '../administration/packageManagement';
import {
LOCAL_ROLE_BINDING_SELECT,
localRoleBindingFromRow,
} from './securityPersistence';
type Row = Record<string, unknown>;
export interface LocalSecurityAuditInstanceAuthorization {
readonly authorityProjectId: string;
readonly actor: SecuritySubject;
readonly fence: SecurityPolicyFence;
}
function exactFence(value: SecurityPolicyFence): void {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
Object.keys(value).sort().join(',') !== 'bindingVersion,projectVersion' ||
!Number.isSafeInteger(value.projectVersion) ||
value.projectVersion < 1 ||
!Number.isSafeInteger(value.bindingVersion) ||
(value.bindingVersion as number) < 1
) {
throw new InvalidProjectPolicyValueError('authorization fence is invalid');
}
}
function integer(row: Row | undefined, key: string): number {
const value = row?.[key];
if (!Number.isSafeInteger(value)) {
throw new InvalidProjectPolicyValueError(
'authority Project version is invalid',
);
}
return value as number;
}
export function normalizeLocalSecurityAuditInstanceAuthorization(
value: LocalSecurityAuditInstanceAuthorization,
): Readonly<LocalSecurityAuditInstanceAuthorization> {
try {
assertProjectPolicyProjectId(value.authorityProjectId);
const actor = normalizeProjectPolicySubject(value.actor);
exactFence(value.fence);
return Object.freeze({
authorityProjectId: value.authorityProjectId,
actor,
fence: Object.freeze({ ...value.fence }),
});
} catch (error) {
if (error instanceof InvalidProjectPolicyValueError) throw error;
throw new InvalidProjectPolicyValueError(
'Local security audit authorization is invalid',
);
}
}
export function assertLocalSecurityAuditInstanceOwnerInTransaction(
authority: LocalSqliteOperationAuthority,
input: Readonly<LocalSecurityAuditInstanceAuthorization>,
beforeOperation: () => void,
conflict: () => Error,
): void {
try {
beforeOperation();
} catch {
throw conflict();
}
const client = authority.client;
if (
resolveLocalInstanceAuthorityProjectId(client) !== input.authorityProjectId
) {
throw conflict();
}
const project = client
.prepare(
`SELECT "status" AS "status", "version" AS "version"
FROM "QingLong3Projects"
WHERE "id" = ?`,
)
.get(input.authorityProjectId) as Row | undefined;
const actorRow = client
.prepare(
`SELECT ${LOCAL_ROLE_BINDING_SELECT}
FROM "QingLong3ProjectRoleBindings"
WHERE "project_id" = ? AND "subject_type" = ?
AND "subject_id" = ?
ORDER BY "version" DESC LIMIT 1`,
)
.get(input.authorityProjectId, input.actor.type, input.actor.id) as
| Row
| undefined;
if (
!project ||
project.status !== 'active' ||
integer(project, 'version') !== input.fence.projectVersion ||
!actorRow
) {
throw conflict();
}
const binding = localRoleBindingFromRow(actorRow);
if (
binding.version !== input.fence.bindingVersion ||
binding.state !== 'active' ||
binding.role !== 'owner'
) {
throw conflict();
}
}
export function sameLocalSqliteAuthenticatedUserCredentialFence(
left: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
right: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
): boolean {
return (
left.credentialId === right.credentialId &&
left.credentialVersion === right.credentialVersion &&
left.pepperKeyId === right.pepperKeyId &&
left.materialDigest === right.materialDigest &&
left.subjectType === right.subjectType &&
left.subjectId === right.subjectId &&
left.secretDigest === right.secretDigest &&
left.notBeforeAtMs === right.notBeforeAtMs &&
left.expiresAtMs === right.expiresAtMs
);
}
@@ -0,0 +1,290 @@
import type { ApiCredentialRepository } from '@qinglong/runtime-core/api-credential';
import {
LocalSecurityAuditQueryAuthorizationFenceConflictError,
LocalSecurityAuditQueryUnavailableError,
MAX_LOCAL_SECURITY_AUDIT_QUERY_PAGE_SIZE,
type ListAuthorizedLocalSecurityAuditCommand,
type ListAuthorizedLocalSecurityAuditResult,
type LocalSecurityAuditQueryAuthorization,
type LocalSecurityAuditQueryRepository,
} from '@qinglong/runtime-core/local-security-audit-query';
import type { LocalOwnerPepperRepository } from '@qinglong/runtime-core/local-owner-pepper';
import {
MAX_EDGE_SECURITY_AUDIT_COMPACTION_BATCH_SIZE,
MAX_STANDALONE_SECURITY_AUDIT_COMPACTION_BATCH_SIZE,
type LocalSecurityAuditRetentionRepository,
} from '@qinglong/runtime-core/local-security-audit-retention';
import {
InvalidProjectPolicyValueError,
assertProjectPolicyProjectId,
type ProjectPolicyRepository,
} from '@qinglong/runtime-core/project-policy';
import {
SecurityAuditUnavailableError,
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
type SecurityAuditSink,
} from '@qinglong/runtime-core/security-audit';
import {
InvalidSecurityAuditQueryError,
normalizeSecurityAuditQuery,
} from '@qinglong/runtime-core/security-audit-query';
import { LocalSqliteApiCredentialRepository } from './apiCredentialRepository';
import {
assertLocalSqliteOptions,
assertLocalSqlitePathBoundary,
openLocalSqliteClient,
type LocalSqliteDatabaseOptions,
type LocalSqliteProfile,
} from '../storage/config';
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
import { LocalSqliteOwnerPepperRepository } from '../local-owner/ownerPepperRepository';
import {
confirmLocalSqliteAuthenticatedUserCredentialFence,
LocalSqliteAuthenticatedManagementFenceError,
type LocalSqliteAuthenticatedUserCredentialFence,
} from '../administration/packageManagement';
import {
auditLocalSqliteReadiness,
type LocalSqliteReadinessEvidence,
} from '../readiness/readiness';
import {
assertLocalSecurityAuditInstanceOwnerInTransaction,
normalizeLocalSecurityAuditInstanceAuthorization,
sameLocalSqliteAuthenticatedUserCredentialFence,
} from './securityAuditAuthority';
import { LocalSqliteSecurityAuditRetentionRepository } from './securityAuditRetention';
import {
insertLocalSecurityAudit,
LOCAL_SECURITY_AUDIT_SELECT,
localSecurityAuditFromRow,
} from './securityPersistence';
import { LocalSqliteSecurityAuthorityStore } from './securityAuthorityStore';
type Row = Record<string, unknown>;
function allowedAudit(
value: SecurityAuditRecord,
authority: Readonly<LocalSecurityAuditQueryAuthorization>,
): Readonly<SecurityAuditRecord> {
const audit = normalizeSecurityAuditRecord(value);
if (
audit.operationId !== 'security.audit.list' ||
audit.projectId !== authority.authorityProjectId ||
audit.subject?.type !== authority.actor.type ||
audit.subject.id !== authority.actor.id ||
audit.outcome !== 'allowed' ||
audit.fence?.projectVersion !== authority.fence.projectVersion ||
audit.fence.bindingVersion !== authority.fence.bindingVersion
) {
throw new InvalidProjectPolicyValueError(
'Local security audit query audit is invalid',
);
}
return audit;
}
export class LocalSqliteSecurityAuditQueryRepository
implements LocalSecurityAuditQueryRepository
{
constructor(
private readonly authority: LocalSqliteOperationAuthority,
private readonly beforeQuery: () => void,
) {
if (
!(authority instanceof LocalSqliteOperationAuthority) ||
typeof beforeQuery !== 'function'
) {
throw new TypeError(
'Local SQLite security audit query dependencies are invalid',
);
}
}
record(value: SecurityAuditRecord): Promise<void> {
return new LocalSqliteSecurityAuthorityStore(this.authority).record(value);
}
listAuthorized(
input: ListAuthorizedLocalSecurityAuditCommand,
): Promise<ListAuthorizedLocalSecurityAuditResult> {
const query = normalizeSecurityAuditQuery(input.query);
if (query.limit > MAX_LOCAL_SECURITY_AUDIT_QUERY_PAGE_SIZE) {
throw new InvalidSecurityAuditQueryError('local query limit exceeds 64');
}
const authorityInput = normalizeLocalSecurityAuditInstanceAuthorization(
input.authorization,
);
const audit = allowedAudit(input.audit, authorityInput);
return this.authority.enqueue(
async () => {
const client = this.authority.client;
client.exec('BEGIN IMMEDIATE');
try {
assertLocalSecurityAuditInstanceOwnerInTransaction(
this.authority,
authorityInput,
this.beforeQuery,
() => new LocalSecurityAuditQueryAuthorizationFenceConflictError(),
);
const subject = query.filter.subject;
const before = query.before;
const rows = client
.prepare(
`SELECT ${LOCAL_SECURITY_AUDIT_SELECT}
FROM "QingLong3SecurityAuditEvents"
WHERE (? IS NULL OR "project_id" = ?)
AND (? IS NULL OR "subject_type" = ?)
AND (? IS NULL OR "subject_id" = ?)
AND (? IS NULL OR "outcome" = ?)
AND (
? IS NULL OR "occurred_at_ms" < ?
OR ("occurred_at_ms" = ? AND "event_id" < ?)
)
ORDER BY "occurred_at_ms" DESC, "event_id" DESC
LIMIT ?`,
)
.all(
query.filter.projectId ?? null,
query.filter.projectId ?? null,
subject?.type ?? null,
subject?.type ?? null,
subject?.id ?? null,
subject?.id ?? null,
query.filter.outcome ?? null,
query.filter.outcome ?? null,
before?.occurredAtMs ?? null,
before?.occurredAtMs ?? null,
before?.occurredAtMs ?? null,
before?.eventId ?? null,
query.limit + 1,
) as Row[];
const hasMore = rows.length > query.limit;
const records = Object.freeze(
rows
.slice(0, query.limit)
.map((row) => localSecurityAuditFromRow(row)),
);
const last = records.at(-1);
insertLocalSecurityAudit(client, audit);
client.exec('COMMIT');
return Object.freeze({
records,
nextCursor:
hasMore && last
? Object.freeze({
occurredAtMs: last.occurredAtMs,
eventId: last.eventId,
})
: null,
audit,
});
} catch (error) {
if (client.isTransaction) client.exec('ROLLBACK');
if (
error instanceof
LocalSecurityAuditQueryAuthorizationFenceConflictError ||
error instanceof InvalidSecurityAuditQueryError ||
error instanceof InvalidProjectPolicyValueError ||
error instanceof SecurityAuditUnavailableError
) {
throw error;
}
throw new LocalSecurityAuditQueryUnavailableError();
}
},
() => new LocalSecurityAuditQueryUnavailableError(),
);
}
}
export interface LocalSqliteSecurityAuditQueryDatabase {
readonly profile: LocalSqliteProfile;
readonly readiness: LocalSqliteReadinessEvidence;
readonly apiCredentials: ApiCredentialRepository;
readonly ownerPepper: Pick<LocalOwnerPepperRepository, 'resolveKey'>;
readonly projectPolicy: ProjectPolicyRepository;
readonly securityAuditQuery: LocalSecurityAuditQueryRepository;
readonly securityAuditRetention: LocalSecurityAuditRetentionRepository;
readonly securityAudit: SecurityAuditSink;
activateUserCredentialFence(
fence: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
): void;
close(): Promise<void>;
}
export async function openLocalSqliteSecurityAuditQueryDatabase(
options: LocalSqliteDatabaseOptions,
): Promise<LocalSqliteSecurityAuditQueryDatabase> {
assertLocalSqliteOptions(options);
assertLocalSqlitePathBoundary(options.databasePath, false);
const client = openLocalSqliteClient(options, false);
try {
const readiness = await auditLocalSqliteReadiness(client);
const authority = new LocalSqliteOperationAuthority(client);
const securityAuthority = new LocalSqliteSecurityAuthorityStore(authority);
let activeFence:
| Readonly<LocalSqliteAuthenticatedUserCredentialFence>
| undefined;
const projectPolicy: ProjectPolicyRepository = Object.freeze({
resolve: (
...[projectId, subject]: Parameters<ProjectPolicyRepository['resolve']>
) => securityAuthority.resolve(projectId, subject),
append: (...[command]: Parameters<ProjectPolicyRepository['append']>) =>
securityAuthority.append(command),
});
const confirmFence = () => {
if (!activeFence) {
throw new LocalSqliteAuthenticatedManagementFenceError();
}
confirmLocalSqliteAuthenticatedUserCredentialFence(
authority,
activeFence,
);
};
const securityAuditQuery = new LocalSqliteSecurityAuditQueryRepository(
authority,
confirmFence,
);
const securityAuditRetention =
new LocalSqliteSecurityAuditRetentionRepository(
authority,
confirmFence,
options.profile === 'edge'
? MAX_EDGE_SECURITY_AUDIT_COMPACTION_BATCH_SIZE
: MAX_STANDALONE_SECURITY_AUDIT_COMPACTION_BATCH_SIZE,
);
let closePromise: Promise<void> | undefined;
return Object.freeze({
profile: options.profile,
readiness,
apiCredentials: new LocalSqliteApiCredentialRepository(authority),
ownerPepper: new LocalSqliteOwnerPepperRepository(authority),
projectPolicy,
securityAuditQuery,
securityAuditRetention,
securityAudit: securityAuthority,
activateUserCredentialFence(
fence: Readonly<LocalSqliteAuthenticatedUserCredentialFence>,
) {
confirmLocalSqliteAuthenticatedUserCredentialFence(authority, fence);
if (
activeFence &&
!sameLocalSqliteAuthenticatedUserCredentialFence(activeFence, fence)
) {
throw new LocalSqliteAuthenticatedManagementFenceError();
}
activeFence = Object.freeze({ ...fence });
},
close() {
if (closePromise) return closePromise;
closePromise = authority.close();
return closePromise;
},
});
} catch (error) {
if (client.isOpen) client.close();
throw error;
}
}
@@ -0,0 +1,498 @@
import {
InvalidLocalSecurityAuditRetentionValueError,
LocalSecurityAuditCompactionMutationConflictError,
LocalSecurityAuditRetentionAuthorizationFenceConflictError,
LocalSecurityAuditRetentionUnavailableError,
MAX_LOCAL_SECURITY_AUDIT_RETENTION_MS,
MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS,
localSecurityAuditCompactionPayload,
type CompactAuthorizedLocalSecurityAuditCommand,
type CompactAuthorizedLocalSecurityAuditResult,
type LocalSecurityAuditCompactionRecord,
type LocalSecurityAuditRetentionRepository,
} from '@qinglong/runtime-core/local-security-audit-retention';
import { InvalidProjectPolicyValueError } from '@qinglong/runtime-core/project-policy';
import {
SecurityAuditUnavailableError,
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
} from '@qinglong/runtime-core/security-audit';
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
import {
assertLocalSecurityAuditInstanceOwnerInTransaction,
normalizeLocalSecurityAuditInstanceAuthorization,
} from './securityAuditAuthority';
import {
insertLocalSecurityAudit,
LOCAL_SECURITY_AUDIT_JOIN_SELECT,
LOCAL_SECURITY_AUDIT_SELECT,
localSecurityAuditFromRow,
sameSecurityAuditSemantic,
} from './securityPersistence';
import { LocalSqliteSecurityAuthorityStore } from './securityAuthorityStore';
type Row = Record<string, unknown>;
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 COMPACTION_SELECT = `
compaction."mutation_id" AS "compactionMutationId",
compaction."request_id" AS "compactionRequestId",
compaction."authority_project_id" AS "compactionAuthorityProjectId",
compaction."retention_ms" AS "compactionRetentionMs",
compaction."eligible_before_ms" AS "compactionEligibleBeforeMs",
compaction."batch_limit" AS "compactionBatchLimit",
compaction."deleted_count" AS "compactionDeletedCount",
compaction."deleted_payload_bytes" AS "compactionDeletedPayloadBytes",
compaction."first_occurred_at_ms" AS "compactionFirstOccurredAtMs",
compaction."first_event_id" AS "compactionFirstEventId",
compaction."last_occurred_at_ms" AS "compactionLastOccurredAtMs",
compaction."last_event_id" AS "compactionLastEventId",
compaction."records_digest" AS "compactionRecordsDigest",
compaction."created_at_ms" AS "compactionCreatedAtMs"
`;
function integer(row: Row, key: string): number {
const value = row[key];
if (!Number.isSafeInteger(value) || (value as number) < 0) {
throw new LocalSecurityAuditRetentionUnavailableError();
}
return value as number;
}
function text(row: Row, key: string): string {
const value = row[key];
if (typeof value !== 'string') {
throw new LocalSecurityAuditRetentionUnavailableError();
}
return value;
}
function optionalInteger(row: Row, key: string): number | null {
const value = row[key];
if (value === null) return null;
return integer(row, key);
}
function optionalText(row: Row, key: string): string | null {
const value = row[key];
if (value === null) return null;
return text(row, key);
}
function compactionFromRow(
row: Row,
): Readonly<LocalSecurityAuditCompactionRecord> {
const deletedCount = integer(row, 'compactionDeletedCount');
const firstOccurredAtMs = optionalInteger(row, 'compactionFirstOccurredAtMs');
const firstEventId = optionalText(row, 'compactionFirstEventId');
const lastOccurredAtMs = optionalInteger(row, 'compactionLastOccurredAtMs');
const lastEventId = optionalText(row, 'compactionLastEventId');
if (
(deletedCount === 0 &&
(firstOccurredAtMs !== null ||
firstEventId !== null ||
lastOccurredAtMs !== null ||
lastEventId !== null)) ||
(deletedCount > 0 &&
(firstOccurredAtMs === null ||
firstEventId === null ||
lastOccurredAtMs === null ||
lastEventId === null))
) {
throw new LocalSecurityAuditRetentionUnavailableError();
}
return Object.freeze({
mutationId: text(row, 'compactionMutationId'),
requestId: text(row, 'compactionRequestId'),
authorityProjectId: text(row, 'compactionAuthorityProjectId'),
retentionMs: integer(row, 'compactionRetentionMs'),
eligibleBeforeMs: integer(row, 'compactionEligibleBeforeMs'),
batchLimit: integer(row, 'compactionBatchLimit'),
deletedCount,
deletedPayloadBytes: integer(row, 'compactionDeletedPayloadBytes'),
first:
firstOccurredAtMs === null
? null
: Object.freeze({
occurredAtMs: firstOccurredAtMs,
eventId: firstEventId!,
}),
last:
lastOccurredAtMs === null
? null
: Object.freeze({
occurredAtMs: lastOccurredAtMs,
eventId: lastEventId!,
}),
recordsDigest: text(row, 'compactionRecordsDigest'),
createdAtMs: integer(row, 'compactionCreatedAtMs'),
});
}
function command(
input: CompactAuthorizedLocalSecurityAuditCommand,
maxBatchSize: number,
): Readonly<CompactAuthorizedLocalSecurityAuditCommand> {
if (
!input ||
typeof input !== 'object' ||
Array.isArray(input) ||
Object.keys(input).sort().join(',') !==
[
'audit',
'authorization',
'eligibleBeforeMs',
'limit',
'mutationId',
'requestId',
'retentionMs',
]
.sort()
.join(',') ||
!UUID_V4_PATTERN.test(input.mutationId) ||
!REQUEST_ID_PATTERN.test(input.requestId) ||
!Number.isSafeInteger(input.retentionMs) ||
input.retentionMs < MIN_LOCAL_SECURITY_AUDIT_RETENTION_MS ||
input.retentionMs > MAX_LOCAL_SECURITY_AUDIT_RETENTION_MS ||
!Number.isSafeInteger(input.eligibleBeforeMs) ||
input.eligibleBeforeMs < 0 ||
!Number.isSafeInteger(input.limit) ||
input.limit < 1 ||
input.limit > maxBatchSize
) {
throw new InvalidLocalSecurityAuditRetentionValueError(
'compaction command shape is invalid',
);
}
const authorization = normalizeLocalSecurityAuditInstanceAuthorization(
input.authorization,
);
const audit = normalizeSecurityAuditRecord(input.audit);
if (
audit.eventId !== input.mutationId ||
audit.requestId !== input.requestId ||
audit.operationId !== 'security.audit.compact' ||
audit.projectId !== authorization.authorityProjectId ||
audit.subject?.type !== authorization.actor.type ||
audit.subject.id !== authorization.actor.id ||
audit.outcome !== 'allowed' ||
audit.reasons.length !== 1 ||
audit.reasons[0] !== 'instance_authority_security_audit_compaction' ||
audit.fence?.projectVersion !== authorization.fence.projectVersion ||
audit.fence.bindingVersion !== authorization.fence.bindingVersion ||
input.eligibleBeforeMs + input.retentionMs > audit.occurredAtMs
) {
throw new InvalidLocalSecurityAuditRetentionValueError(
'compaction audit or retention fence is invalid',
);
}
return Object.freeze({
...input,
authorization,
audit,
});
}
function sameCommand(
record: Readonly<LocalSecurityAuditCompactionRecord>,
audit: Readonly<SecurityAuditRecord>,
input: Readonly<CompactAuthorizedLocalSecurityAuditCommand>,
): boolean {
return (
record.mutationId === input.mutationId &&
record.requestId === input.requestId &&
record.authorityProjectId === input.authorization.authorityProjectId &&
record.retentionMs === input.retentionMs &&
record.eligibleBeforeMs === input.eligibleBeforeMs &&
record.batchLimit === input.limit &&
sameSecurityAuditSemantic(audit, input.audit)
);
}
export class LocalSqliteSecurityAuditRetentionRepository
implements LocalSecurityAuditRetentionRepository
{
constructor(
private readonly authority: LocalSqliteOperationAuthority,
private readonly beforeCompaction: () => void,
private readonly maxBatchSize: number,
) {
if (
!(authority instanceof LocalSqliteOperationAuthority) ||
typeof beforeCompaction !== 'function' ||
!Number.isSafeInteger(maxBatchSize) ||
maxBatchSize < 1 ||
maxBatchSize > 512
) {
throw new TypeError(
'Local SQLite security audit retention dependencies are invalid',
);
}
}
record(value: SecurityAuditRecord): Promise<void> {
return new LocalSqliteSecurityAuthorityStore(this.authority).record(value);
}
resolveCompaction(
mutationId: string,
): Promise<Readonly<LocalSecurityAuditCompactionRecord> | null> {
if (!UUID_V4_PATTERN.test(mutationId)) {
throw new InvalidLocalSecurityAuditRetentionValueError(
'mutation identity is invalid',
);
}
return this.authority.enqueue(
async () => {
try {
const row = this.authority.client
.prepare(
`SELECT ${COMPACTION_SELECT}
FROM "QingLong3SecurityAuditCompactions" AS compaction
WHERE compaction."mutation_id" = ?`,
)
.get(mutationId) as Row | undefined;
return row ? compactionFromRow(row) : null;
} catch (error) {
if (error instanceof InvalidLocalSecurityAuditRetentionValueError) {
throw error;
}
throw new LocalSecurityAuditRetentionUnavailableError();
}
},
() => new LocalSecurityAuditRetentionUnavailableError(),
);
}
compactAuthorized(
input: CompactAuthorizedLocalSecurityAuditCommand,
): Promise<CompactAuthorizedLocalSecurityAuditResult> {
const value = command(input, this.maxBatchSize);
return this.authority.enqueue(
async () => {
const client = this.authority.client;
client.exec('BEGIN IMMEDIATE');
try {
assertLocalSecurityAuditInstanceOwnerInTransaction(
this.authority,
value.authorization,
this.beforeCompaction,
() =>
new LocalSecurityAuditRetentionAuthorizationFenceConflictError(),
);
const replay = client
.prepare(
`SELECT ${COMPACTION_SELECT},
${LOCAL_SECURITY_AUDIT_JOIN_SELECT}
FROM "QingLong3SecurityAuditCompactions" AS compaction
JOIN "QingLong3SecurityAuditEvents" AS audit
ON audit."event_id" = compaction."audit_event_id"
WHERE compaction."mutation_id" = ?`,
)
.get(value.mutationId) as Row | undefined;
if (replay) {
const record = compactionFromRow(replay);
const audit = localSecurityAuditFromRow(replay);
if (!sameCommand(record, audit, value)) {
throw new LocalSecurityAuditCompactionMutationConflictError();
}
client.exec('COMMIT');
return Object.freeze({
status: 'existing' as const,
record,
audit,
});
}
const rows = client
.prepare(
`SELECT ${LOCAL_SECURITY_AUDIT_SELECT}
FROM "QingLong3SecurityAuditEvents" AS candidate
WHERE candidate."occurred_at_ms" < ?
AND (
candidate."outcome" <> 'allowed'
OR candidate."operation_id" IN (
'identity.inspect',
'credential.inspect',
'policy.project.inspect',
'policy.project.list',
'policy.role_binding.inspect',
'policy.role_binding.list',
'security.audit.list'
)
)
AND NOT EXISTS (
SELECT 1
FROM "QingLong3ApiCredentialAdministrationMutations" AS ref
WHERE ref."audit_event_id" = candidate."event_id"
)
AND NOT EXISTS (
SELECT 1
FROM "QingLong3ApiCredentialDeliveryAcknowledgements" AS ref
WHERE ref."audit_event_id" = candidate."event_id"
)
AND NOT EXISTS (
SELECT 1
FROM "QingLong3IdentityAdministrationMutations" AS ref
WHERE ref."audit_event_id" = candidate."event_id"
)
AND NOT EXISTS (
SELECT 1 FROM "QingLong3LegacyAdoptions" AS ref
WHERE ref."audit_event_id" = candidate."event_id"
)
AND NOT EXISTS (
SELECT 1 FROM "QingLong3LocalIdentityProvisionings" AS ref
WHERE ref."audit_event_id" = candidate."event_id"
)
AND NOT EXISTS (
SELECT 1
FROM "QingLong3LocalOwnerBootstrapChallenges" AS ref
WHERE ref."issue_audit_event_id" = candidate."event_id"
OR ref."claim_audit_event_id" = candidate."event_id"
)
AND NOT EXISTS (
SELECT 1
FROM "QingLong3LocalOwnerCredentialRecoveries" AS ref
WHERE ref."issue_audit_event_id" = candidate."event_id"
OR ref."complete_audit_event_id" = candidate."event_id"
)
AND NOT EXISTS (
SELECT 1
FROM "QingLong3LocalOwnerDeliveryAcknowledgementGc" AS ref
WHERE ref."audit_event_id" = candidate."event_id"
)
AND NOT EXISTS (
SELECT 1
FROM "QingLong3LocalOwnerPepperMaterialGc" AS ref
WHERE ref."prepare_audit_event_id" = candidate."event_id"
OR ref."complete_audit_event_id" = candidate."event_id"
)
AND NOT EXISTS (
SELECT 1
FROM "QingLong3PluginPackageAdmissionReceipts" AS ref
WHERE ref."audit_event_id" = candidate."event_id"
)
AND NOT EXISTS (
SELECT 1
FROM "QingLong3ProjectAdministrationMutations" AS ref
WHERE ref."audit_event_id" = candidate."event_id"
)
AND NOT EXISTS (
SELECT 1 FROM "ToolExecutionAuditReceipts" AS ref
WHERE ref."event_id" = candidate."event_id"
)
AND NOT EXISTS (
SELECT 1 FROM "ToolExecutionStartBarriers" AS ref
WHERE ref."audit_event_id" = candidate."event_id"
)
AND NOT EXISTS (
SELECT 1
FROM "QingLong3SecurityAuditCompactions" AS ref
WHERE ref."audit_event_id" = candidate."event_id"
)
ORDER BY candidate."occurred_at_ms" ASC,
candidate."event_id" ASC
LIMIT ?`,
)
.all(value.eligibleBeforeMs, value.limit) as Row[];
const records = Object.freeze(
rows.map((row) => localSecurityAuditFromRow(row)),
);
const payload = localSecurityAuditCompactionPayload(records);
const firstRecord = records.at(0);
const lastRecord = records.at(-1);
const record: Readonly<LocalSecurityAuditCompactionRecord> =
Object.freeze({
mutationId: value.mutationId,
requestId: value.requestId,
authorityProjectId: value.authorization.authorityProjectId,
retentionMs: value.retentionMs,
eligibleBeforeMs: value.eligibleBeforeMs,
batchLimit: value.limit,
deletedCount: records.length,
deletedPayloadBytes: payload.payloadBytes,
first: firstRecord
? Object.freeze({
occurredAtMs: firstRecord.occurredAtMs,
eventId: firstRecord.eventId,
})
: null,
last: lastRecord
? Object.freeze({
occurredAtMs: lastRecord.occurredAtMs,
eventId: lastRecord.eventId,
})
: null,
recordsDigest: payload.recordsDigest,
createdAtMs: value.audit.occurredAtMs,
});
insertLocalSecurityAudit(client, value.audit);
client
.prepare(
`INSERT INTO "QingLong3SecurityAuditCompactions" (
"mutation_id", "request_id", "authority_project_id",
"retention_ms", "eligible_before_ms", "batch_limit",
"deleted_count", "deleted_payload_bytes",
"first_occurred_at_ms", "first_event_id",
"last_occurred_at_ms", "last_event_id",
"records_digest", "audit_event_id", "created_at_ms"
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
record.mutationId,
record.requestId,
record.authorityProjectId,
record.retentionMs,
record.eligibleBeforeMs,
record.batchLimit,
record.deletedCount,
record.deletedPayloadBytes,
record.first?.occurredAtMs ?? null,
record.first?.eventId ?? null,
record.last?.occurredAtMs ?? null,
record.last?.eventId ?? null,
record.recordsDigest,
value.audit.eventId,
record.createdAtMs,
);
if (records.length > 0) {
const placeholders = records.map(() => '?').join(',');
const deleted = client
.prepare(
`DELETE FROM "QingLong3SecurityAuditEvents"
WHERE "event_id" IN (${placeholders})`,
)
.run(...records.map((candidate) => candidate.eventId));
if (deleted.changes !== records.length) {
throw new LocalSecurityAuditRetentionUnavailableError();
}
}
client.exec('COMMIT');
return Object.freeze({
status: 'inserted' as const,
record,
audit: value.audit,
});
} catch (error) {
if (client.isTransaction) client.exec('ROLLBACK');
if (
error instanceof InvalidLocalSecurityAuditRetentionValueError ||
error instanceof
LocalSecurityAuditRetentionAuthorizationFenceConflictError ||
error instanceof
LocalSecurityAuditCompactionMutationConflictError ||
error instanceof SecurityAuditUnavailableError
) {
throw error;
}
throw new LocalSecurityAuditRetentionUnavailableError();
}
},
() => new LocalSecurityAuditRetentionUnavailableError(),
);
}
}
@@ -0,0 +1,712 @@
import type { DatabaseSync } from 'node:sqlite';
import {
LocalSecretMutationConflictError,
LocalSecretVersionConflictError,
MAX_LOCAL_SECRET_BATCH_SIZE,
assertLocalSecretExpectedVersion,
assertLocalSecretMutationId,
assertLocalSecretName,
assertLocalSecretProjectId,
createLocalSecretRef,
normalizeLocalSecretEnvelope,
type AppendLocalSecretEnvelopeCommand,
type AppendLocalSecretEnvelopeResult,
type LocalSecretEnvelope,
type LocalSecretEnvelopeRepository,
type LocalSecretReference,
} from '@qinglong/runtime-core/local-secret';
import {
LocalSecretAuthorizationFenceConflictError,
type AppendAuthorizedLocalSecretEnvelopeCommand,
type AppendAuthorizedLocalSecretEnvelopeResult,
type LocalSecretAdministrationMutation,
type LocalSecretAdministrationRepository,
} from '@qinglong/runtime-core/local-secret-administration';
import {
ProjectPolicyUnavailableError,
ProjectRoleBindingMutationConflictError,
ProjectRoleBindingVersionConflictError,
assertExpectedProjectRoleBindingVersion,
assertProjectPolicyProjectId,
normalizeProjectPolicySnapshot,
normalizeProjectPolicySubject,
normalizeProjectRoleBinding,
type AppendProjectRoleBindingCommand,
type AppendProjectRoleBindingResult,
type ProjectPolicyRepository,
type ProjectPolicySnapshot,
} from '@qinglong/runtime-core/project-policy';
import {
SecurityAuditUnavailableError,
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
type SecurityAuditSink,
} from '@qinglong/runtime-core/security-audit';
import {
RunRepositoryBusyError,
RunRepositoryConstraintError,
RunRepositoryError,
RunRepositoryOperationError,
} from '@qinglong/runtime-core/run-repository';
import { LocalSqliteOperationAuthority } from '../authority/operationAuthority';
import {
insertLocalSecurityAudit,
LOCAL_PROJECT_SELECT,
LOCAL_ROLE_BINDING_SELECT,
LOCAL_SECRET_JOIN_SELECT,
LOCAL_SECRET_SELECT,
LOCAL_SECURITY_AUDIT_JOIN_SELECT,
LOCAL_SECURITY_AUDIT_SELECT,
localProjectFromRow,
localRoleBindingFromRow,
localSecretEnvelopeFromRow,
localSecurityAuditFromRow,
mapSqliteError,
queryRows,
requiredInteger,
requiredString,
sameSecurityAuditSemantic,
singleRow,
type QueryRow,
} from './securityPersistence';
export interface LocalSqliteSecurityAuthorityStoreOptions {
readonly beforeAuthorizedLocalSecretMutation?: () => void;
}
/**
* Package-private owner for Project Policy, Security Audit and Local Secret
* persistence. All operations reuse one LocalSqliteOperationAuthority; this
* class neither creates nor closes the underlying connection.
*/
export class LocalSqliteSecurityAuthorityStore
implements
ProjectPolicyRepository,
SecurityAuditSink,
LocalSecretEnvelopeRepository,
LocalSecretAdministrationRepository
{
private readonly client: DatabaseSync;
private readonly beforeAuthorizedLocalSecretMutation:
| (() => void)
| undefined;
constructor(
private readonly authority: LocalSqliteOperationAuthority,
options: LocalSqliteSecurityAuthorityStoreOptions = {},
) {
if (
!(authority instanceof LocalSqliteOperationAuthority) ||
!options ||
typeof options !== 'object' ||
Array.isArray(options) ||
Object.keys(options).some(
(key) => key !== 'beforeAuthorizedLocalSecretMutation',
) ||
(options.beforeAuthorizedLocalSecretMutation !== undefined &&
typeof options.beforeAuthorizedLocalSecretMutation !== 'function')
) {
throw new TypeError(
'Local SQLite Security authority dependencies are invalid',
);
}
this.client = authority.client;
this.beforeAuthorizedLocalSecretMutation =
options.beforeAuthorizedLocalSecretMutation;
}
private enqueue<T>(work: () => Promise<T>): Promise<T> {
return this.authority.enqueue(work, (reason) =>
reason === 'busy'
? new RunRepositoryBusyError()
: new RunRepositoryOperationError(
new Error('Local SQLite Run repository is closed'),
),
);
}
resolve(
projectId: string,
subjectValue: Parameters<ProjectPolicyRepository['resolve']>[1],
): Promise<Readonly<ProjectPolicySnapshot> | null> {
assertProjectPolicyProjectId(projectId);
const subject = normalizeProjectPolicySubject(subjectValue);
return this.enqueue(async () => {
try {
const projectRow = singleRow(
queryRows(
this.client,
`SELECT ${LOCAL_PROJECT_SELECT}
FROM "QingLong3Projects" WHERE "id" = ? LIMIT 2`,
[projectId],
),
);
if (!projectRow) return null;
const bindingRow = singleRow(
queryRows(
this.client,
`SELECT ${LOCAL_ROLE_BINDING_SELECT}
FROM "QingLong3ProjectRoleBindings"
WHERE "project_id" = ? AND "subject_type" = ?
AND "subject_id" = ?
ORDER BY "version" DESC LIMIT 1`,
[projectId, subject.type, subject.id],
),
);
return normalizeProjectPolicySnapshot({
project: localProjectFromRow(projectRow),
...(bindingRow
? { binding: localRoleBindingFromRow(bindingRow) }
: {}),
});
} catch {
throw new ProjectPolicyUnavailableError();
}
});
}
append(
command: AppendProjectRoleBindingCommand,
): Promise<AppendProjectRoleBindingResult> {
assertExpectedProjectRoleBindingVersion(command.expectedCurrentVersion);
const binding = normalizeProjectRoleBinding(command.binding);
if (binding.version !== command.expectedCurrentVersion + 1) {
return Promise.reject(new ProjectRoleBindingVersionConflictError());
}
return this.enqueue(async () => {
let began = false;
try {
this.client.exec('BEGIN IMMEDIATE');
began = true;
const replayRow = singleRow(
queryRows(
this.client,
`SELECT ${LOCAL_ROLE_BINDING_SELECT}
FROM "QingLong3ProjectRoleBindings"
WHERE "project_id" = ? AND "subject_type" = ?
AND "subject_id" = ? AND "mutation_id" = ?
LIMIT 2`,
[
binding.projectId,
binding.subject.type,
binding.subject.id,
binding.mutationId,
],
),
);
if (replayRow) {
const existing = localRoleBindingFromRow(replayRow);
if (JSON.stringify(existing) !== JSON.stringify(binding)) {
throw new ProjectRoleBindingMutationConflictError();
}
this.client.exec('COMMIT');
began = false;
return Object.freeze({
status: 'existing' as const,
binding: existing,
});
}
const current = this.client
.prepare(
`SELECT MAX("version") AS "version"
FROM "QingLong3ProjectRoleBindings"
WHERE "project_id" = ? AND "subject_type" = ?
AND "subject_id" = ?`,
)
.get(binding.projectId, binding.subject.type, binding.subject.id) as
| QueryRow
| undefined;
const currentVersion =
current?.version === null || current?.version === undefined
? 0
: requiredInteger(current, 'version');
if (currentVersion !== command.expectedCurrentVersion) {
throw new ProjectRoleBindingVersionConflictError();
}
const project = this.client
.prepare(`SELECT "id" FROM "QingLong3Projects" WHERE "id" = ?`)
.get(binding.projectId);
if (!project) throw new ProjectPolicyUnavailableError();
this.client
.prepare(
`INSERT INTO "QingLong3ProjectRoleBindings" (
"project_id", "subject_type", "subject_id", "version",
"state", "role", "mutation_id", "changed_by_type",
"changed_by_id", "created_at_ms"
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
binding.projectId,
binding.subject.type,
binding.subject.id,
binding.version,
binding.state,
binding.role ?? null,
binding.mutationId,
binding.changedBy.type,
binding.changedBy.id,
binding.createdAtMs,
);
this.client.exec('COMMIT');
began = false;
return Object.freeze({
status: 'inserted' as const,
binding,
});
} catch (error) {
if (began && this.client.isTransaction) {
try {
this.client.exec('ROLLBACK');
} catch {
// Preserve the original failure.
}
}
if (
error instanceof ProjectRoleBindingVersionConflictError ||
error instanceof ProjectRoleBindingMutationConflictError ||
error instanceof ProjectPolicyUnavailableError
) {
throw error;
}
throw new ProjectPolicyUnavailableError();
}
});
}
record(value: SecurityAuditRecord): Promise<void> {
const audit = normalizeSecurityAuditRecord(value);
return this.enqueue(async () => {
try {
insertLocalSecurityAudit(this.client, audit);
} catch {
try {
const row = singleRow(
queryRows(
this.client,
`SELECT ${LOCAL_SECURITY_AUDIT_SELECT}
FROM "QingLong3SecurityAuditEvents"
WHERE "event_id" = ? LIMIT 2`,
[audit.eventId],
),
);
if (
row &&
sameSecurityAuditSemantic(localSecurityAuditFromRow(row), audit)
) {
return;
}
} catch {
// Collapse storage and corruption failures to one low-sensitive error.
}
throw new SecurityAuditUnavailableError();
}
});
}
resolveLocalSecretAdministrationMutation(
projectId: string,
name: string,
mutationId: string,
): Promise<Readonly<LocalSecretAdministrationMutation> | null> {
assertLocalSecretProjectId(projectId);
assertLocalSecretName(name);
assertLocalSecretMutationId(mutationId);
return this.enqueue(async () => {
const row = singleRow(
queryRows(
this.client,
`SELECT ${LOCAL_SECRET_JOIN_SELECT}, ${LOCAL_SECURITY_AUDIT_JOIN_SELECT}
FROM "QingLong3LocalSecretEnvelopes" AS secret
JOIN "QingLong3SecurityAuditEvents" AS audit
ON audit."event_id" = secret."mutation_id"
WHERE secret."project_id" = ? AND secret."secret_name" = ?
AND secret."mutation_id" = ?
LIMIT 2`,
[projectId, name, mutationId],
),
);
if (!row) return null;
return Object.freeze({
envelope: localSecretEnvelopeFromRow(row),
audit: localSecurityAuditFromRow(row),
});
});
}
appendAuthorizedLocalSecretEnvelope(
command: AppendAuthorizedLocalSecretEnvelopeCommand,
): Promise<AppendAuthorizedLocalSecretEnvelopeResult> {
assertLocalSecretExpectedVersion(command.expectedCurrentVersion);
const envelope = normalizeLocalSecretEnvelope(command.envelope);
const subject = normalizeProjectPolicySubject(command.subject);
const audit = normalizeSecurityAuditRecord(command.audit);
const fence = command.fence;
if (
!fence ||
typeof fence !== 'object' ||
Array.isArray(fence) ||
Object.keys(fence).sort().join(',') !== 'bindingVersion,projectVersion' ||
!Number.isSafeInteger(fence.projectVersion) ||
fence.projectVersion < 1 ||
!Number.isSafeInteger(fence.bindingVersion) ||
(fence.bindingVersion as number) < 1 ||
envelope.version !== command.expectedCurrentVersion + 1 ||
audit.eventId !== envelope.mutationId ||
audit.projectId !== envelope.projectId ||
audit.subject?.type !== subject.type ||
audit.subject?.id !== subject.id ||
audit.outcome !== 'allowed' ||
audit.fence?.projectVersion !== fence.projectVersion ||
audit.fence?.bindingVersion !== fence.bindingVersion ||
(audit.operationId !== 'secret.create' &&
audit.operationId !== 'secret.rotate')
) {
return Promise.reject(new LocalSecretMutationConflictError());
}
return this.enqueue(async () => {
let began = false;
try {
this.client.exec('BEGIN IMMEDIATE');
began = true;
if (this.beforeAuthorizedLocalSecretMutation) {
try {
this.beforeAuthorizedLocalSecretMutation();
} catch {
throw new LocalSecretAuthorizationFenceConflictError();
}
}
const replay = singleRow(
queryRows(
this.client,
`SELECT ${LOCAL_SECRET_JOIN_SELECT}, ${LOCAL_SECURITY_AUDIT_JOIN_SELECT}
FROM "QingLong3LocalSecretEnvelopes" AS secret
JOIN "QingLong3SecurityAuditEvents" AS audit
ON audit."event_id" = secret."mutation_id"
WHERE secret."project_id" = ? AND secret."secret_name" = ?
AND secret."mutation_id" = ?
LIMIT 2`,
[envelope.projectId, envelope.name, envelope.mutationId],
),
);
if (replay) {
const existingEnvelope = localSecretEnvelopeFromRow(replay);
const existingAudit = localSecurityAuditFromRow(replay);
if (!sameSecurityAuditSemantic(existingAudit, audit)) {
throw new LocalSecretMutationConflictError();
}
this.client.exec('COMMIT');
began = false;
return Object.freeze({
status: 'existing' as const,
envelope: existingEnvelope,
audit: existingAudit,
});
}
const occupiedAudit = this.client
.prepare(
`SELECT "event_id" FROM "QingLong3SecurityAuditEvents"
WHERE "event_id" = ? LIMIT 1`,
)
.get(audit.eventId);
if (occupiedAudit) throw new LocalSecretMutationConflictError();
const projectRow = this.client
.prepare(
`SELECT "version", "status" FROM "QingLong3Projects"
WHERE "id" = ? LIMIT 1`,
)
.get(envelope.projectId) as QueryRow | undefined;
if (
!projectRow ||
requiredInteger(projectRow, 'version') !== fence.projectVersion ||
requiredString(projectRow, 'status') !== 'active'
) {
throw new LocalSecretAuthorizationFenceConflictError();
}
const bindingRow = this.client
.prepare(
`SELECT "version", "state", "role"
FROM "QingLong3ProjectRoleBindings"
WHERE "project_id" = ? AND "subject_type" = ?
AND "subject_id" = ?
ORDER BY "version" DESC LIMIT 1`,
)
.get(envelope.projectId, subject.type, subject.id) as
| QueryRow
| undefined;
if (
!bindingRow ||
requiredInteger(bindingRow, 'version') !== fence.bindingVersion ||
requiredString(bindingRow, 'state') !== 'active' ||
!['owner', 'admin'].includes(requiredString(bindingRow, 'role'))
) {
throw new LocalSecretAuthorizationFenceConflictError();
}
const current = this.client
.prepare(
`SELECT MAX("version") AS "version"
FROM "QingLong3LocalSecretEnvelopes"
WHERE "project_id" = ? AND "secret_name" = ?`,
)
.get(envelope.projectId, envelope.name) as QueryRow | undefined;
const currentVersion =
current?.version === null || current?.version === undefined
? 0
: requiredInteger(current, 'version');
if (currentVersion !== command.expectedCurrentVersion) {
throw new LocalSecretVersionConflictError();
}
const nonce = Buffer.from(envelope.nonce, 'base64url');
const ciphertext = Buffer.from(envelope.ciphertext, 'base64url');
const authTag = Buffer.from(envelope.authTag, 'base64url');
try {
this.client
.prepare(
`INSERT INTO "QingLong3LocalSecretEnvelopes" (
"project_id", "secret_name", "version", "mutation_id",
"key_id", "algorithm", "nonce", "ciphertext", "auth_tag",
"created_at_ms"
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
envelope.projectId,
envelope.name,
envelope.version,
envelope.mutationId,
envelope.keyId,
envelope.algorithm,
nonce,
ciphertext,
authTag,
envelope.createdAtMs,
);
} finally {
nonce.fill(0);
ciphertext.fill(0);
authTag.fill(0);
}
insertLocalSecurityAudit(this.client, audit);
this.client.exec('COMMIT');
began = false;
return Object.freeze({
status: 'inserted' as const,
envelope,
audit,
});
} catch (error) {
if (began && this.client.isTransaction) {
try {
this.client.exec('ROLLBACK');
} catch {
// Preserve the original failure.
}
}
if (
error instanceof LocalSecretAuthorizationFenceConflictError ||
error instanceof LocalSecretVersionConflictError ||
error instanceof LocalSecretMutationConflictError
) {
throw error;
}
if (error instanceof RunRepositoryError) throw error;
throw mapSqliteError(error);
}
});
}
appendLocalSecretEnvelope(
command: AppendLocalSecretEnvelopeCommand,
): Promise<AppendLocalSecretEnvelopeResult> {
assertLocalSecretExpectedVersion(command.expectedCurrentVersion);
const envelope = normalizeLocalSecretEnvelope(command.envelope);
if (envelope.version !== command.expectedCurrentVersion + 1) {
return Promise.reject(new LocalSecretVersionConflictError());
}
return this.enqueue(async () => {
let began = false;
try {
this.client.exec('BEGIN IMMEDIATE');
began = true;
const replay = singleRow(
queryRows(
this.client,
`SELECT ${LOCAL_SECRET_SELECT}
FROM "QingLong3LocalSecretEnvelopes"
WHERE "project_id" = ? AND "secret_name" = ?
AND "mutation_id" = ?
LIMIT 2`,
[envelope.projectId, envelope.name, envelope.mutationId],
),
);
if (replay) {
const existing = localSecretEnvelopeFromRow(replay);
this.client.exec('COMMIT');
began = false;
return Object.freeze({
status: 'existing' as const,
envelope: existing,
});
}
const current = this.client
.prepare(
`SELECT MAX("version") AS "version"
FROM "QingLong3LocalSecretEnvelopes"
WHERE "project_id" = ? AND "secret_name" = ?`,
)
.get(envelope.projectId, envelope.name) as QueryRow | undefined;
const currentVersion =
current?.version === null || current?.version === undefined
? 0
: requiredInteger(current, 'version');
if (currentVersion !== command.expectedCurrentVersion) {
throw new LocalSecretVersionConflictError();
}
const nonce = Buffer.from(envelope.nonce, 'base64url');
const ciphertext = Buffer.from(envelope.ciphertext, 'base64url');
const authTag = Buffer.from(envelope.authTag, 'base64url');
try {
this.client
.prepare(
`INSERT INTO "QingLong3LocalSecretEnvelopes" (
"project_id", "secret_name", "version", "mutation_id",
"key_id", "algorithm", "nonce", "ciphertext", "auth_tag",
"created_at_ms"
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
envelope.projectId,
envelope.name,
envelope.version,
envelope.mutationId,
envelope.keyId,
envelope.algorithm,
nonce,
ciphertext,
authTag,
envelope.createdAtMs,
);
} finally {
nonce.fill(0);
ciphertext.fill(0);
authTag.fill(0);
}
this.client.exec('COMMIT');
began = false;
return Object.freeze({
status: 'inserted' as const,
envelope,
});
} catch (error) {
if (began && this.client.isTransaction) {
try {
this.client.exec('ROLLBACK');
} catch {
// Preserve the original failure; close discards a broken handle.
}
}
if (error instanceof LocalSecretVersionConflictError) throw error;
if (error instanceof RunRepositoryError) throw error;
throw mapSqliteError(error);
}
});
}
findLocalSecretEnvelopeByMutation(
projectId: string,
name: string,
mutationId: string,
): Promise<LocalSecretEnvelope | null> {
assertLocalSecretProjectId(projectId);
assertLocalSecretName(name);
assertLocalSecretMutationId(mutationId);
return this.enqueue(async () => {
const row = singleRow(
queryRows(
this.client,
`SELECT ${LOCAL_SECRET_SELECT}
FROM "QingLong3LocalSecretEnvelopes"
WHERE "project_id" = ? AND "secret_name" = ?
AND "mutation_id" = ?
LIMIT 2`,
[projectId, name, mutationId],
),
);
return row ? localSecretEnvelopeFromRow(row) : null;
});
}
resolveLocalSecretEnvelopes(
references: readonly LocalSecretReference[],
): Promise<readonly (LocalSecretEnvelope | null)[]> {
if (
!Array.isArray(references) ||
references.length > MAX_LOCAL_SECRET_BATCH_SIZE
) {
return Promise.reject(new RangeError('Local Secret batch is too large'));
}
const normalized = references.map((reference) => {
createLocalSecretRef(reference);
return Object.freeze({ ...reference });
});
if (normalized.length === 0) return Promise.resolve(Object.freeze([]));
return this.enqueue(async () => {
const values: (string | number | null)[] = [];
const requested = normalized.map((reference, position) => {
values.push(
position,
reference.projectId,
reference.name,
reference.version ?? null,
);
return '(?, ?, ?, ?)';
});
const rows = queryRows(
this.client,
`WITH requested(position, project_id, secret_name, requested_version) AS (
VALUES ${requested.join(', ')}
)
SELECT requested.position AS "position",
envelope."project_id" AS "projectId",
envelope."secret_name" AS "name",
envelope."version" AS "version",
envelope."mutation_id" AS "mutationId",
envelope."key_id" AS "keyId",
envelope."algorithm" AS "algorithm",
envelope."nonce" AS "nonce",
envelope."ciphertext" AS "ciphertext",
envelope."auth_tag" AS "authTag",
envelope."created_at_ms" AS "createdAtMs"
FROM requested
LEFT JOIN "QingLong3LocalSecretEnvelopes" AS envelope
ON envelope."project_id" = requested.project_id
AND envelope."secret_name" = requested.secret_name
AND envelope."version" = COALESCE(
requested.requested_version,
(SELECT MAX(current."version")
FROM "QingLong3LocalSecretEnvelopes" AS current
WHERE current."project_id" = requested.project_id
AND current."secret_name" = requested.secret_name)
)
ORDER BY requested.position`,
values,
);
if (rows.length !== normalized.length) {
throw new RunRepositoryConstraintError(
'Local SQLite Secret batch result is incomplete',
);
}
return Object.freeze(
rows.map((row, position) => {
if (requiredInteger(row, 'position') !== position) {
throw new RunRepositoryConstraintError(
'Local SQLite Secret batch order is invalid',
);
}
return row.version === null ? null : localSecretEnvelopeFromRow(row);
}),
);
});
}
}
@@ -0,0 +1,350 @@
import type { DatabaseSync } from 'node:sqlite';
import {
normalizeLocalSecretEnvelope,
type LocalSecretEnvelope,
} from '@qinglong/runtime-core/local-secret';
import {
normalizeProjectRecord,
normalizeProjectRoleBinding,
type ProjectRecord,
type ProjectRoleBindingRecord,
} from '@qinglong/runtime-core/project-policy';
import {
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
} from '@qinglong/runtime-core/security-audit';
import {
RunRepositoryBusyError,
RunRepositoryConstraintError,
RunRepositoryError,
RunRepositoryOperationError,
} from '@qinglong/runtime-core/run-repository';
import {
createSqlitePersistencePrimitives,
sqliteDriverErrorCode,
sqliteDriverErrorNumber,
type SqliteQueryRow,
type SqliteQueryValue,
} from '../storage/sqlitePersistence';
export type QueryRow = SqliteQueryRow;
export function mapSqliteError(error: unknown): RunRepositoryError {
if (error instanceof RunRepositoryError) return error;
const baseCode = (sqliteDriverErrorNumber(error) ?? 0) & 0xff;
if (baseCode === 5 || baseCode === 6) {
return new RunRepositoryBusyError(error);
}
if (
baseCode === 19 ||
sqliteDriverErrorCode(error) === 'ERR_SQLITE_CONSTRAINT'
) {
return new RunRepositoryConstraintError(
'Local SQLite Run repository constraint violation',
error,
);
}
return new RunRepositoryOperationError(error);
}
const SECURITY_SQLITE_PERSISTENCE = createSqlitePersistencePrimitives({
invalidRowValue: (property) =>
new RunRepositoryConstraintError(
`Local SQLite Run row has an invalid ${property}`,
),
invalidJson: (property) =>
new RunRepositoryConstraintError(
`Local SQLite Run row has invalid ${property} JSON`,
),
unsupportedRowValue: (property) =>
new RunRepositoryConstraintError(
`Local SQLite Run row has an unsupported ${property}`,
),
duplicateIdentityRows: () =>
new RunRepositoryConstraintError(
'Local SQLite Run repository returned duplicate identity rows',
),
mapDriverError: mapSqliteError,
});
export function optionalInteger(
row: QueryRow,
property: string,
): number | undefined {
return SECURITY_SQLITE_PERSISTENCE.optionalInteger(row, property);
}
export function optionalString(
row: QueryRow,
property: string,
): string | undefined {
return SECURITY_SQLITE_PERSISTENCE.optionalString(row, property);
}
export function requiredBlob(row: QueryRow, property: string): Buffer {
try {
return SECURITY_SQLITE_PERSISTENCE.requiredBlob(row, property);
} catch (error) {
if (error instanceof RunRepositoryConstraintError) {
throw new RunRepositoryConstraintError(
`Local SQLite row has an invalid ${property}`,
);
}
throw error;
}
}
export function requiredInteger(row: QueryRow, property: string): number {
return SECURITY_SQLITE_PERSISTENCE.requiredInteger(row, property);
}
export function requiredJson(row: QueryRow, property: string): unknown {
return SECURITY_SQLITE_PERSISTENCE.requiredJson(row, property);
}
export function requiredString(row: QueryRow, property: string): string {
return SECURITY_SQLITE_PERSISTENCE.requiredString(row, property);
}
export function queryRows(
client: DatabaseSync,
sql: string,
values: readonly SqliteQueryValue[] = [],
): QueryRow[] {
return SECURITY_SQLITE_PERSISTENCE.queryRows(client, sql, values);
}
export function singleRow(rows: QueryRow[]): QueryRow | null {
return SECURITY_SQLITE_PERSISTENCE.singleRow(rows);
}
export function localSecretEnvelopeFromRow(row: QueryRow): LocalSecretEnvelope {
const nonce = requiredBlob(row, 'nonce');
const ciphertext = requiredBlob(row, 'ciphertext');
const authTag = requiredBlob(row, 'authTag');
try {
return normalizeLocalSecretEnvelope({
projectId: requiredString(row, 'projectId'),
name: requiredString(row, 'name'),
version: requiredInteger(row, 'version'),
mutationId: requiredString(row, 'mutationId'),
keyId: requiredString(row, 'keyId'),
algorithm: requiredString(row, 'algorithm') as 'aes-256-gcm',
nonce: nonce.toString('base64url'),
ciphertext: ciphertext.toString('base64url'),
authTag: authTag.toString('base64url'),
createdAtMs: requiredInteger(row, 'createdAtMs'),
});
} finally {
nonce.fill(0);
ciphertext.fill(0);
authTag.fill(0);
}
}
export const LOCAL_SECRET_SELECT = `
"project_id" AS "projectId",
"secret_name" AS "name",
"version" AS "version",
"mutation_id" AS "mutationId",
"key_id" AS "keyId",
"algorithm" AS "algorithm",
"nonce" AS "nonce",
"ciphertext" AS "ciphertext",
"auth_tag" AS "authTag",
"created_at_ms" AS "createdAtMs"
`;
export const LOCAL_PROJECT_SELECT = `
"id" AS "id",
"name" AS "name",
"slug" AS "slug",
"status" AS "status",
"version" AS "version",
"created_at_ms" AS "createdAtMs",
"updated_at_ms" AS "updatedAtMs"
`;
export const LOCAL_ROLE_BINDING_SELECT = `
"project_id" AS "projectId",
"subject_type" AS "subjectType",
"subject_id" AS "subjectId",
"version" AS "version",
"state" AS "state",
"role" AS "role",
"mutation_id" AS "mutationId",
"changed_by_type" AS "changedByType",
"changed_by_id" AS "changedById",
"created_at_ms" AS "createdAtMs"
`;
export const LOCAL_SECURITY_AUDIT_SELECT = `
"event_id" AS "eventId",
"request_id" AS "requestId",
"operation_id" AS "operationId",
"project_id" AS "projectId",
"subject_type" AS "subjectType",
"subject_id" AS "subjectId",
"authentication_id" AS "authenticationId",
"outcome" AS "outcome",
"reasons_json" AS "reasonsJson",
"fence_project_version" AS "fenceProjectVersion",
"fence_binding_version" AS "fenceBindingVersion",
"occurred_at_ms" AS "occurredAtMs"
`;
export const LOCAL_SECRET_JOIN_SELECT = `
secret."project_id" AS "projectId",
secret."secret_name" AS "name",
secret."version" AS "version",
secret."mutation_id" AS "mutationId",
secret."key_id" AS "keyId",
secret."algorithm" AS "algorithm",
secret."nonce" AS "nonce",
secret."ciphertext" AS "ciphertext",
secret."auth_tag" AS "authTag",
secret."created_at_ms" AS "createdAtMs"
`;
export const LOCAL_SECURITY_AUDIT_JOIN_SELECT = `
audit."event_id" AS "eventId",
audit."request_id" AS "requestId",
audit."operation_id" AS "operationId",
audit."project_id" AS "auditProjectId",
audit."subject_type" AS "subjectType",
audit."subject_id" AS "subjectId",
audit."authentication_id" AS "authenticationId",
audit."outcome" AS "outcome",
audit."reasons_json" AS "reasonsJson",
audit."fence_project_version" AS "fenceProjectVersion",
audit."fence_binding_version" AS "fenceBindingVersion",
audit."occurred_at_ms" AS "occurredAtMs"
`;
export function localProjectFromRow(row: QueryRow): Readonly<ProjectRecord> {
return normalizeProjectRecord({
id: requiredString(row, 'id'),
name: requiredString(row, 'name'),
slug: requiredString(row, 'slug'),
status: requiredString(row, 'status') as ProjectRecord['status'],
version: requiredInteger(row, 'version'),
createdAtMs: requiredInteger(row, 'createdAtMs'),
updatedAtMs: requiredInteger(row, 'updatedAtMs'),
});
}
export function localRoleBindingFromRow(
row: QueryRow,
): Readonly<ProjectRoleBindingRecord> {
const state = requiredString(
row,
'state',
) as ProjectRoleBindingRecord['state'];
return normalizeProjectRoleBinding({
projectId: requiredString(row, 'projectId'),
subject: {
type: requiredString(
row,
'subjectType',
) as ProjectRoleBindingRecord['subject']['type'],
id: requiredString(row, 'subjectId'),
},
version: requiredInteger(row, 'version'),
state,
...(state === 'active'
? {
role: requiredString(row, 'role') as NonNullable<
ProjectRoleBindingRecord['role']
>,
}
: {}),
mutationId: requiredString(row, 'mutationId'),
changedBy: {
type: requiredString(
row,
'changedByType',
) as ProjectRoleBindingRecord['changedBy']['type'],
id: requiredString(row, 'changedById'),
},
createdAtMs: requiredInteger(row, 'createdAtMs'),
});
}
export function localSecurityAuditFromRow(
row: QueryRow,
): Readonly<SecurityAuditRecord> {
const subjectType = optionalString(row, 'subjectType');
const subjectId = optionalString(row, 'subjectId');
const authenticationId = optionalString(row, 'authenticationId');
const fenceProjectVersion = optionalInteger(row, 'fenceProjectVersion');
const fenceBindingVersion = optionalInteger(row, 'fenceBindingVersion');
return normalizeSecurityAuditRecord({
eventId: requiredString(row, 'eventId'),
requestId: requiredString(row, 'requestId'),
operationId: requiredString(row, 'operationId'),
projectId:
optionalString(row, 'auditProjectId') ??
optionalString(row, 'projectId') ??
null,
subject:
subjectType && subjectId
? {
type: subjectType as NonNullable<
SecurityAuditRecord['subject']
>['type'],
id: subjectId,
}
: null,
authenticationId: authenticationId ?? null,
outcome: requiredString(row, 'outcome') as SecurityAuditRecord['outcome'],
reasons: requiredJson(row, 'reasonsJson') as readonly string[],
fence:
fenceProjectVersion === undefined
? null
: {
projectVersion: fenceProjectVersion,
bindingVersion: fenceBindingVersion ?? null,
},
occurredAtMs: requiredInteger(row, 'occurredAtMs'),
});
}
export function sameSecurityAuditSemantic(
left: Readonly<SecurityAuditRecord>,
right: Readonly<SecurityAuditRecord>,
): boolean {
const { occurredAtMs: _leftTime, ...leftSemantic } = left;
const { occurredAtMs: _rightTime, ...rightSemantic } = right;
return JSON.stringify(leftSemantic) === JSON.stringify(rightSemantic);
}
export function insertLocalSecurityAudit(
client: DatabaseSync,
audit: Readonly<SecurityAuditRecord>,
): void {
client
.prepare(
`INSERT INTO "QingLong3SecurityAuditEvents" (
"event_id", "request_id", "operation_id", "project_id",
"subject_type", "subject_id", "authentication_id", "outcome",
"reasons_json", "fence_project_version", "fence_binding_version",
"occurred_at_ms"
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
audit.eventId,
audit.requestId,
audit.operationId,
audit.projectId,
audit.subject?.type ?? null,
audit.subject?.id ?? null,
audit.authenticationId,
audit.outcome,
JSON.stringify(audit.reasons),
audit.fence?.projectVersion ?? null,
audit.fence?.bindingVersion ?? null,
audit.occurredAtMs,
);
}