blob: 7c07e49552072f47195883949fb8e91097d3927e [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { invalidProtocolFrame } from './errors.js';
import {
requireExactRecord,
requireId,
requireShapedRecord,
requireString,
requireUtf8String,
} from './codec.js';
import { defineOperation } from './operation-spec.js';
import type { OperationKey } from './operations.js';
export const ACCESS_CREDENTIAL_MAX_GRANTS = 256;
export type AccessCredentialPrincipalKind =
| 'remote_owner'
| 'capability_provider'
| 'session_guest';
export type ManagedAccessCredentialPrincipalKind = Exclude<
AccessCredentialPrincipalKind,
'session_guest'
>;
export interface ClientCapabilityOwnerIdentity {
readonly principalId: string;
readonly clientInstanceId: string;
}
const ACCESS_ERRORS = [
'host_not_ready',
'host_draining',
'operation_unavailable',
'invalid_request',
'persistence_failed',
'commit_outcome_unknown',
'internal_failure',
] as const;
export interface AccessCredentialIssueInput {
readonly principalKind: ManagedAccessCredentialPrincipalKind;
readonly principalId: string;
readonly operationGrants: readonly OperationKey[];
readonly canPublishClientCapabilities: boolean;
readonly canUseHostPaths: boolean;
readonly capabilityOwnerCredentialId?: string;
}
export interface AccessCredentialIssueResult {
readonly credentialId: string;
readonly deliveryId: string;
readonly principalKind: ManagedAccessCredentialPrincipalKind;
readonly principalId: string;
readonly operationGrants: readonly OperationKey[];
readonly canPublishClientCapabilities: boolean;
readonly canUseHostPaths: boolean;
readonly capabilityOwner?: ClientCapabilityOwnerIdentity;
}
export type AccessCredentialReplaceInput = AccessCredentialIssueInput;
export type AccessCredentialReplaceResult = AccessCredentialIssueResult;
export interface AccessCredentialPrepareInput
extends Omit<AccessCredentialIssueInput, 'capabilityOwnerCredentialId'> {
readonly bindClientInstance?: boolean;
}
export type AccessCredentialPrepareResult = AccessCredentialIssueResult;
export interface AccessCredentialRevokeInput {
readonly credentialId: string;
}
export interface AccessPrincipalRevokeInput {
readonly principalKind: AccessCredentialPrincipalKind;
readonly principalId: string;
}
export interface AccessPrincipalRevokeResult {
readonly revoked: boolean;
}
export interface AccessCredentialRotationPrepareInput {
readonly replacementOfCredentialId: string;
}
export type AccessCredentialRotationPrepareResult = AccessCredentialPrepareResult;
export interface AccessCredentialRotationRevokeInput {
readonly credentialId: string;
readonly requiredActiveCredentialId: string;
}
export type AccessCredentialRotationRevokeResult = AccessCredentialRevokeResult;
export interface AccessCredentialRevokeResult {
readonly credentialId: string;
readonly revoked: boolean;
}
export type AccessCredentialFinalizeInput = Record<string, never>;
export interface AccessCredentialFinalizeResult {
readonly reconnectRequired: boolean;
}
export const ACCESS_AUTHORITY_OPERATION_SPECS = {
'access.credential.issue': defineOperation<
AccessCredentialIssueInput,
AccessCredentialIssueResult,
(typeof ACCESS_ERRORS)[number]
>({
mode: 'command',
availability: 'ready',
errors: ACCESS_ERRORS,
decodeInput: decodeAccessCredentialIssueInput,
decodeOutput: decodeAccessCredentialIssueResult,
}),
'access.credential.replace': defineOperation<
AccessCredentialReplaceInput,
AccessCredentialReplaceResult,
(typeof ACCESS_ERRORS)[number]
>({
mode: 'command',
availability: 'ready',
errors: ACCESS_ERRORS,
decodeInput: decodeAccessCredentialIssueInput,
decodeOutput: decodeAccessCredentialIssueResult,
}),
'access.credential.prepare': defineOperation<
AccessCredentialPrepareInput,
AccessCredentialPrepareResult,
(typeof ACCESS_ERRORS)[number]
>({
mode: 'command',
availability: 'ready',
errors: ACCESS_ERRORS,
decodeInput: decodeAccessCredentialPrepareInput,
decodeOutput: decodeAccessCredentialIssueResult,
}),
'access.credential.revoke': defineOperation<
AccessCredentialRevokeInput,
AccessCredentialRevokeResult,
(typeof ACCESS_ERRORS)[number]
>({
mode: 'command',
availability: 'ready',
errors: ACCESS_ERRORS,
decodeInput: decodeAccessCredentialRevokeInput,
decodeOutput: decodeAccessCredentialRevokeResult,
}),
'access.principal.revoke': defineOperation<
AccessPrincipalRevokeInput,
AccessPrincipalRevokeResult,
(typeof ACCESS_ERRORS)[number]
>({
mode: 'command',
availability: 'ready',
errors: ACCESS_ERRORS,
decodeInput: decodeAccessPrincipalRevokeInput,
decodeOutput: decodeAccessPrincipalRevokeResult,
}),
'access.credential.rotation.prepare': defineOperation<
AccessCredentialRotationPrepareInput,
AccessCredentialRotationPrepareResult,
(typeof ACCESS_ERRORS)[number]
>({
mode: 'command',
availability: 'ready',
errors: ACCESS_ERRORS,
decodeInput: decodeAccessCredentialRotationPrepareInput,
decodeOutput: decodeAccessCredentialIssueResult,
}),
'access.credential.rotation.revoke': defineOperation<
AccessCredentialRotationRevokeInput,
AccessCredentialRotationRevokeResult,
(typeof ACCESS_ERRORS)[number]
>({
mode: 'command',
availability: 'ready',
errors: ACCESS_ERRORS,
decodeInput: decodeAccessCredentialRotationRevokeInput,
decodeOutput: decodeAccessCredentialRevokeResult,
}),
'access.credential.finalize': defineOperation<
AccessCredentialFinalizeInput,
AccessCredentialFinalizeResult,
(typeof ACCESS_ERRORS)[number]
>({
mode: 'command',
availability: 'ready',
errors: ACCESS_ERRORS,
decodeInput: decodeAccessCredentialFinalizeInput,
decodeOutput: decodeAccessCredentialFinalizeResult,
}),
} as const;
export function decodeAccessCredentialIssueInput(value: unknown): AccessCredentialIssueInput {
const record = requireShapedRecord(
value,
'access credential issue input',
[
'principalKind',
'principalId',
'operationGrants',
'canPublishClientCapabilities',
'canUseHostPaths',
],
['capabilityOwnerCredentialId'],
);
const decodedPrincipalKind = principalKind(record.principalKind);
const capabilityOwnerCredentialId =
record.capabilityOwnerCredentialId === undefined
? undefined
: requireId(record.capabilityOwnerCredentialId, 'capabilityOwnerCredentialId');
if (capabilityOwnerCredentialId && decodedPrincipalKind !== 'capability_provider') {
throw invalidProtocolFrame('Only a capability provider credential may declare a Client owner');
}
return {
principalKind: decodedPrincipalKind,
principalId: principalId(record.principalId),
operationGrants: operationGrants(record.operationGrants),
canPublishClientCapabilities: boolean(
record.canPublishClientCapabilities,
'canPublishClientCapabilities',
),
canUseHostPaths: boolean(record.canUseHostPaths, 'canUseHostPaths'),
...(capabilityOwnerCredentialId ? { capabilityOwnerCredentialId } : {}),
};
}
export function decodeAccessCredentialPrepareInput(value: unknown): AccessCredentialPrepareInput {
const record = requireShapedRecord(
value,
'access credential prepare input',
[
'principalKind',
'principalId',
'operationGrants',
'canPublishClientCapabilities',
'canUseHostPaths',
],
['bindClientInstance'],
);
return {
principalKind: principalKind(record.principalKind),
principalId: principalId(record.principalId),
operationGrants: operationGrants(record.operationGrants),
canPublishClientCapabilities: boolean(
record.canPublishClientCapabilities,
'canPublishClientCapabilities',
),
canUseHostPaths: boolean(record.canUseHostPaths, 'canUseHostPaths'),
...(record.bindClientInstance === undefined
? {}
: { bindClientInstance: boolean(record.bindClientInstance, 'bindClientInstance') }),
};
}
export function decodeAccessCredentialIssueResult(value: unknown): AccessCredentialIssueResult {
const record = requireShapedRecord(
value,
'access credential issue result',
[
'credentialId',
'deliveryId',
'principalKind',
'principalId',
'operationGrants',
'canPublishClientCapabilities',
'canUseHostPaths',
],
['capabilityOwner'],
);
const decodedPrincipalKind = principalKind(record.principalKind);
const capabilityOwner =
record.capabilityOwner === undefined
? undefined
: clientCapabilityOwnerIdentity(record.capabilityOwner);
if (capabilityOwner && decodedPrincipalKind !== 'capability_provider') {
throw invalidProtocolFrame('Only a capability provider credential may declare a Client owner');
}
return {
credentialId: requireId(record.credentialId, 'credentialId'),
deliveryId: requireId(record.deliveryId, 'deliveryId'),
principalKind: decodedPrincipalKind,
principalId: principalId(record.principalId),
operationGrants: operationGrants(record.operationGrants),
canPublishClientCapabilities: boolean(
record.canPublishClientCapabilities,
'canPublishClientCapabilities',
),
canUseHostPaths: boolean(record.canUseHostPaths, 'canUseHostPaths'),
...(capabilityOwner ? { capabilityOwner } : {}),
};
}
function principalKind(value: unknown): ManagedAccessCredentialPrincipalKind {
if (value !== 'remote_owner' && value !== 'capability_provider') {
throw invalidProtocolFrame('Invalid access credential principalKind');
}
return value;
}
function revocablePrincipalKind(value: unknown): AccessCredentialPrincipalKind {
if (value !== 'remote_owner' && value !== 'capability_provider' && value !== 'session_guest') {
throw invalidProtocolFrame('Invalid access credential principalKind');
}
return value;
}
export function decodeAccessCredentialRevokeInput(value: unknown): AccessCredentialRevokeInput {
const record = requireExactRecord(value, 'access credential revoke input', ['credentialId']);
return { credentialId: requireId(record.credentialId, 'credentialId') };
}
export function decodeAccessPrincipalRevokeInput(value: unknown): AccessPrincipalRevokeInput {
const record = requireExactRecord(value, 'access principal revoke input', [
'principalKind',
'principalId',
]);
return {
principalKind: revocablePrincipalKind(record.principalKind),
principalId: principalId(record.principalId),
};
}
export function decodeAccessCredentialRotationPrepareInput(
value: unknown,
): AccessCredentialRotationPrepareInput {
const record = requireExactRecord(value, 'access credential rotation prepare input', [
'replacementOfCredentialId',
]);
return {
replacementOfCredentialId: requireId(
record.replacementOfCredentialId,
'replacementOfCredentialId',
),
};
}
export function decodeAccessCredentialRotationRevokeInput(
value: unknown,
): AccessCredentialRotationRevokeInput {
const record = requireExactRecord(value, 'access credential rotation revoke input', [
'credentialId',
'requiredActiveCredentialId',
]);
return {
credentialId: requireId(record.credentialId, 'credentialId'),
requiredActiveCredentialId: requireId(
record.requiredActiveCredentialId,
'requiredActiveCredentialId',
),
};
}
export function decodeAccessCredentialRevokeResult(value: unknown): AccessCredentialRevokeResult {
const record = requireExactRecord(value, 'access credential revoke result', [
'credentialId',
'revoked',
]);
return {
credentialId: requireId(record.credentialId, 'credentialId'),
revoked: boolean(record.revoked, 'revoked'),
};
}
export function decodeAccessPrincipalRevokeResult(value: unknown): AccessPrincipalRevokeResult {
const record = requireExactRecord(value, 'access principal revoke result', ['revoked']);
return { revoked: boolean(record.revoked, 'revoked') };
}
export function decodeAccessCredentialFinalizeInput(value: unknown): AccessCredentialFinalizeInput {
requireExactRecord(value, 'access credential finalize input', []);
return {};
}
export function decodeAccessCredentialFinalizeResult(
value: unknown,
): AccessCredentialFinalizeResult {
const record = requireExactRecord(value, 'access credential finalize result', [
'reconnectRequired',
]);
return { reconnectRequired: boolean(record.reconnectRequired, 'reconnectRequired') };
}
function operationGrants(value: unknown): readonly OperationKey[] {
if (!Array.isArray(value) || value.length > ACCESS_CREDENTIAL_MAX_GRANTS) {
throw invalidProtocolFrame('Invalid access credential operation grants');
}
const grants = value.map((grant) =>
requireString(grant, 'access credential operation grant', 128),
);
if (new Set(grants).size !== grants.length) {
throw invalidProtocolFrame('Duplicate access credential operation grant');
}
return grants as OperationKey[];
}
function principalId(value: unknown): string {
const principal = requireUtf8String(value, 'access credential principalId', 128);
if (!/^[A-Za-z0-9_.:-]+$/u.test(principal)) {
throw invalidProtocolFrame('Invalid access credential principalId');
}
return principal;
}
function clientCapabilityOwnerIdentity(value: unknown): ClientCapabilityOwnerIdentity {
const record = requireExactRecord(value, 'Client Capability owner identity', [
'principalId',
'clientInstanceId',
]);
return {
principalId: principalId(record.principalId),
clientInstanceId: requireId(record.clientInstanceId, 'clientInstanceId'),
};
}
function boolean(value: unknown, label: string): boolean {
if (typeof value !== 'boolean') throw invalidProtocolFrame(`Invalid ${label}`);
return value;
}