@things-factory/auth-base 10.1.79 → 10.1.106

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.
Files changed (56) hide show
  1. package/dist-client/tsconfig.tsbuildinfo +1 -1
  2. package/dist-server/controllers/access-request.d.ts +53 -0
  3. package/dist-server/controllers/access-request.js +333 -0
  4. package/dist-server/controllers/access-request.js.map +1 -0
  5. package/dist-server/service/access-request/access-request-mutation.d.ts +17 -0
  6. package/dist-server/service/access-request/access-request-mutation.js +65 -0
  7. package/dist-server/service/access-request/access-request-mutation.js.map +1 -0
  8. package/dist-server/service/access-request/access-request-query.d.ts +11 -0
  9. package/dist-server/service/access-request/access-request-query.js +85 -0
  10. package/dist-server/service/access-request/access-request-query.js.map +1 -0
  11. package/dist-server/service/access-request/access-request-types.d.ts +32 -0
  12. package/dist-server/service/access-request/access-request-types.js +115 -0
  13. package/dist-server/service/access-request/access-request-types.js.map +1 -0
  14. package/dist-server/service/access-request/access-request.d.ts +33 -0
  15. package/dist-server/service/access-request/access-request.js +160 -0
  16. package/dist-server/service/access-request/access-request.js.map +1 -0
  17. package/dist-server/service/access-request/index.d.ts +5 -0
  18. package/dist-server/service/access-request/index.js +9 -0
  19. package/dist-server/service/access-request/index.js.map +1 -0
  20. package/dist-server/service/index.d.ts +3 -1
  21. package/dist-server/service/index.js +42 -37
  22. package/dist-server/service/index.js.map +1 -1
  23. package/dist-server/service/open-fields.js +4 -1
  24. package/dist-server/service/open-fields.js.map +1 -1
  25. package/dist-server/service/privilege/privilege-directive.js +6 -6
  26. package/dist-server/service/privilege/privilege-directive.js.map +1 -1
  27. package/dist-server/service/privilege/privilege-rejection.d.ts +8 -2
  28. package/dist-server/service/privilege/privilege-rejection.js +27 -6
  29. package/dist-server/service/privilege/privilege-rejection.js.map +1 -1
  30. package/dist-server/service/user/user-mutation.js +5 -4
  31. package/dist-server/service/user/user-mutation.js.map +1 -1
  32. package/dist-server/service/user/user-query.js +5 -17
  33. package/dist-server/service/user/user-query.js.map +1 -1
  34. package/dist-server/tsconfig.tsbuildinfo +1 -1
  35. package/dist-server/utils/access-request-review.d.ts +69 -0
  36. package/dist-server/utils/access-request-review.js +119 -0
  37. package/dist-server/utils/access-request-review.js.map +1 -0
  38. package/dist-server/utils/assignable-roles.d.ts +23 -0
  39. package/dist-server/utils/assignable-roles.js +89 -0
  40. package/dist-server/utils/assignable-roles.js.map +1 -0
  41. package/dist-server/utils/check-permission.d.ts +29 -2
  42. package/dist-server/utils/check-permission.js +48 -14
  43. package/dist-server/utils/check-permission.js.map +1 -1
  44. package/package.json +4 -4
  45. package/tests/access-request-db.test.ts +236 -0
  46. package/tests/access-request-review.test.ts +155 -0
  47. package/tests/assignable-roles-db.test.ts +141 -0
  48. package/tests/permission-gate.test.ts +5 -7
  49. package/tests/privilege-directive.test.ts +21 -0
  50. package/tests/privilege-rejection.test.ts +32 -0
  51. package/tests/refusal-cause.test.ts +80 -0
  52. package/translations/en.json +26 -1
  53. package/translations/ja.json +26 -1
  54. package/translations/ko.json +26 -1
  55. package/translations/ms.json +26 -1
  56. package/translations/zh.json +26 -1
