Compare commits

...
Author SHA1 Message Date
jason.woltje 257a156845 feat(organization): add grantable org.hire right (#1873) 2026-11-10 15:34:55 -06:00
2 changed files with 29 additions and 2 deletions

No files matched your search

+2
View File
@@ -10,6 +10,7 @@ export const RIGHTS = record({
'board.read': flag(true),
'task.write': flag(true),
decide: flag(true),
'org.hire': flag(true),
'request.write': flag(false),
'members.manage': flag(false),
'org.setup': flag(false),
@@ -65,6 +66,7 @@ export const VERB_RIGHTS = record({
'tasks.delete': null,
'decision.preview.console': 'decide',
'decision.resolve.console': 'decide',
'hire.resolve.console': 'org.hire',
'request.answer.console': 'request.write',
'request.attach.console': 'request.write',
'request.cancel.console': 'request.write',
@@ -12,13 +12,14 @@ const RIGHTS = {
'board.read': { grantable: true },
'task.write': { grantable: true },
decide: { grantable: true },
'org.hire': { grantable: true },
'request.write': { grantable: false },
'members.manage': { grantable: false },
'org.setup': { grantable: false },
'org.lifecycle': { grantable: false },
};
const BUNDLES = {
owner: ['board.read', 'decide', 'members.manage', 'org.lifecycle', 'org.setup', 'org.view', 'request.write', 'task.write', 'thread.read', 'thread.write'],
owner: ['board.read', 'decide', 'members.manage', 'org.hire', 'org.lifecycle', 'org.setup', 'org.view', 'request.write', 'task.write', 'thread.read', 'thread.write'],
member: ['board.read', 'org.view', 'thread.read'],
collaborator: ['board.read', 'org.view', 'task.write', 'thread.read', 'thread.write'],
approver: ['board.read', 'decide', 'org.view', 'task.write', 'thread.read', 'thread.write'],
@@ -58,6 +59,7 @@ const VERBS = {
'tasks.delete': null,
'decision.preview.console': 'decide',
'decision.resolve.console': 'decide',
'hire.resolve.console': 'org.hire',
'request.answer.console': 'request.write',
'request.attach.console': 'request.write',
'request.cancel.console': 'request.write',
@@ -70,10 +72,32 @@ const VERBS = {
'project.remote.set.console': 'org.setup',
};
test('M1 the ten rights pin the grantable flags, including compulsory org.view', () => {
test('M1/R1 the eleven rights pin the grantable flags, including compulsory org.view', () => {
assert.deepEqual({ ...policy.RIGHTS }, RIGHTS);
});
test('R1 org.hire belongs to owner and no non-owner template', () => {
assert.ok(policy.ROLE_RIGHTS.owner.includes('org.hire'));
for (const role of ['member', 'collaborator', 'approver']) {
assert.ok(!policy.ROLE_RIGHTS[role].includes('org.hire'), role);
}
});
test('R1 an owner can grant org.hire to a member without widening the template', () => {
const rights = policy.normalizeRights(policy.ROLE_RIGHTS.member, { add: ['org.hire'] });
assert.deepEqual(rights, ['board.read', 'org.hire', 'org.view', 'thread.read']);
assert.equal(policy.canGrant(policy.ROLE_RIGHTS.owner, rights), true);
assert.equal(policy.canGrant(policy.ROLE_RIGHTS.member, ['org.hire']), false);
assert.equal(policy.canGrant(policy.ROLE_RIGHTS.owner, ['org.hire', 'future.right']), false);
assert.throws(() => policy.normalizeRights(rights, { add: ['future.right'] }), { code: 'unknown-right' });
assert.deepEqual(policy.ROLE_RIGHTS.member, ['board.read', 'org.view', 'thread.read']);
});
test('R1 hire resolution requires org.hire while generic decision resolution keeps decide', () => {
assert.equal(policy.VERB_RIGHTS['hire.resolve.console'], 'org.hire');
assert.equal(policy.VERB_RIGHTS['decision.resolve.console'], 'decide');
});
for (const [role, rights] of Object.entries(BUNDLES)) {
test(`M1 ${role} has exactly its reviewed rights, expanded and sorted`, () => {
assert.deepEqual(policy.ROLE_RIGHTS[role], rights);
@@ -161,6 +185,7 @@ test('M1 every actual console, user, board and person-task verb has a reviewed a
}
assert.deepEqual(Object.keys(VERBS).sort(), [...new Set([
...CONSOLE_VERBS, ...USER_VERBS, ...BOARD_VERBS, ...PERSON_TASK_VERBS, 'plugins', 'plugins.set', 'limits',
'hire.resolve.console', // R1 reserves the policy mapping; A4 implements the verb.
])].sort());
});