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,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,
);
}
}