fix(mosaic): preserve credential transaction evidence
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import { timingSafeEqual } from 'node:crypto';
|
||||||
import { fstatSync, writeSync } from 'node:fs';
|
import { fstatSync, writeSync } from 'node:fs';
|
||||||
import { open, rename } from 'node:fs/promises';
|
import { open, rename } from 'node:fs/promises';
|
||||||
import { homedir } from 'node:os';
|
import { homedir } from 'node:os';
|
||||||
@@ -552,7 +553,18 @@ export async function executeCredentialRotate(
|
|||||||
audit: { journalId: journal.journalId(), state: 'sealed' },
|
audit: { journalId: journal.journalId(), state: 'sealed' },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
await journal.recordMutation('token-mint-applied');
|
await journal.recordMutation('token-mint-applied');
|
||||||
|
} catch (error: unknown) {
|
||||||
|
await context.provider.revokeToken(authority, identity, options.tokenName);
|
||||||
|
if (await context.provider.tokenExists(authority, identity, options.tokenName)) {
|
||||||
|
throw new Error('replacement rollback after journal failure could not be verified');
|
||||||
|
}
|
||||||
|
await context.store.put(old.binding, old.secret);
|
||||||
|
await context.teaStore.put(identity, options.host, old.secret);
|
||||||
|
old.secret.fill(0);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await context.provider.revokeToken(authority, identity, old.binding.tokenName);
|
await context.provider.revokeToken(authority, identity, old.binding.tokenName);
|
||||||
if (await context.provider.tokenExists(authority, identity, old.binding.tokenName)) {
|
if (await context.provider.tokenExists(authority, identity, old.binding.tokenName)) {
|
||||||
@@ -560,20 +572,14 @@ export async function executeCredentialRotate(
|
|||||||
}
|
}
|
||||||
await journal.recordMutation('token-revoke-applied');
|
await journal.recordMutation('token-revoke-applied');
|
||||||
} catch {
|
} catch {
|
||||||
await context.provider.revokeToken(authority, identity, options.tokenName);
|
await journal.seal('indeterminate', 'old-credential-state-unknown');
|
||||||
if (await context.provider.tokenExists(authority, identity, options.tokenName)) {
|
|
||||||
throw new Error('replacement rollback could not be verified');
|
|
||||||
}
|
|
||||||
await context.store.put(old.binding, old.secret);
|
|
||||||
await context.teaStore.put(identity, options.host, old.secret);
|
|
||||||
await journal.recordMutation('rotate-rollback-verified');
|
|
||||||
await journal.seal('indeterminate', 'old-credential-preserved');
|
|
||||||
old.secret.fill(0);
|
old.secret.fill(0);
|
||||||
return localLifecycleResult('rotate', identity, options, {
|
return localLifecycleResult('rotate', identity, options, {
|
||||||
outcome: 'indeterminate',
|
outcome: 'indeterminate',
|
||||||
mutation: 'none',
|
mutation: 'unknown',
|
||||||
code: 'old-credential-preserved',
|
code: 'old-credential-state-unknown',
|
||||||
message: 'Replacement failed; the previous credential remains the canonical binding.',
|
message:
|
||||||
|
'The verified replacement remains canonical, but old-token revocation state is unknown.',
|
||||||
audit: { journalId: journal.journalId(), state: 'sealed' },
|
audit: { journalId: journal.journalId(), state: 'sealed' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -707,16 +713,12 @@ export async function executeCredentialGet(
|
|||||||
});
|
});
|
||||||
await journal.recordIntent('get-requested');
|
await journal.recordIntent('get-requested');
|
||||||
try {
|
try {
|
||||||
if (
|
if (options.authorityFd === undefined || options.actor !== identity) {
|
||||||
process.env['MOSAIC_AGENT_NAME'] === undefined ||
|
|
||||||
process.env['MOSAIC_AGENT_NAME'] !== identity ||
|
|
||||||
options.actor !== identity
|
|
||||||
) {
|
|
||||||
await journal.seal('refused', 'provider-identity-mismatch');
|
await journal.seal('refused', 'provider-identity-mismatch');
|
||||||
return localLifecycleResult('get', identity, options, {
|
return localLifecycleResult('get', identity, options, {
|
||||||
outcome: 'refused',
|
outcome: 'refused',
|
||||||
code: 'provider-identity-mismatch',
|
code: 'provider-identity-mismatch',
|
||||||
message: 'Runtime fleet identity, explicit actor, and requested identity must match.',
|
message: 'Protected authority, explicit actor, and requested identity must match.',
|
||||||
audit: { journalId: journal.journalId(), state: 'sealed' },
|
audit: { journalId: journal.journalId(), state: 'sealed' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -731,11 +733,27 @@ export async function executeCredentialGet(
|
|||||||
throw new Error('unsafe output fd');
|
throw new Error('unsafe output fd');
|
||||||
}
|
}
|
||||||
const context = await lifecycleContext(options);
|
const context = await lifecycleContext(options);
|
||||||
|
const authority = await lifecycleAuthority(identity, options);
|
||||||
|
const providerIdentity = await context.provider.readIdentity(authority);
|
||||||
|
if (providerIdentity.login !== identity) {
|
||||||
|
await journal.seal('refused', 'provider-identity-mismatch');
|
||||||
|
return localLifecycleResult('get', identity, options, {
|
||||||
|
outcome: 'refused',
|
||||||
|
code: 'provider-identity-mismatch',
|
||||||
|
message:
|
||||||
|
'Provider read-back did not bind the protected authority to the requested identity.',
|
||||||
|
audit: { journalId: journal.journalId(), state: 'sealed' },
|
||||||
|
});
|
||||||
|
}
|
||||||
const resolved = await new FileCredentialResolver(
|
const resolved = await new FileCredentialResolver(
|
||||||
lifecycleLocations(options).tokenDirectory,
|
lifecycleLocations(options).tokenDirectory,
|
||||||
context.registry,
|
context.registry,
|
||||||
).resolve(identity, options.estate, options.host);
|
).resolve(identity, options.estate, options.host);
|
||||||
if (resolved === undefined) {
|
if (
|
||||||
|
resolved === undefined ||
|
||||||
|
authority.secret.byteLength !== resolved.secret.byteLength ||
|
||||||
|
!timingSafeEqual(Buffer.from(authority.secret), Buffer.from(resolved.secret))
|
||||||
|
) {
|
||||||
await journal.seal('refused', 'no-token-for-identity');
|
await journal.seal('refused', 'no-token-for-identity');
|
||||||
return localLifecycleResult('get', identity, options, {
|
return localLifecycleResult('get', identity, options, {
|
||||||
outcome: 'refused',
|
outcome: 'refused',
|
||||||
@@ -862,6 +880,17 @@ type PrintableCredentialResult = Pick<
|
|||||||
'operation' | 'outcome' | 'exitCode' | 'reason'
|
'operation' | 'outcome' | 'exitCode' | 'reason'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
function withParentMosaicHome<T extends { readonly mosaicHome?: string }>(
|
||||||
|
parent: Command,
|
||||||
|
options: T,
|
||||||
|
): T {
|
||||||
|
const inherited = parent.opts<{ mosaicHome?: string }>();
|
||||||
|
return {
|
||||||
|
...options,
|
||||||
|
...(inherited.mosaicHome === undefined ? {} : { mosaicHome: inherited.mosaicHome }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function printCredentialResult(result: PrintableCredentialResult, json: boolean): void {
|
function printCredentialResult(result: PrintableCredentialResult, json: boolean): void {
|
||||||
if (json) {
|
if (json) {
|
||||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||||
@@ -897,7 +926,10 @@ export function registerCredentialCommand(parent: Command): void {
|
|||||||
.option('--state-dir <path>', 'Durable credential journal root')
|
.option('--state-dir <path>', 'Durable credential journal root')
|
||||||
.option('--json', 'Emit one machine result object')
|
.option('--json', 'Emit one machine result object')
|
||||||
.action(async (identity: string, options: CredentialLifecycleCommandOptions): Promise<void> => {
|
.action(async (identity: string, options: CredentialLifecycleCommandOptions): Promise<void> => {
|
||||||
const result = await executeCredentialProvision(identity, options);
|
const result = await executeCredentialProvision(
|
||||||
|
identity,
|
||||||
|
withParentMosaicHome(cred, options),
|
||||||
|
);
|
||||||
printCredentialResult(result, options.json === true);
|
printCredentialResult(result, options.json === true);
|
||||||
process.exitCode = result.exitCode;
|
process.exitCode = result.exitCode;
|
||||||
});
|
});
|
||||||
@@ -912,7 +944,7 @@ export function registerCredentialCommand(parent: Command): void {
|
|||||||
.option('--state-dir <path>', 'Durable credential journal root')
|
.option('--state-dir <path>', 'Durable credential journal root')
|
||||||
.option('--json', 'Emit one machine result object')
|
.option('--json', 'Emit one machine result object')
|
||||||
.action(async (identity: string, options: CredentialLifecycleCommandOptions): Promise<void> => {
|
.action(async (identity: string, options: CredentialLifecycleCommandOptions): Promise<void> => {
|
||||||
const result = await executeCredentialWire(identity, options);
|
const result = await executeCredentialWire(identity, withParentMosaicHome(cred, options));
|
||||||
printCredentialResult(result, options.json === true);
|
printCredentialResult(result, options.json === true);
|
||||||
process.exitCode = result.exitCode;
|
process.exitCode = result.exitCode;
|
||||||
});
|
});
|
||||||
@@ -922,14 +954,15 @@ export function registerCredentialCommand(parent: Command): void {
|
|||||||
.description('Emit one exact credential only to a protected inherited fd')
|
.description('Emit one exact credential only to a protected inherited fd')
|
||||||
.requiredOption('--estate <estate>', 'Explicit target estate')
|
.requiredOption('--estate <estate>', 'Explicit target estate')
|
||||||
.requiredOption('--host <host>', 'Explicit provider host')
|
.requiredOption('--host <host>', 'Explicit provider host')
|
||||||
.requiredOption('--actor <identity>', 'Explicit audit actor')
|
.requiredOption('--actor <identity>', 'Explicit protected authority identity')
|
||||||
|
.requiredOption('--authority-fd <fd>', 'Inherited protected credential authority fd')
|
||||||
.requiredOption('--output-fd <fd>', 'Protected inherited credential output fd')
|
.requiredOption('--output-fd <fd>', 'Protected inherited credential output fd')
|
||||||
.option('--registry <path>', 'Strict non-secret estate registry')
|
.option('--registry <path>', 'Strict non-secret estate registry')
|
||||||
.option('--token-dir <path>', 'Governed token directory')
|
.option('--token-dir <path>', 'Governed token directory')
|
||||||
.option('--state-dir <path>', 'Durable credential journal root')
|
.option('--state-dir <path>', 'Durable credential journal root')
|
||||||
.option('--json', 'Emit one non-secret machine result object')
|
.option('--json', 'Emit one non-secret machine result object')
|
||||||
.action(async (identity: string, options: CredentialLifecycleCommandOptions): Promise<void> => {
|
.action(async (identity: string, options: CredentialLifecycleCommandOptions): Promise<void> => {
|
||||||
const result = await executeCredentialGet(identity, options);
|
const result = await executeCredentialGet(identity, withParentMosaicHome(cred, options));
|
||||||
printCredentialResult(result, options.json === true);
|
printCredentialResult(result, options.json === true);
|
||||||
process.exitCode = result.exitCode;
|
process.exitCode = result.exitCode;
|
||||||
});
|
});
|
||||||
@@ -949,7 +982,7 @@ export function registerCredentialCommand(parent: Command): void {
|
|||||||
.option('--state-dir <path>', 'Durable credential journal root')
|
.option('--state-dir <path>', 'Durable credential journal root')
|
||||||
.option('--json', 'Emit one machine result object')
|
.option('--json', 'Emit one machine result object')
|
||||||
.action(async (identity: string, options: CredentialLifecycleCommandOptions): Promise<void> => {
|
.action(async (identity: string, options: CredentialLifecycleCommandOptions): Promise<void> => {
|
||||||
const result = await executeCredentialRotate(identity, options);
|
const result = await executeCredentialRotate(identity, withParentMosaicHome(cred, options));
|
||||||
printCredentialResult(result, options.json === true);
|
printCredentialResult(result, options.json === true);
|
||||||
process.exitCode = result.exitCode;
|
process.exitCode = result.exitCode;
|
||||||
});
|
});
|
||||||
@@ -966,7 +999,7 @@ export function registerCredentialCommand(parent: Command): void {
|
|||||||
.option('--state-dir <path>', 'Durable credential journal root')
|
.option('--state-dir <path>', 'Durable credential journal root')
|
||||||
.option('--json', 'Emit one machine result object')
|
.option('--json', 'Emit one machine result object')
|
||||||
.action(async (identity: string, options: CredentialLifecycleCommandOptions): Promise<void> => {
|
.action(async (identity: string, options: CredentialLifecycleCommandOptions): Promise<void> => {
|
||||||
const result = await executeCredentialRevoke(identity, options);
|
const result = await executeCredentialRevoke(identity, withParentMosaicHome(cred, options));
|
||||||
printCredentialResult(result, options.json === true);
|
printCredentialResult(result, options.json === true);
|
||||||
process.exitCode = result.exitCode;
|
process.exitCode = result.exitCode;
|
||||||
});
|
});
|
||||||
@@ -983,7 +1016,7 @@ export function registerCredentialCommand(parent: Command): void {
|
|||||||
.option('--state-dir <path>', 'Durable credential journal root')
|
.option('--state-dir <path>', 'Durable credential journal root')
|
||||||
.option('--json', 'Emit one machine result object')
|
.option('--json', 'Emit one machine result object')
|
||||||
.action(async (options: CredentialLifecycleCommandOptions): Promise<void> => {
|
.action(async (options: CredentialLifecycleCommandOptions): Promise<void> => {
|
||||||
const result = await executeCredentialList(options);
|
const result = await executeCredentialList(withParentMosaicHome(cred, options));
|
||||||
printCredentialResult(result, options.json === true);
|
printCredentialResult(result, options.json === true);
|
||||||
process.exitCode = result.exitCode;
|
process.exitCode = result.exitCode;
|
||||||
});
|
});
|
||||||
@@ -999,7 +1032,7 @@ export function registerCredentialCommand(parent: Command): void {
|
|||||||
.option('--state-dir <path>', 'Durable credential journal root')
|
.option('--state-dir <path>', 'Durable credential journal root')
|
||||||
.option('--json', 'Emit one machine result object')
|
.option('--json', 'Emit one machine result object')
|
||||||
.action(async (options: CredentialLifecycleCommandOptions): Promise<void> => {
|
.action(async (options: CredentialLifecycleCommandOptions): Promise<void> => {
|
||||||
const result = await executeCredentialAudit(options);
|
const result = await executeCredentialAudit(withParentMosaicHome(cred, options));
|
||||||
printCredentialResult(result, options.json === true);
|
printCredentialResult(result, options.json === true);
|
||||||
process.exitCode = result.exitCode;
|
process.exitCode = result.exitCode;
|
||||||
});
|
});
|
||||||
@@ -1066,7 +1099,7 @@ export function registerCredentialCommand(parent: Command): void {
|
|||||||
.option('--json', 'Emit one machine result object')
|
.option('--json', 'Emit one machine result object')
|
||||||
.action(async (identity: string, options: CredentialValidateCommandOptions): Promise<void> => {
|
.action(async (identity: string, options: CredentialValidateCommandOptions): Promise<void> => {
|
||||||
const result = await executeCredentialValidate(identity, {
|
const result = await executeCredentialValidate(identity, {
|
||||||
...options,
|
...withParentMosaicHome(cred, options),
|
||||||
require: 'read',
|
require: 'read',
|
||||||
operation: 'whoami',
|
operation: 'whoami',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -343,6 +343,14 @@ export class FileCredentialStore {
|
|||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
throw new CredentialStoreError('invalid-binding', 'credential envelope is malformed');
|
throw new CredentialStoreError('invalid-binding', 'credential envelope is malformed');
|
||||||
}
|
}
|
||||||
|
const verified = await new FileCredentialResolver(
|
||||||
|
this.tokenDirectory,
|
||||||
|
this.estateRegistry,
|
||||||
|
).resolve(identity, estate, host);
|
||||||
|
if (verified === undefined) {
|
||||||
|
throw new CredentialStoreError('invalid-binding', 'credential envelope was not resolvable');
|
||||||
|
}
|
||||||
|
verified.secret.fill(0);
|
||||||
return {
|
return {
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
identity: parsed.data.identity,
|
identity: parsed.data.identity,
|
||||||
|
|||||||
@@ -239,11 +239,19 @@ describe('Gitea read validation', (): void => {
|
|||||||
describe('principal-bound Gitea write validation contract v1.1', (): void => {
|
describe('principal-bound Gitea write validation contract v1.1', (): void => {
|
||||||
it('confirms write capability when identity is scope-forbidden without exposing the internal reason', async (): Promise<void> => {
|
it('confirms write capability when identity is scope-forbidden without exposing the internal reason', async (): Promise<void> => {
|
||||||
const observed = fixture();
|
const observed = fixture();
|
||||||
observed.dependencies.provider.readIdentity = async (): Promise<ProviderIdentityEvidence> => {
|
const readIdentity = observed.dependencies.provider.readIdentity.bind(
|
||||||
|
observed.dependencies.provider,
|
||||||
|
);
|
||||||
|
observed.dependencies.provider.readIdentity = async (
|
||||||
|
resolved,
|
||||||
|
): Promise<ProviderIdentityEvidence> => {
|
||||||
|
if (resolved.identity === SUBJECT) {
|
||||||
throw new CredentialProviderEvidenceError(
|
throw new CredentialProviderEvidenceError(
|
||||||
'identity-read-forbidden',
|
'identity-read-forbidden',
|
||||||
'identity endpoint scope forbidden',
|
'identity endpoint scope forbidden',
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
return readIdentity(resolved);
|
||||||
};
|
};
|
||||||
const result = await evaluateGiteaWriteValidation(
|
const result = await evaluateGiteaWriteValidation(
|
||||||
{
|
{
|
||||||
@@ -259,7 +267,11 @@ describe('principal-bound Gitea write validation contract v1.1', (): void => {
|
|||||||
expect(result.outcome).toBe('indeterminate');
|
expect(result.outcome).toBe('indeterminate');
|
||||||
expect(result.reason.code).toBe('identity-not-measured');
|
expect(result.reason.code).toBe('identity-not-measured');
|
||||||
expect(result.evidence.repositoryPermission?.effective).toBe('write');
|
expect(result.evidence.repositoryPermission?.effective).toBe('write');
|
||||||
expect(observed.receivePackHandles).toEqual([expect.objectContaining({ identity: SUBJECT })]);
|
expect(observed.receivePackHandles).toEqual([
|
||||||
|
expect.objectContaining({ identity: SUBJECT }),
|
||||||
|
expect.objectContaining({ identity: CONTROL }),
|
||||||
|
undefined,
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
it('uses one immutable subject credential handle for identity, permission, and receive-pack', async (): Promise<void> => {
|
it('uses one immutable subject credential handle for identity, permission, and receive-pack', async (): Promise<void> => {
|
||||||
const { result, observed } = await validate();
|
const { result, observed } = await validate();
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { CredentialProviderEvidenceError } from './gitea-provider.js';
|
import { CredentialProviderEvidenceError } from './gitea-provider.js';
|
||||||
import type {
|
import type {
|
||||||
CredentialValidationDependencies,
|
CredentialValidationDependencies,
|
||||||
GiteaCredentialProvider,
|
|
||||||
GiteaReadValidationRequestDto,
|
GiteaReadValidationRequestDto,
|
||||||
GiteaWriteValidationRequestDto,
|
GiteaWriteValidationRequestDto,
|
||||||
ResolvedCredential,
|
ResolvedCredential,
|
||||||
@@ -150,23 +149,9 @@ async function resolveCredential(
|
|||||||
return dependencies.resolver.resolve(identity, request.estate, request.host);
|
return dependencies.resolver.resolve(identity, request.estate, request.host);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readSubjectEvidence(
|
|
||||||
request: GiteaWriteValidationRequestDto,
|
|
||||||
resolved: ResolvedCredential,
|
|
||||||
provider: GiteaCredentialProvider,
|
|
||||||
): Promise<{
|
|
||||||
readonly identity: ProviderIdentityEvidenceDto;
|
|
||||||
readonly permission: RepositoryPermissionEvidenceDto;
|
|
||||||
readonly receivePack: ReceivePackEvidenceDto;
|
|
||||||
}> {
|
|
||||||
const identity = await provider.readIdentity(resolved);
|
|
||||||
const permission = await provider.readRepositoryPermission(resolved, request.repo);
|
|
||||||
const receivePack = await provider.probeReceivePack(resolved, request.repo);
|
|
||||||
return { identity, permission, receivePack };
|
|
||||||
}
|
|
||||||
|
|
||||||
function successfulEvidence(
|
function successfulEvidence(
|
||||||
subjectIdentity: ProviderIdentityEvidenceDto,
|
subjectLogin: string,
|
||||||
|
subjectIdentity: ProviderIdentityEvidenceDto | null,
|
||||||
subjectPermission: RepositoryPermissionEvidenceDto,
|
subjectPermission: RepositoryPermissionEvidenceDto,
|
||||||
subjectReceivePack: ReceivePackEvidenceDto,
|
subjectReceivePack: ReceivePackEvidenceDto,
|
||||||
controlIdentity: ProviderIdentityEvidenceDto,
|
controlIdentity: ProviderIdentityEvidenceDto,
|
||||||
@@ -176,7 +161,7 @@ function successfulEvidence(
|
|||||||
const writeDifferential: WriteDifferentialEvidenceDto = {
|
const writeDifferential: WriteDifferentialEvidenceDto = {
|
||||||
state: 'can-write',
|
state: 'can-write',
|
||||||
credentialBinding: 'same-resolution',
|
credentialBinding: 'same-resolution',
|
||||||
transportPrincipal: subjectIdentity.login,
|
transportPrincipal: subjectLogin,
|
||||||
authenticatedReceivePack: 'advertised',
|
authenticatedReceivePack: 'advertised',
|
||||||
readOnlyControl: {
|
readOnlyControl: {
|
||||||
identity: controlIdentity.login,
|
identity: controlIdentity.login,
|
||||||
@@ -353,69 +338,30 @@ async function evaluateGiteaWriteValidationUnsafe(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let subjectEvidence: Awaited<ReturnType<typeof readSubjectEvidence>>;
|
let subjectIdentity: ProviderIdentityEvidenceDto | null = null;
|
||||||
try {
|
try {
|
||||||
subjectEvidence = await readSubjectEvidence(request, resolved, dependencies.provider);
|
subjectIdentity = await dependencies.provider.readIdentity(resolved);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (
|
if (
|
||||||
error instanceof CredentialProviderEvidenceError &&
|
!(error instanceof CredentialProviderEvidenceError) ||
|
||||||
error.code === 'identity-read-forbidden'
|
error.code !== 'identity-read-forbidden'
|
||||||
) {
|
) {
|
||||||
const permission = await dependencies.provider.readRepositoryPermission(
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const subjectPermission = await dependencies.provider.readRepositoryPermission(
|
||||||
resolved,
|
resolved,
|
||||||
request.repo,
|
request.repo,
|
||||||
);
|
);
|
||||||
const receivePack = await dependencies.provider.probeReceivePack(resolved, request.repo);
|
const subjectReceivePack = await dependencies.provider.probeReceivePack(resolved, request.repo);
|
||||||
const evidence: CredentialValidationEvidenceDto = {
|
|
||||||
providerIdentity: null,
|
|
||||||
tokenCapabilities: RUNTIME_SCOPE_NOT_MEASURED,
|
|
||||||
repositoryPermission: permission,
|
|
||||||
writeDifferential: null,
|
|
||||||
};
|
|
||||||
if (permission.effective === 'none' || permission.effective === 'read') {
|
|
||||||
return refused(
|
|
||||||
request,
|
|
||||||
{
|
|
||||||
code: 'permission-denied',
|
|
||||||
message: 'The in-scope provider object denies required write capability.',
|
|
||||||
},
|
|
||||||
evidence,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
receivePack.principal !== request.identity ||
|
|
||||||
receivePack.resolutionId !== resolved.resolutionId ||
|
|
||||||
!advertised(receivePack)
|
|
||||||
) {
|
|
||||||
return indeterminate(
|
|
||||||
request,
|
|
||||||
{
|
|
||||||
code: 'permission-evidence-disagrees',
|
|
||||||
message: 'In-scope repository and write transport evidence did not agree.',
|
|
||||||
},
|
|
||||||
evidence,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return indeterminate(
|
|
||||||
request,
|
|
||||||
{
|
|
||||||
code: 'identity-not-measured',
|
|
||||||
message:
|
|
||||||
'Write capability was confirmed, but identity was not measured because this least-privilege token cannot read /user.',
|
|
||||||
},
|
|
||||||
evidence,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
const baseEvidence: CredentialValidationEvidenceDto = {
|
const baseEvidence: CredentialValidationEvidenceDto = {
|
||||||
providerIdentity: subjectEvidence.identity,
|
providerIdentity: subjectIdentity,
|
||||||
tokenCapabilities: RUNTIME_SCOPE_NOT_MEASURED,
|
tokenCapabilities: RUNTIME_SCOPE_NOT_MEASURED,
|
||||||
repositoryPermission: subjectEvidence.permission,
|
repositoryPermission: subjectPermission,
|
||||||
writeDifferential: null,
|
writeDifferential: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!identityContentTypeValid(subjectEvidence.identity)) {
|
if (subjectIdentity !== null && !identityContentTypeValid(subjectIdentity)) {
|
||||||
return indeterminate(
|
return indeterminate(
|
||||||
request,
|
request,
|
||||||
{
|
{
|
||||||
@@ -425,7 +371,7 @@ async function evaluateGiteaWriteValidationUnsafe(
|
|||||||
baseEvidence,
|
baseEvidence,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (subjectEvidence.identity.login !== request.identity) {
|
if (subjectIdentity !== null && subjectIdentity.login !== request.identity) {
|
||||||
return refused(
|
return refused(
|
||||||
request,
|
request,
|
||||||
{
|
{
|
||||||
@@ -435,7 +381,7 @@ async function evaluateGiteaWriteValidationUnsafe(
|
|||||||
baseEvidence,
|
baseEvidence,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!permissionContentTypeValid(subjectEvidence.permission)) {
|
if (!permissionContentTypeValid(subjectPermission)) {
|
||||||
return indeterminate(
|
return indeterminate(
|
||||||
request,
|
request,
|
||||||
{
|
{
|
||||||
@@ -445,7 +391,7 @@ async function evaluateGiteaWriteValidationUnsafe(
|
|||||||
baseEvidence,
|
baseEvidence,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (request.requiredPermission === 'admin' && subjectEvidence.permission.effective !== 'admin') {
|
if (request.requiredPermission === 'admin' && subjectPermission.effective !== 'admin') {
|
||||||
return refused(
|
return refused(
|
||||||
request,
|
request,
|
||||||
{
|
{
|
||||||
@@ -455,10 +401,7 @@ async function evaluateGiteaWriteValidationUnsafe(
|
|||||||
baseEvidence,
|
baseEvidence,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (subjectPermission.effective === 'read' || subjectPermission.effective === 'none') {
|
||||||
subjectEvidence.permission.effective === 'read' ||
|
|
||||||
subjectEvidence.permission.effective === 'none'
|
|
||||||
) {
|
|
||||||
return refused(
|
return refused(
|
||||||
request,
|
request,
|
||||||
{
|
{
|
||||||
@@ -469,8 +412,8 @@ async function evaluateGiteaWriteValidationUnsafe(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
subjectEvidence.receivePack.principal !== request.identity ||
|
subjectReceivePack.principal !== request.identity ||
|
||||||
subjectEvidence.receivePack.resolutionId !== resolved.resolutionId
|
subjectReceivePack.resolutionId !== resolved.resolutionId
|
||||||
) {
|
) {
|
||||||
return indeterminate(
|
return indeterminate(
|
||||||
request,
|
request,
|
||||||
@@ -481,7 +424,7 @@ async function evaluateGiteaWriteValidationUnsafe(
|
|||||||
baseEvidence,
|
baseEvidence,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!advertised(subjectEvidence.receivePack)) {
|
if (!advertised(subjectReceivePack)) {
|
||||||
return indeterminate(
|
return indeterminate(
|
||||||
request,
|
request,
|
||||||
{
|
{
|
||||||
@@ -544,17 +487,21 @@ async function evaluateGiteaWriteValidationUnsafe(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const evidence = successfulEvidence(
|
const evidence = successfulEvidence(
|
||||||
subjectEvidence.identity,
|
request.identity,
|
||||||
subjectEvidence.permission,
|
subjectIdentity,
|
||||||
subjectEvidence.receivePack,
|
subjectPermission,
|
||||||
|
subjectReceivePack,
|
||||||
controlIdentity,
|
controlIdentity,
|
||||||
controlPermission,
|
controlPermission,
|
||||||
controlReceivePack,
|
controlReceivePack,
|
||||||
);
|
);
|
||||||
return result(request, {
|
return result(request, {
|
||||||
outcome: 'ok',
|
outcome: subjectIdentity === null ? 'indeterminate' : 'ok',
|
||||||
code: 'validation-verified',
|
code: subjectIdentity === null ? 'identity-not-measured' : 'validation-verified',
|
||||||
message: 'Every required provider evidence layer agreed.',
|
message:
|
||||||
|
subjectIdentity === null
|
||||||
|
? 'Write capability and both controls were confirmed, but identity was not measured because this least-privilege token cannot read /user.'
|
||||||
|
: 'Every required provider evidence layer agreed.',
|
||||||
evidence,
|
evidence,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user