mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 02:27:44 +08:00
308 lines
9.4 KiB
TypeScript
308 lines
9.4 KiB
TypeScript
import {
|
|
assertTaskDefinitionIdentifier,
|
|
assertTaskDefinitionPageSize,
|
|
normalizeAppendTaskDefinitionRevisionCommand,
|
|
normalizeTaskDefinitionCursor,
|
|
type AppendTaskDefinitionRevisionCommand,
|
|
type TaskDefinitionCursor,
|
|
type TaskDefinitionPage,
|
|
type TaskDefinitionRecord,
|
|
} from './taskDefinition';
|
|
import { normalizeProjectPolicySubject } from '../security/project-policy/projectPolicy';
|
|
import type { SecurityPolicyFence, SecuritySubject } from '../security/security';
|
|
import {
|
|
normalizeSecurityAuditRecord,
|
|
type SecurityAuditRecord,
|
|
} from '../security/audit/securityAudit';
|
|
|
|
export interface AuthorizedTaskDefinitionRevisionMutation {
|
|
readonly command: AppendTaskDefinitionRevisionCommand;
|
|
readonly actor: SecuritySubject;
|
|
readonly fence: SecurityPolicyFence;
|
|
readonly audit: SecurityAuditRecord;
|
|
}
|
|
|
|
export interface TaskDefinitionAdministrationRepository {
|
|
appendAuthorizedTaskDefinitionRevision(
|
|
mutation: AuthorizedTaskDefinitionRevisionMutation,
|
|
): Promise<
|
|
Readonly<{
|
|
status: 'created' | 'updated' | 'existing';
|
|
definition: TaskDefinitionRecord;
|
|
}>
|
|
>;
|
|
}
|
|
|
|
export interface AuthorizedTaskDefinitionInspection {
|
|
readonly projectId: string;
|
|
readonly taskId: string;
|
|
readonly actor: SecuritySubject;
|
|
readonly fence: SecurityPolicyFence;
|
|
readonly audit: SecurityAuditRecord;
|
|
}
|
|
|
|
export interface AuthorizedTaskDefinitionList {
|
|
readonly projectId: string;
|
|
readonly limit: number;
|
|
readonly after?: TaskDefinitionCursor;
|
|
readonly actor: SecuritySubject;
|
|
readonly fence: SecurityPolicyFence;
|
|
readonly audit: SecurityAuditRecord;
|
|
}
|
|
|
|
export interface TaskDefinitionAdministrationSource {
|
|
findAuthorizedCurrentTaskDefinition(
|
|
inspection: AuthorizedTaskDefinitionInspection,
|
|
): Promise<TaskDefinitionRecord | null>;
|
|
listAuthorizedTaskDefinitions(
|
|
query: AuthorizedTaskDefinitionList,
|
|
): Promise<TaskDefinitionPage>;
|
|
}
|
|
|
|
export class InvalidTaskDefinitionAdministrationMutationError extends TypeError {
|
|
readonly code = 'TASK_DEFINITION_ADMINISTRATION_MUTATION_INVALID';
|
|
|
|
constructor(message: string) {
|
|
super(`TaskDefinition administration mutation is invalid: ${message}`);
|
|
this.name = 'InvalidTaskDefinitionAdministrationMutationError';
|
|
}
|
|
}
|
|
|
|
export class TaskDefinitionAdministrationAuthorizationFenceConflictError extends Error {
|
|
readonly code = 'TASK_DEFINITION_ADMINISTRATION_AUTHORIZATION_FENCE_CONFLICT';
|
|
|
|
constructor() {
|
|
super('TaskDefinition administration authorization fence changed');
|
|
this.name =
|
|
'TaskDefinitionAdministrationAuthorizationFenceConflictError';
|
|
}
|
|
}
|
|
|
|
export class TaskDefinitionAdministrationMutationConflictError extends Error {
|
|
readonly code = 'TASK_DEFINITION_ADMINISTRATION_MUTATION_CONFLICT';
|
|
|
|
constructor() {
|
|
super('TaskDefinition administration mutation conflicts with durable state');
|
|
this.name = 'TaskDefinitionAdministrationMutationConflictError';
|
|
}
|
|
}
|
|
|
|
export class InvalidTaskDefinitionAdministrationReadError extends TypeError {
|
|
readonly code = 'TASK_DEFINITION_ADMINISTRATION_READ_INVALID';
|
|
|
|
constructor(message: string) {
|
|
super(`TaskDefinition administration read is invalid: ${message}`);
|
|
this.name = 'InvalidTaskDefinitionAdministrationReadError';
|
|
}
|
|
}
|
|
|
|
export class TaskDefinitionAdministrationReadConflictError extends Error {
|
|
readonly code = 'TASK_DEFINITION_ADMINISTRATION_READ_CONFLICT';
|
|
|
|
constructor() {
|
|
super('TaskDefinition administration read conflicts with durable state');
|
|
this.name = 'TaskDefinitionAdministrationReadConflictError';
|
|
}
|
|
}
|
|
|
|
function exactKeys(value: object, expected: readonly string[]): boolean {
|
|
const actual = Object.keys(value).sort();
|
|
const canonical = [...expected].sort();
|
|
return (
|
|
actual.length === canonical.length &&
|
|
actual.every((key, index) => key === canonical[index])
|
|
);
|
|
}
|
|
|
|
function sameSubject(
|
|
left: Readonly<SecuritySubject>,
|
|
right: Readonly<SecuritySubject>,
|
|
): boolean {
|
|
return left.type === right.type && left.id === right.id;
|
|
}
|
|
|
|
function normalizeFence(value: SecurityPolicyFence): SecurityPolicyFence {
|
|
if (
|
|
!value ||
|
|
typeof value !== 'object' ||
|
|
Array.isArray(value) ||
|
|
!exactKeys(value, ['bindingVersion', 'projectVersion']) ||
|
|
!Number.isSafeInteger(value.projectVersion) ||
|
|
value.projectVersion < 1 ||
|
|
!Number.isSafeInteger(value.bindingVersion) ||
|
|
(value.bindingVersion as number) < 1
|
|
) {
|
|
throw new InvalidTaskDefinitionAdministrationMutationError(
|
|
'authorization fence is invalid',
|
|
);
|
|
}
|
|
return Object.freeze({
|
|
projectVersion: value.projectVersion,
|
|
bindingVersion: value.bindingVersion,
|
|
});
|
|
}
|
|
|
|
export function normalizeAuthorizedTaskDefinitionRevisionMutation(
|
|
value: AuthorizedTaskDefinitionRevisionMutation,
|
|
): Readonly<AuthorizedTaskDefinitionRevisionMutation> {
|
|
if (
|
|
!value ||
|
|
typeof value !== 'object' ||
|
|
Array.isArray(value) ||
|
|
!exactKeys(value, ['actor', 'audit', 'command', 'fence'])
|
|
) {
|
|
throw new InvalidTaskDefinitionAdministrationMutationError(
|
|
'mutation shape is invalid',
|
|
);
|
|
}
|
|
try {
|
|
const command = normalizeAppendTaskDefinitionRevisionCommand(
|
|
value.command,
|
|
);
|
|
const actor = normalizeProjectPolicySubject(value.actor);
|
|
const fence = normalizeFence(value.fence);
|
|
const audit = normalizeSecurityAuditRecord(value.audit);
|
|
const operationId =
|
|
command.expectedRevision === null ? 'task.create' : 'task.update';
|
|
if (
|
|
audit.eventId !== command.mutationId ||
|
|
audit.operationId !== operationId ||
|
|
audit.projectId !== command.projectId ||
|
|
audit.outcome !== 'allowed' ||
|
|
!audit.subject ||
|
|
!sameSubject(audit.subject, actor) ||
|
|
audit.authenticationId === null ||
|
|
!audit.fence ||
|
|
audit.fence.projectVersion !== fence.projectVersion ||
|
|
audit.fence.bindingVersion !== fence.bindingVersion
|
|
) {
|
|
throw new InvalidTaskDefinitionAdministrationMutationError(
|
|
'audit binding is invalid',
|
|
);
|
|
}
|
|
return Object.freeze({ command, actor, fence, audit });
|
|
} catch (error) {
|
|
if (error instanceof InvalidTaskDefinitionAdministrationMutationError) {
|
|
throw error;
|
|
}
|
|
throw new InvalidTaskDefinitionAdministrationMutationError(
|
|
'mutation value is invalid',
|
|
);
|
|
}
|
|
}
|
|
|
|
function normalizeTaskDefinitionReadAuthority(
|
|
value: Readonly<{
|
|
projectId: string;
|
|
actor: SecuritySubject;
|
|
fence: SecurityPolicyFence;
|
|
audit: SecurityAuditRecord;
|
|
}>,
|
|
): Readonly<{
|
|
projectId: string;
|
|
actor: SecuritySubject;
|
|
fence: SecurityPolicyFence;
|
|
audit: SecurityAuditRecord;
|
|
}> {
|
|
const actor = normalizeProjectPolicySubject(value.actor);
|
|
const fence = normalizeFence(value.fence);
|
|
const audit = normalizeSecurityAuditRecord(value.audit);
|
|
if (
|
|
audit.operationId !== 'task.read' ||
|
|
audit.projectId !== value.projectId ||
|
|
audit.outcome !== 'allowed' ||
|
|
!audit.subject ||
|
|
!sameSubject(audit.subject, actor) ||
|
|
audit.authenticationId === null ||
|
|
!audit.fence ||
|
|
audit.fence.projectVersion !== fence.projectVersion ||
|
|
audit.fence.bindingVersion !== fence.bindingVersion
|
|
) {
|
|
throw new InvalidTaskDefinitionAdministrationReadError(
|
|
'audit binding is invalid',
|
|
);
|
|
}
|
|
return Object.freeze({
|
|
projectId: value.projectId,
|
|
actor,
|
|
fence,
|
|
audit,
|
|
});
|
|
}
|
|
|
|
export function normalizeAuthorizedTaskDefinitionInspection(
|
|
value: AuthorizedTaskDefinitionInspection,
|
|
): Readonly<AuthorizedTaskDefinitionInspection> {
|
|
if (
|
|
!value ||
|
|
typeof value !== 'object' ||
|
|
Array.isArray(value) ||
|
|
!exactKeys(value, ['actor', 'audit', 'fence', 'projectId', 'taskId'])
|
|
) {
|
|
throw new InvalidTaskDefinitionAdministrationReadError(
|
|
'inspection shape is invalid',
|
|
);
|
|
}
|
|
try {
|
|
assertTaskDefinitionIdentifier(value.projectId, 'projectId');
|
|
assertTaskDefinitionIdentifier(value.taskId, 'taskId');
|
|
return Object.freeze({
|
|
...normalizeTaskDefinitionReadAuthority(value),
|
|
taskId: value.taskId,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof InvalidTaskDefinitionAdministrationReadError) {
|
|
throw error;
|
|
}
|
|
throw new InvalidTaskDefinitionAdministrationReadError(
|
|
'inspection value is invalid',
|
|
);
|
|
}
|
|
}
|
|
|
|
export function normalizeAuthorizedTaskDefinitionList(
|
|
value: AuthorizedTaskDefinitionList,
|
|
): Readonly<AuthorizedTaskDefinitionList> {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
throw new InvalidTaskDefinitionAdministrationReadError(
|
|
'list shape is invalid',
|
|
);
|
|
}
|
|
const keys = Object.keys(value);
|
|
if (
|
|
!['actor', 'audit', 'fence', 'limit', 'projectId'].every((key) =>
|
|
keys.includes(key),
|
|
) ||
|
|
keys.some(
|
|
(key) =>
|
|
!['actor', 'after', 'audit', 'fence', 'limit', 'projectId'].includes(
|
|
key,
|
|
),
|
|
)
|
|
) {
|
|
throw new InvalidTaskDefinitionAdministrationReadError(
|
|
'list shape is invalid',
|
|
);
|
|
}
|
|
try {
|
|
assertTaskDefinitionIdentifier(value.projectId, 'projectId');
|
|
assertTaskDefinitionPageSize(value.limit);
|
|
const authority = normalizeTaskDefinitionReadAuthority(value);
|
|
const after = Object.hasOwn(value, 'after')
|
|
? normalizeTaskDefinitionCursor(value.after as TaskDefinitionCursor)
|
|
: undefined;
|
|
return Object.freeze({
|
|
...authority,
|
|
limit: value.limit,
|
|
...(after ? { after } : {}),
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof InvalidTaskDefinitionAdministrationReadError) {
|
|
throw error;
|
|
}
|
|
throw new InvalidTaskDefinitionAdministrationReadError(
|
|
'list value is invalid',
|
|
);
|
|
}
|
|
}
|