@@ -0,0 +1,69 @@
1
+ export type AccessRequestState = 'PENDING' | 'GRANTED' | 'DENIED' | 'WITHDRAWN' | 'EXPIRED';
2
+ export type AccessDecisionKind = 'GRANT' | 'DENY';
3
+ export type PrivilegeName = {
4
+ category: string;
5
+ privilege: string;
6
+ };
7
+ /** The privilege that lets its holder put people into roles - a role carrying it hands that power on. */
8
+ export declare const ROLE_GRANTING_PRIVILEGE: PrivilegeName;
9
+ /** How long a request waits for a decision (questions Q7). */
10
+ export declare const ACCESS_REQUEST_PENDING_DAYS = 14;
11
+ export interface DecisionFacts {
12
+ request: {
13
+ state: AccessRequestState;
14
+ requesterId: string;
15
+ category: string;
16
+ privilege: string;
17
+ expiresAt?: Date | string | null;
18
+ } | null;
19
+ deciderId: string;
20
+ decision: AccessDecisionKind;
21
+ reason?: string | null;
22
+ /** The role chosen to grant - absent when it is not this domain's to assign. */
23
+ role?: {
24
+ id: string;
25
+ name: string;
26
+ privileges: PrivilegeName[];
27
+ } | null;
28
+ /** A role id was named but not found in the domain's scope. */
29
+ roleNamedButNotAssignable?: boolean;
30
+ requester: {
31
+ active: boolean;
32
+ inDomain: boolean;
33
+ } | null;
34
+ /** The requester holds the privilege now - through some other role, since the request was made. */
35
+ requesterHoldsIt: boolean;
36
+ /** Every assignable role that grants the privilege, with how many privileges each carries. */
37
+ candidates: {
38
+ id: string;
39
+ name: string;
40
+ privilegeCount: number;
41
+ }[];
42
+ /** The decider's own privileges - to tell them when a role gives what they do not have. */
43
+ deciderPrivileges: PrivilegeName[];
44
+ /** Whether the decider is a domain owner - an owner may give anything. */
45
+ deciderIsOwner: boolean;
46
+ /** The latest earlier denial of the same requester and privilege. */
47
+ deniedBefore?: {
48
+ by: string;
49
+ at: Date | string;
50
+ reason?: string | null;
51
+ } | null;
52
+ now: Date;
53
+ }
54
+ export interface DecisionIssue {
55
+ code: string;
56
+ blocking: boolean;
57
+ field?: 'request' | 'decision' | 'role' | 'reason';
58
+ params?: Record<string, string | number | boolean | null>;
59
+ }
60
+ /** A pending request past its time is expired, whatever the column still says. */
61
+ export declare function effectiveState(state: AccessRequestState, expiresAt: Date | string | null | undefined, now: Date): AccessRequestState;
62
+ export declare function expiryFrom(now: Date, days?: number): Date;
63
+ export declare function reviewAccessDecision(facts: DecisionFacts): DecisionIssue[];
64
+ /** The smallest candidate - what the draft card proposes by default. Ties go by name, so the answer is the same every time. */
65
+ export declare function smallestRole<T extends {
66
+ name: string;
67
+ privilegeCount: number;
68
+ }>(candidates: T[]): T | undefined;
69
+ export declare const isBlocked: (issues: DecisionIssue[]) => boolean;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ /*
3
+ * The checks an access decision passes - one function, read by the list screen, by the AI 입력 draft card
4
+ * (its `check`, when the card is made and again when it is pressed) and by `decideAccessRequest` itself
5
+ * (ADR-0103 decision 4: the card and the screen cannot say different things).
6
+ *
7
+ * Pure: the facts are collected by `access-request-facts.ts`; this decides what they mean. A model never
8
+ * judges a role as risky - this does, the same way every time, and the model only words the result.
9
+ *
10
+ * Blocking issues keep the decision from being written. The rest are told to the person deciding.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.isBlocked = exports.ACCESS_REQUEST_PENDING_DAYS = exports.ROLE_GRANTING_PRIVILEGE = void 0;
14
+ exports.effectiveState = effectiveState;
15
+ exports.expiryFrom = expiryFrom;
16
+ exports.reviewAccessDecision = reviewAccessDecision;
17
+ exports.smallestRole = smallestRole;
18
+ /** The privilege that lets its holder put people into roles - a role carrying it hands that power on. */
19
+ exports.ROLE_GRANTING_PRIVILEGE = { category: 'user', privilege: 'mutation' };
20
+ /** How long a request waits for a decision (questions Q7). */
21
+ exports.ACCESS_REQUEST_PENDING_DAYS = 14;
22
+ const same = (a, b) => a.category === b.category && a.privilege === b.privilege;
23
+ const nameOf = (p) => `${p.category}:${p.privilege}`;
24
+ /** A pending request past its time is expired, whatever the column still says. */
25
+ function effectiveState(state, expiresAt, now) {
26
+ if (state !== 'PENDING' || !expiresAt)
27
+ return state;
28
+ const at = new Date(expiresAt);
29
+ return isNaN(at.getTime()) || at.getTime() > now.getTime() ? state : 'EXPIRED';
30
+ }
31
+ function expiryFrom(now, days = exports.ACCESS_REQUEST_PENDING_DAYS) {
32
+ return new Date(now.getTime() + days * 24 * 60 * 60 * 1000);
33
+ }
34
+ function reviewAccessDecision(facts) {
35
+ const issues = [];
36
+ const { request } = facts;
37
+ if (!request) {
38
+ return [{ code: 'access-request-not-found', blocking: true, field: 'request' }];
39
+ }
40
+ const state = effectiveState(request.state, request.expiresAt, facts.now);
41
+ if (state !== 'PENDING') {
42
+ issues.push({ code: 'request-not-pending', blocking: true, field: 'request', params: { state } });
43
+ }
44
+ if (request.requesterId === facts.deciderId) {
45
+ issues.push({ code: 'own-request', blocking: true, field: 'request' });
46
+ }
47
+ if (!facts.requester || !facts.requester.inDomain || !facts.requester.active) {
48
+ issues.push({ code: 'requester-unavailable', blocking: true, field: 'request' });
49
+ }
50
+ if (facts.decision === 'DENY') {
51
+ if (!facts.reason || !facts.reason.trim()) {
52
+ issues.push({ code: 'deny-needs-reason', blocking: true, field: 'reason' });
53
+ }
54
+ }
55
+ else {
56
+ issues.push(...grantIssues(facts, { category: request.category, privilege: request.privilege }));
57
+ }
58
+ if (facts.deniedBefore) {
59
+ issues.push({
60
+ code: 'denied-before',
61
+ blocking: false,
62
+ field: 'request',
63
+ params: { by: facts.deniedBefore.by, at: new Date(facts.deniedBefore.at).toISOString(), reason: facts.deniedBefore.reason ?? '' }
64
+ });
65
+ }
66
+ return issues;
67
+ }
68
+ function grantIssues(facts, asked) {
69
+ const issues = [];
70
+ if (facts.requesterHoldsIt) {
71
+ issues.push({ code: 'already-holds', blocking: true, field: 'request', params: { privilege: nameOf(asked) } });
72
+ }
73
+ if (!facts.role) {
74
+ issues.push({
75
+ code: facts.roleNamedButNotAssignable ? 'role-not-assignable' : 'role-required',
76
+ blocking: true,
77
+ field: 'role'
78
+ });
79
+ return issues;
80
+ }
81
+ const { role } = facts;
82
+ if (!role.privileges.some(p => same(p, asked))) {
83
+ issues.push({ code: 'role-does-not-grant', blocking: true, field: 'role', params: { role: role.name, privilege: nameOf(asked) } });
84
+ return issues;
85
+ }
86
+ const more = role.privileges.filter(p => !same(p, asked));
87
+ if (more.length > 0) {
88
+ issues.push({
89
+ code: 'role-grants-more',
90
+ blocking: false,
91
+ field: 'role',
92
+ params: { role: role.name, count: more.length, privileges: more.map(nameOf).join(', ') }
93
+ });
94
+ }
95
+ if (!same(asked, exports.ROLE_GRANTING_PRIVILEGE) && role.privileges.some(p => same(p, exports.ROLE_GRANTING_PRIVILEGE))) {
96
+ issues.push({ code: 'role-grants-admin', blocking: false, field: 'role', params: { role: role.name } });
97
+ }
98
+ const smaller = facts.candidates
99
+ .filter(c => c.id !== role.id && c.privilegeCount < role.privileges.length)
100
+ .sort((a, b) => a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0];
101
+ if (smaller) {
102
+ issues.push({ code: 'smaller-role-exists', blocking: false, field: 'role', params: { role: smaller.name, count: smaller.privilegeCount } });
103
+ }
104
+ if (!facts.deciderIsOwner) {
105
+ const lacking = role.privileges.filter(p => !facts.deciderPrivileges.some(d => same(d, p)));
106
+ if (lacking.length > 0) {
107
+ /* Told, not refused (questions Q6) - the role screen gives the same role today; ruling both is a separate decision. */
108
+ issues.push({ code: 'decider-lacks-it', blocking: false, field: 'role', params: { privileges: lacking.map(nameOf).join(', ') } });
109
+ }
110
+ }
111
+ return issues;
112
+ }
113
+ /** The smallest candidate - what the draft card proposes by default. Ties go by name, so the answer is the same every time. */
114
+ function smallestRole(candidates) {
115
+ return [...candidates].sort((a, b) => a.privilegeCount - b.privilegeCount || a.name.localeCompare(b.name))[0];
116
+ }
117
+ const isBlocked = (issues) => issues.some(issue => issue.blocking);
118
+ exports.isBlocked = isBlocked;
119
+ //# sourceMappingURL=access-request-review.js.map
@@ -0,0 +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 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 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"]}
@@ -0,0 +1,23 @@
1
+ import { EntityManager, WhereExpressionBuilder } from 'typeorm';
2
+ import { Domain } from '@things-factory/shell';
3
+ import { Role } from '../service/role/role.js';
4
+ import { User } from '../service/user/user.js';
5
+ /** Restricts `alias` (a Role) to what `domain` can assign. Parameters are named after the alias, so two uses do not collide. */
6
+ export declare function whereAssignableBy<T extends WhereExpressionBuilder>(qb: T, alias: string, domain: Domain): T;
7
+ /** The roles among `ids` that `domain` can assign. */
8
+ export declare function findAssignableRoles(ids: string[], domain: Domain, tx?: EntityManager): Promise<Role[]>;
9
+ /** Refuses with the ids that are not this domain's to assign - a name and values, spoken by `SpeakRefusals`. */
10
+ export declare function requireAssignableRoles(ids: string[], domain: Domain, tx?: EntityManager): Promise<Role[]>;
11
+ /**
12
+ * Puts `user` into `add` and takes them out of `remove` - every id found again in `domain`'s scope first.
13
+ * Roles the user holds outside that scope are not touched, whatever `remove` says.
14
+ *
15
+ * @returns the saved user, and the roles that were added (not already held).
16
+ */
17
+ export declare function assignRoles(user: User, { add, remove }: {
18
+ add?: string[];
19
+ remove?: string[];
20
+ }, domain: Domain, tx?: EntityManager): Promise<{
21
+ user: User;
22
+ added: Role[];
23
+ }>;
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.whereAssignableBy = whereAssignableBy;
4
+ exports.findAssignableRoles = findAssignableRoles;
5
+ exports.requireAssignableRoles = requireAssignableRoles;
6
+ exports.assignRoles = assignRoles;
7
+ const typeorm_1 = require("typeorm");
8
+ const shell_1 = require("@things-factory/shell");
9
+ const granted_role_js_1 = require("../service/granted-role/granted-role.js");
10
+ const role_js_1 = require("../service/role/role.js");
11
+ const user_js_1 = require("../service/user/user.js");
12
+ /*
13
+ * The roles a domain can assign - and the one place a person is put into or taken out of them.
14
+ *
15
+ * ── The scope ─────────────────────────────────────────────────────────────
16
+ * Three kinds: the domain's own roles, its parent's (one level, as `userRoles` · `myRoles`), and roles a
17
+ * partner granted to it (`grantedRoles`). `User.roles` answers exactly this scope (cb9a41e040), so the
18
+ * user screen's checkboxes and what can be saved are the same set.
19
+ *
20
+ * ── Why the ids are found again ──────────────────────────────────────────
21
+ * `updateUserRoles` took the client's `selectedRoles` and concatenated them onto the user, and took its
22
+ * `availableRoles` and removed every held role with one of those ids. Neither was looked at. A role id of
23
+ * another tenant was added as sent, and another tenant's role id in `availableRoles` took that role away from
24
+ * the person in the other domain. Both are found here, in this domain's scope, by id. A role to add that is not
25
+ * there is refused by name (ADR-0103 decision 2); a role to remove that is not there is not this domain's to take.
26
+ *
27
+ * The access request grants through this too, so the two ways into a role are one.
28
+ */
29
+ /** Restricts `alias` (a Role) to what `domain` can assign. Parameters are named after the alias, so two uses do not collide. */
30
+ function whereAssignableBy(qb, alias, domain) {
31
+ const domainIds = [domain.id, domain.parentId].filter(Boolean);
32
+ const own = `${alias}_assignableDomainIds`;
33
+ const grantedTo = `${alias}_assignableGrantedTo`;
34
+ qb.andWhere(new typeorm_1.Brackets(inner => {
35
+ inner.where(`${alias}.domain IN (:...${own})`, { [own]: domainIds });
36
+ inner.orWhere(sub => {
37
+ const granted = sub
38
+ .subQuery()
39
+ .select('GRANTED.role')
40
+ .from(granted_role_js_1.GrantedRole, 'GRANTED')
41
+ .where(`GRANTED.domain = :${grantedTo}`, { [grantedTo]: domain.id })
42
+ .getQuery();
43
+ return `${alias}.id IN ${granted}`;
44
+ });
45
+ }));
46
+ return qb;
47
+ }
48
+ /** The roles among `ids` that `domain` can assign. */
49
+ async function findAssignableRoles(ids, domain, tx) {
50
+ const unique = [...new Set((ids || []).filter(Boolean))];
51
+ if (unique.length === 0)
52
+ return [];
53
+ const qb = (0, shell_1.getRepository)(role_js_1.Role, tx).createQueryBuilder('ROLE').where({ id: (0, typeorm_1.In)(unique) });
54
+ return await whereAssignableBy(qb, 'ROLE', domain).getMany();
55
+ }
56
+ /** Refuses with the ids that are not this domain's to assign - a name and values, spoken by `SpeakRefusals`. */
57
+ async function requireAssignableRoles(ids, domain, tx) {
58
+ const found = await findAssignableRoles(ids, domain, tx);
59
+ const foundIds = new Set(found.map(role => role.id));
60
+ const missing = [...new Set((ids || []).filter(Boolean))].filter(id => !foundIds.has(id));
61
+ if (missing.length > 0) {
62
+ throw new shell_1.Refusal('role-not-assignable-here', { roles: missing.join(', ') }, `This domain cannot assign the role(s) ${missing.join(', ')}.`);
63
+ }
64
+ return found;
65
+ }
66
+ /**
67
+ * Puts `user` into `add` and takes them out of `remove` - every id found again in `domain`'s scope first.
68
+ * Roles the user holds outside that scope are not touched, whatever `remove` says.
69
+ *
70
+ * @returns the saved user, and the roles that were added (not already held).
71
+ */
72
+ async function assignRoles(user, { add = [], remove = [] }, domain, tx) {
73
+ const adding = await requireAssignableRoles(add, domain, tx);
74
+ /*
75
+ * `remove` is the caller's scope ("every role this screen shows"), not a demand - `user-detail.ts` builds it
76
+ * from the list it read. Inside this domain's scope it is honoured; an id outside it is a role this domain
77
+ * does not own, and it stays with the person.
78
+ */
79
+ const removing = await findAssignableRoles(remove, domain, tx);
80
+ const addingIds = new Set(adding.map(role => role.id));
81
+ const removingIds = new Set(removing.map(role => role.id).filter(id => !addingIds.has(id)));
82
+ const held = (user.roles || []).filter(role => !removingIds.has(role.id));
83
+ const heldIds = new Set(held.map(role => role.id));
84
+ const added = adding.filter(role => !heldIds.has(role.id));
85
+ user.roles = [...held, ...added];
86
+ const saved = await (0, shell_1.getRepository)(user_js_1.User, tx).save(user);
87
+ return { user: saved, added };
88
+ }
89
+ //# sourceMappingURL=assignable-roles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assignable-roles.js","sourceRoot":"","sources":["../../server/utils/assignable-roles.ts"],"names":[],"mappings":"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","sourcesContent":["import { Brackets, EntityManager, In, SelectQueryBuilder, WhereExpressionBuilder } from 'typeorm'\n\nimport { Domain, getRepository, Refusal } from '@things-factory/shell'\n\nimport { GrantedRole } from '../service/granted-role/granted-role.js'\nimport { Role } from '../service/role/role.js'\nimport { User } from '../service/user/user.js'\n\n/*\n * The roles a domain can assign - and the one place a person is put into or taken out of them.\n *\n * ── The scope ─────────────────────────────────────────────────────────────\n * Three kinds: the domain's own roles, its parent's (one level, as `userRoles` · `myRoles`), and roles a\n * partner granted to it (`grantedRoles`). `User.roles` answers exactly this scope (cb9a41e040), so the\n * user screen's checkboxes and what can be saved are the same set.\n *\n * ── Why the ids are found again ──────────────────────────────────────────\n * `updateUserRoles` took the client's `selectedRoles` and concatenated them onto the user, and took its\n * `availableRoles` and removed every held role with one of those ids. Neither was looked at. A role id of\n * another tenant was added as sent, and another tenant's role id in `availableRoles` took that role away from\n * the person in the other domain. Both are found here, in this domain's scope, by id. A role to add that is not\n * there is refused by name (ADR-0103 decision 2); a role to remove that is not there is not this domain's to take.\n *\n * The access request grants through this too, so the two ways into a role are one.\n */\n\n/** Restricts `alias` (a Role) to what `domain` can assign. Parameters are named after the alias, so two uses do not collide. */\nexport function whereAssignableBy<T extends WhereExpressionBuilder>(qb: T, alias: string, domain: Domain): T {\n const domainIds = [domain.id, domain.parentId].filter(Boolean)\n const own = `${alias}_assignableDomainIds`\n const grantedTo = `${alias}_assignableGrantedTo`\n\n qb.andWhere(\n new Brackets(inner => {\n inner.where(`${alias}.domain IN (:...${own})`, { [own]: domainIds })\n inner.orWhere(sub => {\n const granted = (sub as unknown as SelectQueryBuilder<Role>)\n .subQuery()\n .select('GRANTED.role')\n .from(GrantedRole, 'GRANTED')\n .where(`GRANTED.domain = :${grantedTo}`, { [grantedTo]: domain.id })\n .getQuery()\n return `${alias}.id IN ${granted}`\n })\n })\n )\n return qb\n}\n\n/** The roles among `ids` that `domain` can assign. */\nexport async function findAssignableRoles(ids: string[], domain: Domain, tx?: EntityManager): Promise<Role[]> {\n const unique = [...new Set((ids || []).filter(Boolean))]\n if (unique.length === 0) return []\n\n const qb = getRepository(Role, tx).createQueryBuilder('ROLE').where({ id: In(unique) })\n return await whereAssignableBy(qb, 'ROLE', domain).getMany()\n}\n\n/** Refuses with the ids that are not this domain's to assign - a name and values, spoken by `SpeakRefusals`. */\nexport async function requireAssignableRoles(ids: string[], domain: Domain, tx?: EntityManager): Promise<Role[]> {\n const found = await findAssignableRoles(ids, domain, tx)\n const foundIds = new Set(found.map(role => role.id))\n const missing = [...new Set((ids || []).filter(Boolean))].filter(id => !foundIds.has(id))\n\n if (missing.length > 0) {\n throw new Refusal(\n 'role-not-assignable-here',\n { roles: missing.join(', ') },\n `This domain cannot assign the role(s) ${missing.join(', ')}.`\n )\n }\n return found\n}\n\n/**\n * Puts `user` into `add` and takes them out of `remove` - every id found again in `domain`'s scope first.\n * Roles the user holds outside that scope are not touched, whatever `remove` says.\n *\n * @returns the saved user, and the roles that were added (not already held).\n */\nexport async function assignRoles(\n user: User,\n { add = [], remove = [] }: { add?: string[]; remove?: string[] },\n domain: Domain,\n tx?: EntityManager\n): Promise<{ user: User; added: Role[] }> {\n const adding = await requireAssignableRoles(add, domain, tx)\n /*\n * `remove` is the caller's scope (\"every role this screen shows\"), not a demand - `user-detail.ts` builds it\n * from the list it read. Inside this domain's scope it is honoured; an id outside it is a role this domain\n * does not own, and it stays with the person.\n */\n const removing = await findAssignableRoles(remove, domain, tx)\n\n const addingIds = new Set(adding.map(role => role.id))\n const removingIds = new Set(removing.map(role => role.id).filter(id => !addingIds.has(id)))\n\n const held = (user.roles || []).filter(role => !removingIds.has(role.id))\n const heldIds = new Set(held.map(role => role.id))\n const added = adding.filter(role => !heldIds.has(role.id))\n\n user.roles = [...held, ...added]\n const saved = await getRepository(User, tx).save(user)\n return { user: saved, added }\n}\n"]}
@@ -1,7 +1,34 @@
1
1
  import { Domain } from '@things-factory/shell';
