mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
+115
@@ -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)
|
||||
);
|
||||
}
|
||||
+21
@@ -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(),
|
||||
);
|
||||
}
|
||||
+402
@@ -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(),
|
||||
);
|
||||
}
|
||||
+127
@@ -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;
|
||||
}
|
||||
}
|
||||
+240
@@ -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(),
|
||||
);
|
||||
}
|
||||
+346
@@ -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(),
|
||||
);
|
||||
}
|
||||
+137
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user