142 lines
4.6 KiB
TypeScript
142 lines
4.6 KiB
TypeScript
import { CredentialAuditJournal, CredentialJournalError } from './audit-journal.js';
|
|
import type {
|
|
CredentialValidationDependencies,
|
|
GiteaReadValidationRequestDto,
|
|
GiteaWriteValidationRequestDto,
|
|
} from './credential-provider.dto.js';
|
|
import type {
|
|
CredentialValidationResultDto,
|
|
RepositoryPermission,
|
|
} from './credential-result.dto.js';
|
|
import { evaluateGiteaReadValidation, evaluateGiteaWriteValidation } from './validate.js';
|
|
|
|
export interface CredentialValidationServiceOptions {
|
|
readonly stateRoot: string;
|
|
readonly actor: string;
|
|
readonly operation?: 'validate' | 'whoami';
|
|
}
|
|
|
|
function permissionDecision(permission: RepositoryPermission): string {
|
|
if (permission === 'none') return 'permission-none';
|
|
if (permission === 'admin') return 'permission-admin';
|
|
if (permission === 'write') return 'permission-write';
|
|
return 'permission-read';
|
|
}
|
|
|
|
async function openValidationJournal(
|
|
request: GiteaReadValidationRequestDto,
|
|
options: CredentialValidationServiceOptions,
|
|
): Promise<CredentialAuditJournal> {
|
|
const journal = await CredentialAuditJournal.open(options.stateRoot, {
|
|
operation: options.operation ?? 'validate',
|
|
actor: options.actor,
|
|
identity: request.identity,
|
|
estate: request.estate,
|
|
host: request.host,
|
|
repo: request.repo,
|
|
});
|
|
await journal.recordIntent(
|
|
options.operation === 'whoami' ? 'whoami-requested' : 'validation-requested',
|
|
);
|
|
return journal;
|
|
}
|
|
|
|
async function recordAndSealValidation(
|
|
journal: CredentialAuditJournal,
|
|
validation: CredentialValidationResultDto,
|
|
): Promise<CredentialValidationResultDto> {
|
|
if (validation.evidence.providerIdentity !== null) {
|
|
await journal.recordProviderEvidence({
|
|
endpoint: validation.evidence.providerIdentity.endpoint,
|
|
contentType: validation.evidence.providerIdentity.contentType,
|
|
decision: 'identity-verified',
|
|
});
|
|
}
|
|
if (validation.evidence.repositoryPermission !== null) {
|
|
await journal.recordProviderEvidence({
|
|
endpoint: validation.evidence.repositoryPermission.endpoint,
|
|
contentType: validation.evidence.repositoryPermission.contentType,
|
|
decision: permissionDecision(validation.evidence.repositoryPermission.effective),
|
|
});
|
|
}
|
|
await journal.seal(validation.outcome, validation.reason.code);
|
|
return {
|
|
...validation,
|
|
audit: { journalId: journal.journalId(), state: 'sealed' },
|
|
};
|
|
}
|
|
|
|
function journalFailureResult(
|
|
request: GiteaReadValidationRequestDto,
|
|
journal: CredentialAuditJournal,
|
|
error: CredentialJournalError,
|
|
operation: 'validate' | 'whoami',
|
|
): CredentialValidationResultDto {
|
|
return {
|
|
schemaVersion: 1,
|
|
operation,
|
|
outcome: 'error',
|
|
exitCode: 20,
|
|
retryable: false,
|
|
subject: {
|
|
identity: request.identity,
|
|
estate: request.estate,
|
|
host: request.host,
|
|
repo: request.repo,
|
|
},
|
|
mutation: 'none',
|
|
reason: {
|
|
code: error.code,
|
|
message: 'Validation audit persistence failed; inspect the durable open journal.',
|
|
},
|
|
evidence: {
|
|
providerIdentity: null,
|
|
tokenCapabilities: {
|
|
state: 'not-measured',
|
|
scopes: [],
|
|
source: 'runtime-not-authorized',
|
|
},
|
|
repositoryPermission: null,
|
|
writeDifferential: null,
|
|
},
|
|
audit: { journalId: journal.journalId(), state: 'open' },
|
|
};
|
|
}
|
|
|
|
export async function runCredentialReadValidation(
|
|
request: GiteaReadValidationRequestDto,
|
|
dependencies: CredentialValidationDependencies,
|
|
options: CredentialValidationServiceOptions,
|
|
): Promise<CredentialValidationResultDto> {
|
|
const journal = await openValidationJournal(request, options);
|
|
try {
|
|
const validation = await evaluateGiteaReadValidation(request, dependencies);
|
|
return await recordAndSealValidation(journal, {
|
|
...validation,
|
|
operation: options.operation ?? 'validate',
|
|
});
|
|
} catch (error: unknown) {
|
|
if (error instanceof CredentialJournalError) {
|
|
return journalFailureResult(request, journal, error, options.operation ?? 'validate');
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function runCredentialValidation(
|
|
request: GiteaWriteValidationRequestDto,
|
|
dependencies: CredentialValidationDependencies,
|
|
options: CredentialValidationServiceOptions,
|
|
): Promise<CredentialValidationResultDto> {
|
|
const journal = await openValidationJournal(request, options);
|
|
try {
|
|
const validation = await evaluateGiteaWriteValidation(request, dependencies);
|
|
return await recordAndSealValidation(journal, validation);
|
|
} catch (error: unknown) {
|
|
if (error instanceof CredentialJournalError) {
|
|
return journalFailureResult(request, journal, error, 'validate');
|
|
}
|
|
throw error;
|
|
}
|
|
}
|