2
2
  import { PrivilegeObject } from '../service/privilege/privilege.js';
3
3
  import { User } from '../service/user/user.js';
4
- export declare function checkPermission(privilegeObject: PrivilegeObject, user: User, domain: Domain, unsafeIP?: boolean, prohibitedPrivileges?: {
4
+ /**
5
+ * Why a gate said no - what the person can do about it differs (ADR-0103 decision 1).
6
+ *
7
+ * role none of their roles holds category:privilege - a role can be given (an access request)
8
+ * address refused from where they are: the domain's protected address list
9
+ * license refused by what this installation is licensed for
10
+ * ownership the declaration opens only to domain or system owners - no role gives it
11
+ * unknown a prohibition that does not say where it came from - the reason is not made up
12
+ */
13
+ export type RefusalCause = 'role' | 'address' | 'license' | 'ownership' | 'unknown';
14
+ /**
15
+ * A privilege refused by the request, whatever the person's roles. `by` says who refused it:
16
+ * the domain's address list (shell domain-middleware) or the installation's license (board-service).
17
+ */
18
+ export type ProhibitedPrivilege = {
5
19
  category: string;
6
20
  privilege: string;
7
- }[]): Promise<boolean>;
21
+ by?: 'address' | 'license';
22
+ };
23
+ export type PermissionJudgement = {
24
+ permitted: true;
25
+ } | {
26
+ permitted: false;
27
+ because: RefusalCause;
28
+ };
29
+ /**
30
+ * The gate, with its reason. `checkPermission` answers only the boolean and is what most callers
31
+ * want; the privilege directive reads this so a refusal can say which of the four it was.
32
+ */
33
+ export declare function judgePermission(privilegeObject: PrivilegeObject, user: User, domain: Domain, unsafeIP?: boolean, prohibitedPrivileges?: ProhibitedPrivilege[]): Promise<PermissionJudgement>;
34
+ export declare function checkPermission(privilegeObject: PrivilegeObject, user: User, domain: Domain, unsafeIP?: boolean, prohibitedPrivileges?: ProhibitedPrivilege[]): Promise<boolean>;
@@ -1,19 +1,51 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.judgePermission = judgePermission;
3
4
  exports.checkPermission = checkPermission;
