@things-factory/auth-base 10.1.192 → 10.1.202
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/ai-entry/access-assistant.js +4 -3
- package/dist-server/ai-entry/access-assistant.js.map +1 -1
- package/dist-server/controllers/access-request.d.ts +8 -2
- package/dist-server/controllers/access-request.js +33 -6
- package/dist-server/controllers/access-request.js.map +1 -1
- package/dist-server/controllers/signin.d.ts +5 -0
- package/dist-server/controllers/signin.js +25 -0
- package/dist-server/controllers/signin.js.map +1 -1
- package/dist-server/index.js +7 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/middlewares/audit-scope-middleware.d.ts +7 -0
- package/dist-server/middlewares/audit-scope-middleware.js +15 -0
- package/dist-server/middlewares/audit-scope-middleware.js.map +1 -0
- package/dist-server/middlewares/index.js +3 -0
- package/dist-server/middlewares/index.js.map +1 -1
- package/dist-server/role-tasks.d.ts +2 -0
- package/dist-server/role-tasks.js +20 -0
- package/dist-server/role-tasks.js.map +1 -0
- package/dist-server/router/auth-checkin-router.js +3 -5
- package/dist-server/router/auth-checkin-router.js.map +1 -1
- package/dist-server/router/auth-public-process-router.js +7 -1
- package/dist-server/router/auth-public-process-router.js.map +1 -1
- package/dist-server/router/site-root-router.d.ts +2 -0
- package/dist-server/router/site-root-router.js +2 -0
- package/dist-server/router/site-root-router.js.map +1 -1
- package/dist-server/service/access-request/access-request-query.d.ts +7 -2
- package/dist-server/service/access-request/access-request-query.js +25 -4
- package/dist-server/service/access-request/access-request-query.js.map +1 -1
- package/dist-server/service/access-request/access-request-types.d.ts +7 -0
- package/dist-server/service/access-request/access-request-types.js +27 -1
- package/dist-server/service/access-request/access-request-types.js.map +1 -1
- package/dist-server/service/access-request/access-request.d.ts +1 -0
- package/dist-server/service/access-request/access-request.js +5 -0
- package/dist-server/service/access-request/access-request.js.map +1 -1
- package/dist-server/service/audit/audit-action.d.ts +36 -0
- package/dist-server/service/audit/audit-action.js +137 -0
- package/dist-server/service/audit/audit-action.js.map +1 -0
- package/dist-server/service/audit/audit-kinds.d.ts +19 -0
- package/dist-server/service/audit/audit-kinds.js +70 -0
- package/dist-server/service/audit/audit-kinds.js.map +1 -0
- package/dist-server/service/audit/audit-query.d.ts +11 -0
- package/dist-server/service/audit/audit-query.js +295 -0
- package/dist-server/service/audit/audit-query.js.map +1 -0
- package/dist-server/service/audit/audit-recorder.d.ts +77 -0
- package/dist-server/service/audit/audit-recorder.js +247 -0
- package/dist-server/service/audit/audit-recorder.js.map +1 -0
- package/dist-server/service/audit/audit-request.d.ts +28 -0
- package/dist-server/service/audit/audit-request.js +96 -0
- package/dist-server/service/audit/audit-request.js.map +1 -0
- package/dist-server/service/audit/audit-retention.d.ts +12 -0
- package/dist-server/service/audit/audit-retention.js +42 -0
- package/dist-server/service/audit/audit-retention.js.map +1 -0
- package/dist-server/service/audit/audit-subscriber.d.ts +39 -0
- package/dist-server/service/audit/audit-subscriber.js +191 -0
- package/dist-server/service/audit/audit-subscriber.js.map +1 -0
- package/dist-server/service/audit/audit-target.d.ts +19 -0
- package/dist-server/service/audit/audit-target.js +46 -0
- package/dist-server/service/audit/audit-target.js.map +1 -0
- package/dist-server/service/audit/audit-types.d.ts +44 -0
- package/dist-server/service/audit/audit-types.js +161 -0
- package/dist-server/service/audit/audit-types.js.map +1 -0
- package/dist-server/service/audit/index.d.ts +6 -0
- package/dist-server/service/audit/index.js +11 -0
- package/dist-server/service/audit/index.js.map +1 -0
- package/dist-server/service/domain-owner/domain-owner-mutation.js +3 -0
- package/dist-server/service/domain-owner/domain-owner-mutation.js.map +1 -1
- package/dist-server/service/index.d.ts +9 -2
- package/dist-server/service/index.js +19 -4
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/open-fields.js +2 -1
- package/dist-server/service/open-fields.js.map +1 -1
- package/dist-server/service/privilege/privilege-directive.js +25 -1
- package/dist-server/service/privilege/privilege-directive.js.map +1 -1
- package/dist-server/service/role/role-mutation.js +8 -1
- package/dist-server/service/role/role-mutation.js.map +1 -1
- package/dist-server/service/role-template/role-task.d.ts +81 -0
- package/dist-server/service/role-template/role-task.js +129 -0
- package/dist-server/service/role-template/role-task.js.map +1 -0
- package/dist-server/service/role-template/role-template-gap-resolver.d.ts +9 -5
- package/dist-server/service/role-template/role-template-gap-resolver.js +41 -13
- package/dist-server/service/role-template/role-template-gap-resolver.js.map +1 -1
- package/dist-server/service/role-template/role-template-mutation.js +10 -1
- package/dist-server/service/role-template/role-template-mutation.js.map +1 -1
- package/dist-server/service/role-template/role-template-query.js +4 -2
- package/dist-server/service/role-template/role-template-query.js.map +1 -1
- package/dist-server/service/role-template/role-template-types.d.ts +24 -0
- package/dist-server/service/role-template/role-template-types.js +63 -1
- package/dist-server/service/role-template/role-template-types.js.map +1 -1
- package/dist-server/service/role-template/role-template.d.ts +63 -10
- package/dist-server/service/role-template/role-template.js +92 -18
- package/dist-server/service/role-template/role-template.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/utils/access-request-review.d.ts +12 -2
- package/dist-server/utils/access-request-review.js +13 -3
- package/dist-server/utils/access-request-review.js.map +1 -1
- package/dist-server/utils/check-permission.d.ts +6 -0
- package/dist-server/utils/check-permission.js +23 -8
- package/dist-server/utils/check-permission.js.map +1 -1
- package/package.json +3 -2
- package/tests/access-assistant-db.test.ts +1 -1
- package/tests/access-request-db.test.ts +71 -0
- package/tests/access-request-review.test.ts +30 -0
- package/tests/audit-db.test.ts +394 -0
- package/tests/auth-base-role-tasks.test.ts +58 -0
- package/tests/list-inheritance-db.test.ts +0 -7
- package/tests/open-doors.test.ts +2 -2
- package/tests/refusal-cause.test.ts +1 -1
- package/tests/role-task.test.ts +168 -0
- package/tests/role-template-gap-db.test.ts +77 -2
- package/translations/en.json +14 -1
- package/translations/es.json +14 -1
- package/translations/ja.json +14 -1
- package/translations/ko.json +14 -1
- package/translations/ms.json +14 -1
- package/translations/zh.json +14 -1
- package/dist-server/service/login-history/index.d.ts +0 -4
- package/dist-server/service/login-history/index.js +0 -8
- package/dist-server/service/login-history/index.js.map +0 -1
- package/dist-server/service/login-history/login-history-query.d.ts +0 -10
- package/dist-server/service/login-history/login-history-query.js +0 -80
- package/dist-server/service/login-history/login-history-query.js.map +0 -1
- package/dist-server/service/login-history/login-history-type.d.ts +0 -5
- package/dist-server/service/login-history/login-history-type.js +0 -21
- package/dist-server/service/login-history/login-history-type.js.map +0 -1
- package/dist-server/service/login-history/login-history.d.ts +0 -12
- package/dist-server/service/login-history/login-history.js +0 -58
- package/dist-server/service/login-history/login-history.js.map +0 -1
|
@@ -36,12 +36,18 @@ export interface DecisionFacts {
|
|
|
36
36
|
} | null;
|
|
37
37
|
/** The requester holds the privilege now - through some other role, since the request was made. */
|
|
38
38
|
requesterHoldsIt: boolean;
|
|
39
|
-
/** Every assignable role that grants the privilege, with how many privileges each carries. */
|
|
39
|
+
/** Every assignable role that grants the privilege, with how many privileges each carries and whether it holds the task asked. */
|
|
40
40
|
candidates: {
|
|
41
41
|
id: string;
|
|
42
42
|
name: string;
|
|
43
43
|
privilegeCount: number;
|
|
44
|
+
holdsTask?: boolean;
|
|
44
45
|
}[];
|
|
46
|
+
/** The task the requester asked for, with its declared privileges - null when none was asked or it is gone (ADR-0103 decision 9). */
|
|
47
|
+
task?: {
|
|
48
|
+
id: string;
|
|
49
|
+
privileges: PrivilegeName[];
|
|
50
|
+
} | null;
|
|
45
51
|
/** The decider's own privileges - to tell them when a role gives what they do not have. */
|
|
46
52
|
deciderPrivileges: PrivilegeName[];
|
|
47
53
|
/** Whether the decider is a domain owner - an owner may give anything. */
|
|
@@ -64,9 +70,13 @@ export interface DecisionIssue {
|
|
|
64
70
|
export declare function effectiveState(state: AccessRequestState, expiresAt: Date | string | null | undefined, now: Date): AccessRequestState;
|
|
65
71
|
export declare function expiryFrom(now: Date, days?: number): Date;
|
|
66
72
|
export declare function reviewAccessDecision(facts: DecisionFacts): DecisionIssue[];
|
|
67
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* The candidate the draft card proposes by default - one holding the task asked, if any does, then the smallest. Ties go
|
|
75
|
+
* by name, so the answer is the same every time; the order is `rolesGranting`'s.
|
|
76
|
+
*/
|
|
68
77
|
export declare function smallestRole<T extends {
|
|
69
78
|
name: string;
|
|
70
79
|
privilegeCount: number;
|
|
80
|
+
holdsTask?: boolean;
|
|
71
81
|
}>(candidates: T[]): T | undefined;
|
|
72
82
|
export declare const isBlocked: (issues: DecisionIssue[]) => boolean;
|
|
@@ -95,11 +95,18 @@ function grantIssues(facts, asked) {
|
|
|
95
95
|
params: { role: role.name, count: more.length, privileges: more.map(nameOf).join(', ') }
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
|
+
/* Told, not refused: the role opens the refused door, and the rest of the work asked for is the decider's call. */
|
|
99
|
+
const missingOfTask = (facts.task?.privileges || []).filter(p => !role.privileges.some(r => same(r, p)));
|
|
100
|
+
if (facts.task && missingOfTask.length > 0) {
|
|
101
|
+
issues.push({ code: 'role-lacks-task', blocking: false, field: 'role', params: { role: role.name, task: facts.task.id, privileges: missingOfTask.map(nameOf).join(', ') } });
|
|
102
|
+
}
|
|
98
103
|
if (!same(asked, exports.ROLE_GRANTING_PRIVILEGE) && role.privileges.some(p => same(p, exports.ROLE_GRANTING_PRIVILEGE))) {
|
|
99
104
|
issues.push({ code: 'role-grants-admin', blocking: false, field: 'role', params: { role: role.name } });
|
|
100
105
|
}
|
|
106
|
+
/* A smaller role that drops the task asked for is not a better answer - only one holding it as the chosen does counts. */
|
|
107
|
+
const choiceHoldsTask = !!facts.task && missingOfTask.length === 0;
|
|
101
108
|
const smaller = facts.candidates
|
|
102
|
-
.filter(c => c.id !== role.id && c.privilegeCount < role.privileges.length)
|
|
109
|
+
.filter(c => c.id !== role.id && c.privilegeCount < role.privileges.length && (!choiceHoldsTask || c.holdsTask))
|
|
103
110
|
.sort((a, b) => a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0];
|
|
104
111
|
if (smaller) {
|
|
105
112
|
issues.push({ code: 'smaller-role-exists', blocking: false, field: 'role', params: { role: smaller.name, count: smaller.privilegeCount } });
|
|
@@ -113,9 +120,12 @@ function grantIssues(facts, asked) {
|
|
|
113
120
|
}
|
|
114
121
|
return issues;
|
|
115
122
|
}
|
|
116
|
-
/**
|
|
123
|
+
/**
|
|
124
|
+
* The candidate the draft card proposes by default - one holding the task asked, if any does, then the smallest. Ties go
|
|
125
|
+
* by name, so the answer is the same every time; the order is `rolesGranting`'s.
|
|
126
|
+
*/
|
|
117
127
|
function smallestRole(candidates) {
|
|
118
|
-
return [...candidates].sort((a, b) => a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0];
|
|
128
|
+
return [...candidates].sort((a, b) => Number(!!b.holdsTask) - Number(!!a.holdsTask) || a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0];
|
|
119
129
|
}
|
|
120
130
|
const isBlocked = (issues) => issues.some(issue => issue.blocking);
|
|
121
131
|
exports.isBlocked = isBlocked;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access-request-review.js","sourceRoot":"","sources":["../../server/utils/access-request-review.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The checks an access decision passes - one function, read by the list screen, by the AI 입력 draft card\n * (its `check`, when the card is made and again when it is pressed) and by `decideAccessRequest` itself\n * (ADR-0103 decision 4: the card and the screen cannot say different things).\n *\n * Pure: the facts are collected by `access-request-facts.ts`; this decides what they mean. A model never\n * judges a role as risky - this does, the same way every time, and the model only words the result.\n *\n * Blocking issues keep the decision from being written. The rest are told to the person deciding.\n */\n\nexport type AccessRequestState = 'PENDING' | 'GRANTED' | 'DENIED' | 'WITHDRAWN' | 'EXPIRED'\n\nexport type AccessDecisionKind = 'GRANT' | 'DENY'\n\nexport type PrivilegeName = { category: string; privilege: string }\n\n/** The privilege that lets its holder put people into roles - a role carrying it hands that power on. */\nexport const ROLE_GRANTING_PRIVILEGE: PrivilegeName = { category: 'user', privilege: 'mutation' }\n\n/** How long a request waits for a decision (questions Q7). */\nexport const ACCESS_REQUEST_PENDING_DAYS = 14\n\nexport interface DecisionFacts {\n request: {\n state: AccessRequestState\n requesterId: string\n category: string\n privilege: string\n expiresAt?: Date | string | null\n } | null\n deciderId: string\n decision: AccessDecisionKind\n reason?: string | null\n /** The role chosen to grant - absent when it is not this domain's to assign. */\n role?: { id: string; name: string; privileges: PrivilegeName[] } | null\n /** A role id was named but not found in the domain's scope. */\n roleNamedButNotAssignable?: boolean\n /** The chosen role is the parent domain's - and whether the decider may give it (ADR-0103 decision 8). */\n roleIsParents?: boolean\n deciderMayChangeParentRoles?: boolean\n requester: { active: boolean; inDomain: boolean } | null\n /** The requester holds the privilege now - through some other role, since the request was made. */\n requesterHoldsIt: boolean\n /** Every assignable role that grants the privilege, with how many privileges each carries. */\n candidates: { id: string; name: string; privilegeCount: number }[]\n /** The decider's own privileges - to tell them when a role gives what they do not have. */\n deciderPrivileges: PrivilegeName[]\n /** Whether the decider is a domain owner - an owner may give anything. */\n deciderIsOwner: boolean\n /** The latest earlier denial of the same requester and privilege. */\n deniedBefore?: { by: string; at: Date | string; reason?: string | null } | null\n now: Date\n}\n\nexport interface DecisionIssue {\n code: string\n blocking: boolean\n field?: 'request' | 'decision' | 'role' | 'reason'\n params?: Record<string, string | number | boolean | null>\n}\n\nconst same = (a: PrivilegeName, b: PrivilegeName) => a.category === b.category && a.privilege === b.privilege\nconst nameOf = (p: PrivilegeName) => `${p.category}:${p.privilege}`\n\n/** A pending request past its time is expired, whatever the column still says. */\nexport function effectiveState(state: AccessRequestState, expiresAt: Date | string | null | undefined, now: Date): AccessRequestState {\n if (state !== 'PENDING' || !expiresAt) return state\n const at = new Date(expiresAt)\n return isNaN(at.getTime()) || at.getTime() > now.getTime() ? state : 'EXPIRED'\n}\n\nexport function expiryFrom(now: Date, days = ACCESS_REQUEST_PENDING_DAYS): Date {\n return new Date(now.getTime() + days * 24 * 60 * 60 * 1000)\n}\n\nexport function reviewAccessDecision(facts: DecisionFacts): DecisionIssue[] {\n const issues: DecisionIssue[] = []\n const { request } = facts\n\n if (!request) {\n return [{ code: 'access-request-not-found', blocking: true, field: 'request' }]\n }\n\n const state = effectiveState(request.state, request.expiresAt, facts.now)\n if (state !== 'PENDING') {\n issues.push({ code: 'request-not-pending', blocking: true, field: 'request', params: { state } })\n }\n\n if (request.requesterId === facts.deciderId) {\n issues.push({ code: 'own-request', blocking: true, field: 'request' })\n }\n\n if (!facts.requester || !facts.requester.inDomain || !facts.requester.active) {\n issues.push({ code: 'requester-unavailable', blocking: true, field: 'request' })\n }\n\n if (facts.decision === 'DENY') {\n if (!facts.reason || !facts.reason.trim()) {\n issues.push({ code: 'deny-needs-reason', blocking: true, field: 'reason' })\n }\n } else {\n issues.push(...grantIssues(facts, { category: request.category, privilege: request.privilege }))\n }\n\n if (facts.deniedBefore) {\n issues.push({\n code: 'denied-before',\n blocking: false,\n field: 'request',\n params: { by: facts.deniedBefore.by, at: new Date(facts.deniedBefore.at).toISOString(), reason: facts.deniedBefore.reason ?? '' }\n })\n }\n\n return issues\n}\n\nfunction grantIssues(facts: DecisionFacts, asked: PrivilegeName): DecisionIssue[] {\n const issues: DecisionIssue[] = []\n\n if (facts.requesterHoldsIt) {\n issues.push({ code: 'already-holds', blocking: true, field: 'request', params: { privilege: nameOf(asked) } })\n }\n\n if (!facts.role) {\n issues.push({\n code: facts.roleNamedButNotAssignable ? 'role-not-assignable' : 'role-required',\n blocking: true,\n field: 'role'\n })\n return issues\n }\n\n const { role } = facts\n if (!role.privileges.some(p => same(p, asked))) {\n issues.push({ code: 'role-does-not-grant', blocking: true, field: 'role', params: { role: role.name, privilege: nameOf(asked) } })\n return issues\n }\n\n if (facts.roleIsParents && !facts.deciderMayChangeParentRoles) {\n issues.push({ code: 'role-belongs-to-parent', blocking: true, field: 'role', params: { role: role.name } })\n }\n\n const more = role.privileges.filter(p => !same(p, asked))\n if (more.length > 0) {\n issues.push({\n code: 'role-grants-more',\n blocking: false,\n field: 'role',\n params: { role: role.name, count: more.length, privileges: more.map(nameOf).join(', ') }\n })\n }\n\n if (!same(asked, ROLE_GRANTING_PRIVILEGE) && role.privileges.some(p => same(p, ROLE_GRANTING_PRIVILEGE))) {\n issues.push({ code: 'role-grants-admin', blocking: false, field: 'role', params: { role: role.name } })\n }\n\n const smaller = facts.candidates\n .filter(c => c.id !== role.id && c.privilegeCount < role.privileges.length)\n .sort((a, b) => a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0]\n if (smaller) {\n issues.push({ code: 'smaller-role-exists', blocking: false, field: 'role', params: { role: smaller.name, count: smaller.privilegeCount } })\n }\n\n if (!facts.deciderIsOwner) {\n const lacking = role.privileges.filter(p => !facts.deciderPrivileges.some(d => same(d, p)))\n if (lacking.length > 0) {\n /* Told, not refused (questions Q6) - the role screen gives the same role today; ruling both is a separate decision. */\n issues.push({ code: 'decider-lacks-it', blocking: false, field: 'role', params: { privileges: lacking.map(nameOf).join(', ') } })\n }\n }\n\n return issues\n}\n\n/** The smallest candidate - what the draft card proposes by default. Ties go by name, so the answer is the same every time. */\nexport function smallestRole<T extends { name: string; privilegeCount: number }>(candidates: T[]): T | undefined {\n return [...candidates].sort((a, b) => a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0]\n}\n\nexport const isBlocked = (issues: DecisionIssue[]) => issues.some(issue => issue.blocking)\n"]}
|
|
1
|
+
{"version":3,"file":"access-request-review.js","sourceRoot":"","sources":["../../server/utils/access-request-review.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The checks an access decision passes - one function, read by the list screen, by the AI 입력 draft card\n * (its `check`, when the card is made and again when it is pressed) and by `decideAccessRequest` itself\n * (ADR-0103 decision 4: the card and the screen cannot say different things).\n *\n * Pure: the facts are collected by `access-request-facts.ts`; this decides what they mean. A model never\n * judges a role as risky - this does, the same way every time, and the model only words the result.\n *\n * Blocking issues keep the decision from being written. The rest are told to the person deciding.\n */\n\nexport type AccessRequestState = 'PENDING' | 'GRANTED' | 'DENIED' | 'WITHDRAWN' | 'EXPIRED'\n\nexport type AccessDecisionKind = 'GRANT' | 'DENY'\n\nexport type PrivilegeName = { category: string; privilege: string }\n\n/** The privilege that lets its holder put people into roles - a role carrying it hands that power on. */\nexport const ROLE_GRANTING_PRIVILEGE: PrivilegeName = { category: 'user', privilege: 'mutation' }\n\n/** How long a request waits for a decision (questions Q7). */\nexport const ACCESS_REQUEST_PENDING_DAYS = 14\n\nexport interface DecisionFacts {\n request: {\n state: AccessRequestState\n requesterId: string\n category: string\n privilege: string\n expiresAt?: Date | string | null\n } | null\n deciderId: string\n decision: AccessDecisionKind\n reason?: string | null\n /** The role chosen to grant - absent when it is not this domain's to assign. */\n role?: { id: string; name: string; privileges: PrivilegeName[] } | null\n /** A role id was named but not found in the domain's scope. */\n roleNamedButNotAssignable?: boolean\n /** The chosen role is the parent domain's - and whether the decider may give it (ADR-0103 decision 8). */\n roleIsParents?: boolean\n deciderMayChangeParentRoles?: boolean\n requester: { active: boolean; inDomain: boolean } | null\n /** The requester holds the privilege now - through some other role, since the request was made. */\n requesterHoldsIt: boolean\n /** Every assignable role that grants the privilege, with how many privileges each carries and whether it holds the task asked. */\n candidates: { id: string; name: string; privilegeCount: number; holdsTask?: boolean }[]\n /** The task the requester asked for, with its declared privileges - null when none was asked or it is gone (ADR-0103 decision 9). */\n task?: { id: string; privileges: PrivilegeName[] } | null\n /** The decider's own privileges - to tell them when a role gives what they do not have. */\n deciderPrivileges: PrivilegeName[]\n /** Whether the decider is a domain owner - an owner may give anything. */\n deciderIsOwner: boolean\n /** The latest earlier denial of the same requester and privilege. */\n deniedBefore?: { by: string; at: Date | string; reason?: string | null } | null\n now: Date\n}\n\nexport interface DecisionIssue {\n code: string\n blocking: boolean\n field?: 'request' | 'decision' | 'role' | 'reason'\n params?: Record<string, string | number | boolean | null>\n}\n\nconst same = (a: PrivilegeName, b: PrivilegeName) => a.category === b.category && a.privilege === b.privilege\nconst nameOf = (p: PrivilegeName) => `${p.category}:${p.privilege}`\n\n/** A pending request past its time is expired, whatever the column still says. */\nexport function effectiveState(state: AccessRequestState, expiresAt: Date | string | null | undefined, now: Date): AccessRequestState {\n if (state !== 'PENDING' || !expiresAt) return state\n const at = new Date(expiresAt)\n return isNaN(at.getTime()) || at.getTime() > now.getTime() ? state : 'EXPIRED'\n}\n\nexport function expiryFrom(now: Date, days = ACCESS_REQUEST_PENDING_DAYS): Date {\n return new Date(now.getTime() + days * 24 * 60 * 60 * 1000)\n}\n\nexport function reviewAccessDecision(facts: DecisionFacts): DecisionIssue[] {\n const issues: DecisionIssue[] = []\n const { request } = facts\n\n if (!request) {\n return [{ code: 'access-request-not-found', blocking: true, field: 'request' }]\n }\n\n const state = effectiveState(request.state, request.expiresAt, facts.now)\n if (state !== 'PENDING') {\n issues.push({ code: 'request-not-pending', blocking: true, field: 'request', params: { state } })\n }\n\n if (request.requesterId === facts.deciderId) {\n issues.push({ code: 'own-request', blocking: true, field: 'request' })\n }\n\n if (!facts.requester || !facts.requester.inDomain || !facts.requester.active) {\n issues.push({ code: 'requester-unavailable', blocking: true, field: 'request' })\n }\n\n if (facts.decision === 'DENY') {\n if (!facts.reason || !facts.reason.trim()) {\n issues.push({ code: 'deny-needs-reason', blocking: true, field: 'reason' })\n }\n } else {\n issues.push(...grantIssues(facts, { category: request.category, privilege: request.privilege }))\n }\n\n if (facts.deniedBefore) {\n issues.push({\n code: 'denied-before',\n blocking: false,\n field: 'request',\n params: { by: facts.deniedBefore.by, at: new Date(facts.deniedBefore.at).toISOString(), reason: facts.deniedBefore.reason ?? '' }\n })\n }\n\n return issues\n}\n\nfunction grantIssues(facts: DecisionFacts, asked: PrivilegeName): DecisionIssue[] {\n const issues: DecisionIssue[] = []\n\n if (facts.requesterHoldsIt) {\n issues.push({ code: 'already-holds', blocking: true, field: 'request', params: { privilege: nameOf(asked) } })\n }\n\n if (!facts.role) {\n issues.push({\n code: facts.roleNamedButNotAssignable ? 'role-not-assignable' : 'role-required',\n blocking: true,\n field: 'role'\n })\n return issues\n }\n\n const { role } = facts\n if (!role.privileges.some(p => same(p, asked))) {\n issues.push({ code: 'role-does-not-grant', blocking: true, field: 'role', params: { role: role.name, privilege: nameOf(asked) } })\n return issues\n }\n\n if (facts.roleIsParents && !facts.deciderMayChangeParentRoles) {\n issues.push({ code: 'role-belongs-to-parent', blocking: true, field: 'role', params: { role: role.name } })\n }\n\n const more = role.privileges.filter(p => !same(p, asked))\n if (more.length > 0) {\n issues.push({\n code: 'role-grants-more',\n blocking: false,\n field: 'role',\n params: { role: role.name, count: more.length, privileges: more.map(nameOf).join(', ') }\n })\n }\n\n /* Told, not refused: the role opens the refused door, and the rest of the work asked for is the decider's call. */\n const missingOfTask = (facts.task?.privileges || []).filter(p => !role.privileges.some(r => same(r, p)))\n if (facts.task && missingOfTask.length > 0) {\n issues.push({ code: 'role-lacks-task', blocking: false, field: 'role', params: { role: role.name, task: facts.task.id, privileges: missingOfTask.map(nameOf).join(', ') } })\n }\n if (!same(asked, ROLE_GRANTING_PRIVILEGE) && role.privileges.some(p => same(p, ROLE_GRANTING_PRIVILEGE))) {\n issues.push({ code: 'role-grants-admin', blocking: false, field: 'role', params: { role: role.name } })\n }\n\n /* A smaller role that drops the task asked for is not a better answer - only one holding it as the chosen does counts. */\n const choiceHoldsTask = !!facts.task && missingOfTask.length === 0\n const smaller = facts.candidates\n .filter(c => c.id !== role.id && c.privilegeCount < role.privileges.length && (!choiceHoldsTask || c.holdsTask))\n .sort((a, b) => a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0]\n if (smaller) {\n issues.push({ code: 'smaller-role-exists', blocking: false, field: 'role', params: { role: smaller.name, count: smaller.privilegeCount } })\n }\n\n if (!facts.deciderIsOwner) {\n const lacking = role.privileges.filter(p => !facts.deciderPrivileges.some(d => same(d, p)))\n if (lacking.length > 0) {\n /* Told, not refused (questions Q6) - the role screen gives the same role today; ruling both is a separate decision. */\n issues.push({ code: 'decider-lacks-it', blocking: false, field: 'role', params: { privileges: lacking.map(nameOf).join(', ') } })\n }\n }\n\n return issues\n}\n\n/**\n * The candidate the draft card proposes by default - one holding the task asked, if any does, then the smallest. Ties go\n * by name, so the answer is the same every time; the order is `rolesGranting`'s.\n */\nexport function smallestRole<T extends { name: string; privilegeCount: number; holdsTask?: boolean }>(candidates: T[]): T | undefined {\n return [...candidates].sort((a, b) => Number(!!b.holdsTask) - Number(!!a.holdsTask) || a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0]\n}\n\nexport const isBlocked = (issues: DecisionIssue[]) => issues.some(issue => issue.blocking)\n"]}
|
|
@@ -20,8 +20,14 @@ export type ProhibitedPrivilege = {
|
|
|
20
20
|
privilege: string;
|
|
21
21
|
by?: 'address' | 'license';
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* What opened a gate - the audit keeps it with every check (ADR-0141 §3.2), so a pass that rested on
|
|
25
|
+
* superuser is told from one a role gave.
|
|
26
|
+
*/
|
|
27
|
+
export type GrantBasis = 'role' | 'domain-owner' | 'superuser';
|
|
23
28
|
export type PermissionJudgement = {
|
|
24
29
|
permitted: true;
|
|
30
|
+
by?: GrantBasis;
|
|
25
31
|
} | {
|
|
26
32
|
permitted: false;
|
|
27
33
|
because: RefusalCause;
|
|
@@ -3,7 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.judgePermission = judgePermission;
|
|
4
4
|
exports.checkPermission = checkPermission;
|
|
5
5
|
const user_js_1 = require("../service/user/user.js");
|
|
6
|
+
/* A gate that declares nothing - nothing opened it. */
|
|
6
7
|
const PERMITTED = { permitted: true };
|
|
8
|
+
function granted(by) {
|
|
9
|
+
return { permitted: true, by };
|
|
10
|
+
}
|
|
7
11
|
function refused(because) {
|
|
8
12
|
return { permitted: false, because };
|
|
9
13
|
}
|
|
@@ -19,9 +23,18 @@ async function judgePermission(privilegeObject, user, domain, unsafeIP, prohibit
|
|
|
19
23
|
return PERMITTED;
|
|
20
24
|
}
|
|
21
25
|
const { owner: domainOwnerGranted, super: superUserGranted, category, privilege } = privilegeObject;
|
|
22
|
-
/*
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
/*
|
|
27
|
+
* Ownership as the gate has always asked it - `process` carries the answer (domain-authenticate-middleware
|
|
28
|
+
* installs it). Domain owner first, then superuser, as the `||` it replaces; it now says which one opened.
|
|
29
|
+
*/
|
|
30
|
+
const ownershipBasis = async () => {
|
|
31
|
+
if (domainOwnerGranted && (await process.domainOwnerGranted(domain, user)))
|
|
32
|
+
return 'domain-owner';
|
|
33
|
+
if (superUserGranted && (await process.superUserGranted(domain, user)))
|
|
34
|
+
return 'superuser';
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
const ownershipOpens = async () => !!(await ownershipBasis());
|
|
25
38
|
/*
|
|
26
39
|
* From an unsafe address ownership never opened anything and was never asked. It is asked now only on the
|
|
27
40
|
* way to a refusal, to tell "address" from "role" - and a missing answer is "no", so the gate refuses as it did.
|
|
@@ -42,7 +55,7 @@ async function judgePermission(privilegeObject, user, domain, unsafeIP, prohibit
|
|
|
42
55
|
return refused(prohibition.by ?? 'unknown');
|
|
43
56
|
}
|
|
44
57
|
if (await user_js_1.User.hasPrivilege(privilege, category, domain, user)) {
|
|
45
|
-
return
|
|
58
|
+
return granted('role');
|
|
46
59
|
}
|
|
47
60
|
/* An owner without the role would pass from a safe address - what stopped them is where they are. */
|
|
48
61
|
return refused((await ownershipWouldHaveOpened()) ? 'address' : 'role');
|
|
@@ -62,16 +75,18 @@ async function judgePermission(privilegeObject, user, domain, unsafeIP, prohibit
|
|
|
62
75
|
else {
|
|
63
76
|
if (!privilege || !category) {
|
|
64
77
|
// privilege, category가 설정되지 않은 경우에는 ownership granted만을 적용한다.
|
|
65
|
-
|
|
78
|
+
const basis = await ownershipBasis();
|
|
79
|
+
return basis ? granted(basis) : refused('ownership');
|
|
66
80
|
}
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
const basis = await ownershipBasis();
|
|
82
|
+
if (basis) {
|
|
83
|
+
return granted(basis);
|
|
69
84
|
}
|
|
70
85
|
const prohibition = prohibitionOf(prohibitedPrivileges, category, privilege);
|
|
71
86
|
if (prohibition) {
|
|
72
87
|
return refused(prohibition.by ?? 'unknown');
|
|
73
88
|
}
|
|
74
|
-
return (await user_js_1.User.hasPrivilege(privilege, category, domain, user)) ?
|
|
89
|
+
return (await user_js_1.User.hasPrivilege(privilege, category, domain, user)) ? granted('role') : refused('role');
|
|
75
90
|
}
|
|
76
91
|
}
|
|
77
92
|
async function checkPermission(privilegeObject, user, domain, unsafeIP, prohibitedPrivileges) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check-permission.js","sourceRoot":"","sources":["../../server/utils/check-permission.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"check-permission.js","sourceRoot":"","sources":["../../server/utils/check-permission.ts"],"names":[],"mappings":"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","sourcesContent":["import { Domain } from '@things-factory/shell'\nimport { PrivilegeObject } from '../service/privilege/privilege.js'\nimport { User } from '../service/user/user.js'\n\n/**\n * Why a gate said no - what the person can do about it differs (ADR-0103 decision 1).\n *\n * role none of their roles holds category:privilege - a role can be given (an access request)\n * address refused from where they are: the domain's protected address list\n * license refused by what this installation is licensed for\n * ownership the declaration opens only to domain or system owners - no role gives it\n * unknown a prohibition that does not say where it came from - the reason is not made up\n */\nexport type RefusalCause = 'role' | 'address' | 'license' | 'ownership' | 'unknown'\n\n/**\n * A privilege refused by the request, whatever the person's roles. `by` says who refused it:\n * the domain's address list (shell domain-middleware) or the installation's license (board-service).\n */\nexport type ProhibitedPrivilege = { category: string; privilege: string; by?: 'address' | 'license' }\n\n/**\n * What opened a gate - the audit keeps it with every check (ADR-0141 §3.2), so a pass that rested on\n * superuser is told from one a role gave.\n */\nexport type GrantBasis = 'role' | 'domain-owner' | 'superuser'\n\nexport type PermissionJudgement = { permitted: true; by?: GrantBasis } | { permitted: false; because: RefusalCause }\n\n/* A gate that declares nothing - nothing opened it. */\nconst PERMITTED: PermissionJudgement = { permitted: true }\n\nfunction granted(by: GrantBasis): PermissionJudgement {\n return { permitted: true, by }\n}\n\nfunction refused(because: RefusalCause): PermissionJudgement {\n return { permitted: false, because }\n}\n\nfunction prohibitionOf(prohibitedPrivileges: ProhibitedPrivilege[] | undefined, category: string, privilege: string) {\n return (prohibitedPrivileges || []).find(pp => pp.category == category && pp.privilege == privilege)\n}\n\n/**\n * The gate, with its reason. `checkPermission` answers only the boolean and is what most callers\n * want; the privilege directive reads this so a refusal can say which of the four it was.\n */\nexport async function judgePermission(\n privilegeObject: PrivilegeObject,\n user: User,\n domain: Domain,\n unsafeIP?: boolean,\n prohibitedPrivileges?: ProhibitedPrivilege[]\n): Promise<PermissionJudgement> {\n if (!privilegeObject) {\n return PERMITTED\n }\n\n const { owner: domainOwnerGranted, super: superUserGranted, category, privilege } = privilegeObject\n\n /*\n * Ownership as the gate has always asked it - `process` carries the answer (domain-authenticate-middleware\n * installs it). Domain owner first, then superuser, as the `||` it replaces; it now says which one opened.\n */\n const ownershipBasis = async (): Promise<GrantBasis | null> => {\n if (domainOwnerGranted && (await (process as any).domainOwnerGranted(domain, user))) return 'domain-owner'\n if (superUserGranted && (await (process as any).superUserGranted(domain, user))) return 'superuser'\n return null\n }\n const ownershipOpens = async () => !!(await ownershipBasis())\n\n /*\n * From an unsafe address ownership never opened anything and was never asked. It is asked now only on the\n * way to a refusal, to tell \"address\" from \"role\" - and a missing answer is \"no\", so the gate refuses as it did.\n */\n const ownershipWouldHaveOpened = async () => {\n try {\n return await ownershipOpens()\n } catch {\n return false\n }\n }\n\n if (unsafeIP) {\n if (privilege && category) {\n // unsafeIP 상황에서는 ownership granted는 적용되지 않는다.\n const prohibition = prohibitionOf(prohibitedPrivileges, category, privilege)\n if (prohibition) {\n return refused(prohibition.by ?? 'unknown')\n }\n\n if (await User.hasPrivilege(privilege, category, domain, user)) {\n return granted('role')\n }\n\n /* An owner without the role would pass from a safe address - what stopped them is where they are. */\n return refused((await ownershipWouldHaveOpened()) ? 'address' : 'role')\n }\n\n /*\n * Ownership does not count from an unsafe address, so a declaration that rests on\n * ownership alone has nothing left to stand on here and the answer is no.\n *\n * This read `!domainOwnerGranted && !superUserGranted` until 2026-09-15, which answered\n * **yes** for an object that named nothing at all — the same `{}` a safe address\n * refused. An unsafe address was the more permissive of the two, which is backwards.\n * `privilege-directive.ts` now refuses to build a declaration that names nothing, but\n * this function is exported and called directly, so the rule is fixed here too.\n */\n return refused('address')\n } else {\n if (!privilege || !category) {\n // privilege, category가 설정되지 않은 경우에는 ownership granted만을 적용한다.\n const basis = await ownershipBasis()\n return basis ? granted(basis) : refused('ownership')\n }\n\n const basis = await ownershipBasis()\n if (basis) {\n return granted(basis)\n }\n\n const prohibition = prohibitionOf(prohibitedPrivileges, category, privilege)\n if (prohibition) {\n return refused(prohibition.by ?? 'unknown')\n }\n\n return (await User.hasPrivilege(privilege, category, domain, user)) ? granted('role') : refused('role')\n }\n}\n\nexport async function checkPermission(\n privilegeObject: PrivilegeObject,\n user: User,\n domain: Domain,\n unsafeIP?: boolean,\n prohibitedPrivileges?: ProhibitedPrivilege[]\n): Promise<boolean> {\n return (await judgePermission(privilegeObject, user, domain, unsafeIP, prohibitedPrivileges)).permitted\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@things-factory/auth-base",
|
|
3
|
-
"version": "10.1.
|
|
3
|
+
"version": "10.1.202",
|
|
4
4
|
"main": "dist-server/index.js",
|
|
5
5
|
"browser": "dist-client/index.js",
|
|
6
6
|
"things-factory": true,
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"@things-factory/email-base": "^10.1.188",
|
|
39
39
|
"@things-factory/env": "^10.1.149",
|
|
40
40
|
"@things-factory/shell": "^10.1.188",
|
|
41
|
+
"@things-factory/task-lease": "^10.1.188",
|
|
41
42
|
"@things-factory/utils": "^10.1.179",
|
|
42
43
|
"@types/webappsec-credential-management": "^0.6.9",
|
|
43
44
|
"jsonwebtoken": "^9.0.0",
|
|
@@ -49,5 +50,5 @@
|
|
|
49
50
|
"passport-jwt": "^4.0.0",
|
|
50
51
|
"passport-local": "^1.0.0"
|
|
51
52
|
},
|
|
52
|
-
"gitHead": "
|
|
53
|
+
"gitHead": "3243b81eb1a4d3d903a01929f3a85d5b8df438dd"
|
|
53
54
|
}
|
|
@@ -84,7 +84,7 @@ describe('listPendingAccessRequests', () => {
|
|
|
84
84
|
await ask(kim)
|
|
85
85
|
const answer = await assistant.pendingRequestsToDecide({ domain, user: park })
|
|
86
86
|
expect(answer.requests).toEqual([
|
|
87
|
-
expect.objectContaining({ requester: 'kim', privilege: 'warehouse:mutation', roles: [{ name: 'inbound', privilegeCount: 1 }] })
|
|
87
|
+
expect.objectContaining({ requester: 'kim', privilege: 'warehouse:mutation', task: null, roles: [{ name: 'inbound', privilegeCount: 1, holdsTask: false }] })
|
|
88
88
|
])
|
|
89
89
|
})
|
|
90
90
|
|
|
@@ -315,3 +315,74 @@ describe('found in review (2026-10-02)', () => {
|
|
|
315
315
|
expect(mine.requester?.id).toBe(kim.id)
|
|
316
316
|
})
|
|
317
317
|
})
|
|
318
|
+
|
|
319
|
+
describe('asking for a task (ADR-0103 decision 9)', () => {
|
|
320
|
+
const { registerRoleTask } = loadCompiled('service/role-template/role-task')
|
|
321
|
+
const { AccessRequestQuery } = loadCompiled('service/access-request/access-request-query')
|
|
322
|
+
|
|
323
|
+
beforeEach(() => {
|
|
324
|
+
;(process as any)['ROLE_TASKS'] = {}
|
|
325
|
+
;(process as any).PRIVILEGES = {
|
|
326
|
+
'warehouse query': ['warehouse', 'query'],
|
|
327
|
+
'warehouse mutation': ['warehouse', 'mutation'],
|
|
328
|
+
'user mutation': ['user', 'mutation']
|
|
329
|
+
}
|
|
330
|
+
registerRoleTask({ id: 'warehouse.keeping', grants: [{ category: 'warehouse', axes: ['query', 'mutation'] }] })
|
|
331
|
+
registerRoleTask({ id: 'warehouse.reading', grants: [{ category: 'warehouse', axes: ['query'] }] })
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('★ the tasks holding a refused privilege are listed fewest privileges first', async () => {
|
|
335
|
+
const query = new AccessRequestQuery()
|
|
336
|
+
expect((await query.tasksGranting('warehouse', 'query')).map((one: any) => [one.taskId, one.privilegeCount])).toEqual([
|
|
337
|
+
['warehouse.reading', 1],
|
|
338
|
+
['warehouse.keeping', 2]
|
|
339
|
+
])
|
|
340
|
+
expect((await query.tasksGranting('warehouse', 'mutation')).map((one: any) => one.taskId)).toEqual(['warehouse.keeping'])
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('★ the request keeps the task asked for, and refuses one that does not hold the refused privilege', async () => {
|
|
344
|
+
const request = await access.requestAccess({ ...ASK, taskId: 'warehouse.keeping' }, contextOf(kim))
|
|
345
|
+
expect(request.taskId).toBe('warehouse.keeping')
|
|
346
|
+
|
|
347
|
+
await expect(access.requestAccess({ ...ASK, taskId: 'warehouse.reading' }, contextOf(park))).rejects.toMatchObject({ code: 'access-request-task-mismatch' })
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('★ asking again with other work adds to the pending one and does not change the work it named', async () => {
|
|
351
|
+
await access.requestAccess({ ...ASK, taskId: 'warehouse.keeping' }, contextOf(kim))
|
|
352
|
+
registerRoleTask({ id: 'warehouse.receiving', grants: [{ category: 'warehouse', axes: ['mutation'] }] })
|
|
353
|
+
const merged = await access.requestAccess({ ...ASK, taskId: 'warehouse.receiving', reason: 'still needed' }, contextOf(kim))
|
|
354
|
+
|
|
355
|
+
expect(merged.count).toBe(2)
|
|
356
|
+
expect(merged.taskId).toBe('warehouse.keeping')
|
|
357
|
+
expect(await getRepository(AccessRequest).count()).toBe(1)
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it('★ roles holding the whole task come first, then fewest privileges', async () => {
|
|
361
|
+
expect((await access.rolesGranting('warehouse', 'mutation', domain)).map((c: any) => c.name)).toEqual(['inbound', 'manager'])
|
|
362
|
+
const ordered = await access.rolesGranting('warehouse', 'mutation', domain, undefined, 'warehouse.keeping')
|
|
363
|
+
expect(ordered.map((c: any) => [c.name, c.holdsTask])).toEqual([
|
|
364
|
+
['manager', true],
|
|
365
|
+
['inbound', false]
|
|
366
|
+
])
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('★ a role short of the task is told, not refused - and a smaller role short of it is not proposed', async () => {
|
|
370
|
+
const request = await access.requestAccess({ ...ASK, taskId: 'warehouse.keeping' }, contextOf(kim))
|
|
371
|
+
|
|
372
|
+
const short = await access.reviewDecision({ requestId: request.id, decision: 'GRANT', roleId: inbound.id }, contextOf(park))
|
|
373
|
+
expect(short.find((i: any) => i.code === 'role-lacks-task')).toMatchObject({ blocking: false, params: { task: 'warehouse.keeping', privileges: 'warehouse:query' } })
|
|
374
|
+
|
|
375
|
+
const whole = await access.reviewDecision({ requestId: request.id, decision: 'GRANT', roleId: manager.id }, contextOf(park))
|
|
376
|
+
expect(whole.map((i: any) => i.code)).not.toContain('role-lacks-task')
|
|
377
|
+
expect(whole.map((i: any) => i.code)).not.toContain('smaller-role-exists')
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
it('a task gone from the catalog is no error - the review says nothing of it and no role is put first', async () => {
|
|
381
|
+
const request = await access.requestAccess({ ...ASK, taskId: 'warehouse.keeping' }, contextOf(kim))
|
|
382
|
+
;(process as any)['ROLE_TASKS'] = {}
|
|
383
|
+
|
|
384
|
+
const issues = await access.reviewDecision({ requestId: request.id, decision: 'GRANT', roleId: inbound.id }, contextOf(park))
|
|
385
|
+
expect(issues.map((i: any) => i.code)).not.toContain('role-lacks-task')
|
|
386
|
+
expect((await access.rolesGranting('warehouse', 'mutation', domain, undefined, 'warehouse.keeping')).map((c: any) => c.name)).toEqual(['inbound', 'manager'])
|
|
387
|
+
})
|
|
388
|
+
})
|
|
@@ -161,3 +161,33 @@ describe('the parent domain\'s role (ADR-0103 decision 8)', () => {
|
|
|
161
161
|
expect(codes(facts({ roleIsParents: false }))).not.toContain('⛔role-belongs-to-parent')
|
|
162
162
|
})
|
|
163
163
|
})
|
|
164
|
+
|
|
165
|
+
describe('the task asked for (ADR-0103 decision 9)', () => {
|
|
166
|
+
const task = { id: 'warehouse.keeping', privileges: [ASKED, SEE] }
|
|
167
|
+
|
|
168
|
+
it('★ a role short of the task asked is told which privileges it lacks - not refused', () => {
|
|
169
|
+
const issues = reviewAccessDecision(facts({ task }))
|
|
170
|
+
expect(issues.find(issue => issue.code === 'role-lacks-task')).toEqual({
|
|
171
|
+
code: 'role-lacks-task',
|
|
172
|
+
blocking: false,
|
|
173
|
+
field: 'role',
|
|
174
|
+
params: { role: 'inbound', task: 'warehouse.keeping', privileges: 'warehouse:query' }
|
|
175
|
+
})
|
|
176
|
+
expect(isBlocked(issues)).toBe(false)
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('★ says nothing when no task was asked or the task is gone', () => {
|
|
180
|
+
expect(codes(facts({ task: null }))).not.toContain('⚠role-lacks-task')
|
|
181
|
+
expect(codes(facts())).not.toContain('⚠role-lacks-task')
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('★ a smaller role that drops the task is not proposed over a role holding it', () => {
|
|
185
|
+
const keeper = { id: 'r-keeper', name: 'keeper', privileges: [ASKED, SEE, GRANT_ROLES] }
|
|
186
|
+
const candidates = [
|
|
187
|
+
{ id: 'r-keeper', name: 'keeper', privilegeCount: 3, holdsTask: true },
|
|
188
|
+
{ id: 'r-inbound', name: 'inbound', privilegeCount: 1, holdsTask: false }
|
|
189
|
+
]
|
|
190
|
+
expect(codes(facts({ task, role: keeper, candidates }))).not.toContain('⚠smaller-role-exists')
|
|
191
|
+
expect(smallestRole(candidates)?.name).toBe('keeper')
|
|
192
|
+
})
|
|
193
|
+
})
|