Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core/src/delegation/delegation-offer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export async function createDelegationOffer({
assertPolicyConformsToParent(delegationPolicy, parentDetails.delegationPolicy, {
delegationRole,
remainingDepth: parentDetails.remainingDelegationDepth,
parentRoleId: parentDetails.role?.roleId,
});
}
}
Expand Down Expand Up @@ -329,6 +330,7 @@ export const DELEGATION_REQUEST_HANDLER = {
{
delegationRole: delegationOffer.delegationRole,
remainingDepth: parentDetails.remainingDelegationDepth,
parentRoleId: parentDetails.role?.roleId,
},
);
}
Expand Down
44 changes: 44 additions & 0 deletions packages/core/src/delegation/delegation-policy-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,48 @@ describe('assertPolicyConformsToParent', () => {
}),
).toThrow(/items\.enum is not a subset/);
});

it('rejects delegating to the same role when nestedRoleOnly is set on the parent', () => {
const parent = clonePolicy(delegationPolicyTravelAgent);
parent.ruleset.overallConstraints.nestedRoleOnly = true;
expect(() =>
assertPolicyConformsToParent(clonePolicy(delegationPolicyTravelAgent), parent, {
...baseOpts,
parentRoleId: PARENT_ROLE,
}),
).toThrow(/nestedRoleOnly/);
});

it('allows delegating to a descendant role when nestedRoleOnly is set', () => {
const CHILD_ROLE = delegationPolicyTravelAgent.ruleset.roles.find(
r => r.parentRoleId === PARENT_ROLE,
)!.roleId;
const parent = clonePolicy(delegationPolicyTravelAgent);
parent.ruleset.overallConstraints.nestedRoleOnly = true;
expect(() =>
assertPolicyConformsToParent(clonePolicy(delegationPolicyTravelAgent), parent, {
delegationRole: CHILD_ROLE,
remainingDepth: 3,
parentRoleId: PARENT_ROLE,
}),
).not.toThrow();
});

it('ignores the same-role check when nestedRoleOnly is absent (back-compat)', () => {
expect(() =>
assertPolicyConformsToParent(
clonePolicy(delegationPolicyTravelAgent),
delegationPolicyTravelAgent,
{...baseOpts, parentRoleId: PARENT_ROLE},
),
).not.toThrow();
});

it('ignores the same-role check when parentRoleId is omitted', () => {
const parent = clonePolicy(delegationPolicyTravelAgent);
parent.ruleset.overallConstraints.nestedRoleOnly = true;
expect(() =>
assertPolicyConformsToParent(clonePolicy(delegationPolicyTravelAgent), parent, baseOpts),
).not.toThrow();
});
});
10 changes: 9 additions & 1 deletion packages/core/src/delegation/delegation-policy-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,10 +225,18 @@ export function assertPolicyConformsToParent(
{
delegationRole,
remainingDepth,
}: {delegationRole: string; remainingDepth: number},
parentRoleId,
}: {delegationRole: string; remainingDepth: number; parentRoleId?: string},
) {
assert(remainingDepth > 0, 'parent credential has no remaining delegation depth');

if (parentPolicy.ruleset.overallConstraints.nestedRoleOnly && parentRoleId) {
assert(
delegationRole !== parentRoleId,
`nestedRoleOnly: cannot delegate role "${delegationRole}" to the same role`,
);
}

const childConstraints = policy.ruleset.overallConstraints;
const parentConstraints = parentPolicy.ruleset.overallConstraints;

Expand Down
28 changes: 28 additions & 0 deletions packages/core/src/delegation/delegation-policy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {delegationPolicyTravelAgent} from './delegation-fixtures';
import {buildDelegationRoleTree, getRoleNodeById} from './delegation-tree';
import {getDelegationOptions} from './delegation-policy';

const CORPORATE_ACCOUNT_MANAGER = '8e5abc88-7006-42ae-ae48-9e34f8f66124';
const BOOKING_SPECIALIST = '6375baa1-a52d-4838-9100-3facea02ba49';

describe('getDelegationOptions', () => {
const tree = buildDelegationRoleTree(delegationPolicyTravelAgent);

it('returns only the holder subtree, not the whole tree', () => {
const holder = getRoleNodeById(CORPORATE_ACCOUNT_MANAGER, tree)!;
const ids = getDelegationOptions(holder).map(r => r.roleId);

expect(ids).toEqual([
BOOKING_SPECIALIST,
'16f68474-bf3b-4494-9fe5-f141a7d74a33',
'c1bd8821-c645-4dd6-ab07-9bc087755db9',
]);
expect(ids).not.toContain('9726317c-cb60-4ae7-a828-e334b10f6f52');
expect(ids).not.toContain(CORPORATE_ACCOUNT_MANAGER);
});

it('returns an empty list for a leaf holder', () => {
const leaf = getRoleNodeById(BOOKING_SPECIALIST, tree)!;
expect(getDelegationOptions(leaf)).toEqual([]);
});
});
9 changes: 5 additions & 4 deletions packages/core/src/delegation/delegation-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,14 @@ export function getRole(
return delegationPolicy.ruleset.roles.find(r => r.roleId === roleId);
}

export function getDelegationOptions(roleTree: RoleNode): RoleNode[] {
export function getDelegationOptions(holderNode: RoleNode): RoleNode[] {
const roles: RoleNode[] = [];
const traverse = (node: RoleNode) => {
roles.push(node);
node.children?.forEach(traverse);
};

roleTree.children?.forEach(traverse);
holderNode.children?.forEach(traverse);

return roles;
}
Expand All @@ -77,8 +77,9 @@ export async function getDelegationDetails(
const delegationChain = await getDelegationChain(credential, wallet);
const policy = await fetchDelegationPolicyJson(credential);
const roleTree = buildDelegationRoleTree(policy);
const role = getRoleNodeById(roleTree.roleId, roleTree);
const delegationOptions = getDelegationOptions(roleTree);
const role = getRoleNodeById(credential.roleId, roleTree);
assert(role, `credential role ${credential.roleId} not found in policy`);
const delegationOptions = getDelegationOptions(role);
const remainingDelegationDepth = getRemainingDelegationDepth(role, policy);

return {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/delegation/delegation-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type DelegatedCredentialLifetime = {
export type OverallConstraints = {
maxDelegationDepth: number;
delegatedCredentialLifetime: DelegatedCredentialLifetime;
nestedRoleOnly?: boolean;
};

export type Ruleset = {
Expand Down
Loading