4
5
  const user_js_1 = require("../service/user/user.js");
5
- async function checkPermission(privilegeObject, user, domain, unsafeIP, prohibitedPrivileges) {
6
+ const PERMITTED = { permitted: true };
7
+ function refused(because) {
8
+ return { permitted: false, because };
9
+ }
10
+ function prohibitionOf(prohibitedPrivileges, category, privilege) {
11
+ return (prohibitedPrivileges || []).find(pp => pp.category == category && pp.privilege == privilege);
12
+ }
13
+ /**
14
+ * The gate, with its reason. `checkPermission` answers only the boolean and is what most callers
15
+ * want; the privilege directive reads this so a refusal can say which of the four it was.
16
+ */
17
+ async function judgePermission(privilegeObject, user, domain, unsafeIP, prohibitedPrivileges) {
6
18
  if (!privilegeObject) {
7
- return true;
19
+ return PERMITTED;
8
20
  }
9
21
  const { owner: domainOwnerGranted, super: superUserGranted, category, privilege } = privilegeObject;
22
+ /* Ownership as the gate has always asked it - `process` carries the answer (domain-authenticate-middleware installs it). */
23
+ const ownershipOpens = async () => !!((domainOwnerGranted && (await process.domainOwnerGranted(domain, user))) ||
24
+ (superUserGranted && (await process.superUserGranted(domain, user))));
25
+ /*
26
+ * From an unsafe address ownership never opened anything and was never asked. It is asked now only on the
27
+ * way to a refusal, to tell "address" from "role" - and a missing answer is "no", so the gate refuses as it did.
28
+ */
29
+ const ownershipWouldHaveOpened = async () => {
30
+ try {
31
+ return await ownershipOpens();
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ };
10
37
  if (unsafeIP) {
11
38
  if (privilege && category) {
12
39
  // unsafeIP 상황에서는 ownership granted는 적용되지 않는다.
13
- if ((prohibitedPrivileges || []).find(pp => pp.category == category && pp.privilege == privilege)) {
14
- return false;
40
+ const prohibition = prohibitionOf(prohibitedPrivileges, category, privilege);
41
+ if (prohibition) {
42
+ return refused(prohibition.by ?? 'unknown');
43
+ }
44
+ if (await user_js_1.User.hasPrivilege(privilege, category, domain, user)) {
45
+ return PERMITTED;
15
46
  }
16
- return await user_js_1.User.hasPrivilege(privilege, category, domain, user);
47
+ /* An owner without the role would pass from a safe address - what stopped them is where they are. */
48
+ return refused((await ownershipWouldHaveOpened()) ? 'address' : 'role');
17
49
  }
18
50
  /*
19
51
  * Ownership does not count from an unsafe address, so a declaration that rests on
@@ -25,22 +57,24 @@ async function checkPermission(privilegeObject, user, domain, unsafeIP, prohibit
25
57
  * `privilege-directive.ts` now refuses to build a declaration that names nothing, but
26
58
  * this function is exported and called directly, so the rule is fixed here too.
27
59
  */
28
- return false;
60
+ return refused('address');
29
61
  }
30
62
  else {
31
63
  if (!privilege || !category) {
32
64
  // privilege, category가 설정되지 않은 경우에는 ownership granted만을 적용한다.
33
- return ((domainOwnerGranted && (await process.domainOwnerGranted(domain, user))) ||
34
- (superUserGranted && (await process.superUserGranted(domain, user))));
65
+ return (await ownershipOpens()) ? PERMITTED : refused('ownership');
35
66
  }
36
- if ((domainOwnerGranted && (await process.domainOwnerGranted(domain, user))) ||
37
- (superUserGranted && (await process.superUserGranted(domain, user)))) {
38
- return true;
67
+ if (await ownershipOpens()) {
68
+ return PERMITTED;
39
69
  }
40
- if ((prohibitedPrivileges || []).find(pp => pp.category == category && pp.privilege == privilege)) {
41
- return false;
70
+ const prohibition = prohibitionOf(prohibitedPrivileges, category, privilege);
71
+ if (prohibition) {
72
+ return refused(prohibition.by ?? 'unknown');
42
73
  }
43
- return await user_js_1.User.hasPrivilege(privilege, category, domain, user);
74
+ return (await user_js_1.User.hasPrivilege(privilege, category, domain, user)) ? PERMITTED : refused('role');
44
75
  }
45
76
  }
77
+ async function checkPermission(privilegeObject, user, domain, unsafeIP, prohibitedPrivileges) {
78
+ return (await judgePermission(privilegeObject, user, domain, unsafeIP, prohibitedPrivileges)).permitted;
79
+ }
46
80
  //# sourceMappingURL=check-permission.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"check-permission.js","sourceRoot":"","sources":["../../server/utils/check-permission.ts"],"names":[],"mappings":";;AAIA,0CAwDC;AA1DD,qDAA8C;AAEvC,KAAK,UAAU,eAAe,CACnC,eAAgC,EAChC,IAAU,EACV,MAAc,EACd,QAAkB,EAClB,oBAAgE;IAEhE,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,gBAAgB,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,eAAe,CAAA;IAEnG,IAAI,QAAQ,EAAE,CAAC;QACb,IAAI,SAAS,IAAI,QAAQ,EAAE,CAAC;YAC1B,8CAA8C;YAC9C,IAAI,CAAC,oBAAoB,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,IAAI,QAAQ,IAAI,EAAE,CAAC,SAAS,IAAI,SAAS,CAAC,EAAE,CAAC;gBAClG,OAAO,KAAK,CAAA;YACd,CAAC;YAED,OAAO,MAAM,cAAI,CAAC,YAAY,CAAC,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,CAAA;QACnE,CAAC;QAED;;;;;;;;;WASG;QACH,OAAO,KAAK,CAAA;IACd,CAAC;SAAM,CAAC;QACN,IAAI,CAAC,SAAS,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC5B,8DAA8D;YAC9D,OAAO,CACL,CAAC,kBAAkB,IAAI,CAAC,MAAM,OAAO,CAAC,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;gBACxE,CAAC,gBAAgB,IAAI,CAAC,MAAM,OAAO,CAAC,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,CACrE,CAAA;QACH,CAAC;QAED,IACE,CAAC,kBAAkB,IAAI,CAAC,MAAM,OAAO,CAAC,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;YACxE,CAAC,gBAAgB,IAAI,CAAC,MAAM,OAAO,CAAC,gBAAgB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,EACpE,CAAC;YACD,OAAO,IAAI,CAAA;QACb,CAAC;QAED,IAAI,CAAC,oBAAoB,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,QAAQ,IAAI,QAAQ,IAAI,EAAE,CAAC,SAAS,IAAI,SAAS,CAAC,EAAE,CAAC;YAClG,OAAO,KAAK,CAAA;QACd,CAAC;QAED,OAAO,MAAM,cAAI,CAAC,YAAY,CAAC,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,CAAA;IACnE,CAAC;AACH,CAAC","sourcesContent":["import { Domain } from '@things-factory/shell'\nimport { PrivilegeObject } from '../service/privilege/privilege.js'\nimport { User } from '../service/user/user.js'\n\nexport async function checkPermission(\n privilegeObject: PrivilegeObject,\n user: User,\n domain: Domain,\n unsafeIP?: boolean,\n prohibitedPrivileges?: { category: string; privilege: string }[]\n): Promise<boolean> {\n if (!privilegeObject) {\n return true\n }\n\n const { owner: domainOwnerGranted, super: superUserGranted, category, privilege } = privilegeObject\n\n if (unsafeIP) {\n if (privilege && category) {\n // unsafeIP 상황에서는 ownership granted는 적용되지 않는다.\n if ((prohibitedPrivileges || []).find(pp => pp.category == category && pp.privilege == privilege)) {\n return false\n }\n\n return await User.hasPrivilege(privilege, category, domain, user)\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 false\n } else {\n if (!privilege || !category) {\n // privilege, category가 설정되지 않은 경우에는 ownership granted만을 적용한다.\n return (\n (domainOwnerGranted && (await process.domainOwnerGranted(domain, user))) ||\n (superUserGranted && (await process.superUserGranted(domain, user)))\n )\n }\n\n if (\n (domainOwnerGranted && (await process.domainOwnerGranted(domain, user))) ||\n (superUserGranted && (await process.superUserGranted(domain, user)))\n ) {\n return true\n }\n\n if ((prohibitedPrivileges || []).find(pp => pp.category == category && pp.privilege == privilege)) {\n return false\n }\n\n return await User.hasPrivilege(privilege, category, domain, user)\n }\n}\n"]}
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\nexport type PermissionJudgement = { permitted: true } | { permitted: false; because: RefusalCause }\n\nconst PERMITTED: PermissionJudgement = { permitted: true }\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 /* Ownership as the gate has always asked it - `process` carries the answer (domain-authenticate-middleware installs it). */\n const ownershipOpens = async () =>\n !!(\n (domainOwnerGranted && (await (process as any).domainOwnerGranted(domain, user))) ||\n (superUserGranted && (await (process as any).superUserGranted(domain, user)))\n )\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 PERMITTED\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 return (await ownershipOpens()) ? PERMITTED : refused('ownership')\n }\n\n if (await ownershipOpens()) {\n return PERMITTED\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)) ? PERMITTED : 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.79",
3
+ "version": "10.1.106",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -34,9 +34,9 @@
34
34
  "@reduxjs/toolkit": "^2.2.5",
35
35
  "@simplewebauthn/browser": "^13.0.0",
36
36
  "@simplewebauthn/server": "^13.0.0",
37
- "@things-factory/email-base": "^10.1.79",
37
+ "@things-factory/email-base": "^10.1.106",
38
38
  "@things-factory/env": "^10.1.20",
39
- "@things-factory/shell": "^10.1.79",
39
+ "@things-factory/shell": "^10.1.106",
40
40
  "@things-factory/utils": "^10.1.20",
41
41
  "@types/webappsec-credential-management": "^0.6.9",
42
42
  "jsonwebtoken": "^9.0.0",
@@ -48,5 +48,5 @@
48
48
  "passport-jwt": "^4.0.0",
49
49
  "passport-local": "^1.0.0"
50
50
  },
51
- "gitHead": "94cde9ba17dfdddb0a16139b71e5a7e370977e4a"
51
+ "gitHead": "8d78c14708420773ec32d18b8dd44c300771c44d"
52
52
  }