@things-factory/auth-base 10.1.6 → 10.1.16
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/CHANGELOG.md +47 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/index.js +16 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/router/auth-private-process-router.js +20 -1
- package/dist-server/router/auth-private-process-router.js.map +1 -1
- package/dist-server/service/index.d.ts +3 -0
- package/dist-server/service/index.js +16 -11
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/privilege/privilege-axis.d.ts +72 -0
- package/dist-server/service/privilege/privilege-axis.js +188 -0
- package/dist-server/service/privilege/privilege-axis.js.map +1 -0
- package/dist-server/service/privilege/privilege-directive.d.ts +10 -41
- package/dist-server/service/privilege/privilege-directive.js +62 -74
- package/dist-server/service/privilege/privilege-directive.js.map +1 -1
- package/dist-server/service/role/role-mutation.js +2 -2
- package/dist-server/service/role/role-mutation.js.map +1 -1
- package/dist-server/service/role/role-query.d.ts +43 -1
- package/dist-server/service/role/role-query.js +76 -33
- package/dist-server/service/role/role-query.js.map +1 -1
- package/dist-server/service/role-template/index.d.ts +4 -0
- package/dist-server/service/role-template/index.js +9 -0
- package/dist-server/service/role-template/index.js.map +1 -0
- package/dist-server/service/role-template/role-template-mutation.d.ts +39 -0
- package/dist-server/service/role-template/role-template-mutation.js +124 -0
- package/dist-server/service/role-template/role-template-mutation.js.map +1 -0
- package/dist-server/service/role-template/role-template-query.d.ts +11 -0
- package/dist-server/service/role-template/role-template-query.js +79 -0
- package/dist-server/service/role-template/role-template-query.js.map +1 -0
- package/dist-server/service/role-template/role-template-types.d.ts +51 -0
- package/dist-server/service/role-template/role-template-types.js +71 -0
- package/dist-server/service/role-template/role-template-types.js.map +1 -0
- package/dist-server/service/role-template/role-template.d.ts +110 -0
- package/dist-server/service/role-template/role-template.js +147 -0
- package/dist-server/service/role-template/role-template.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/utils/check-permission.js +11 -2
- package/dist-server/utils/check-permission.js.map +1 -1
- package/package.json +4 -4
- package/tests/domain-inheritance-sentinel.test.ts +93 -0
- package/tests/permission-gate.test.ts +16 -12
- package/tests/privilege-axis.test.ts +157 -0
- package/tests/privilege-directive.test.ts +70 -81
- package/tests/role-privileges-db.test.ts +98 -0
- package/tests/role-template-seed-db.test.ts +179 -0
- package/tests/role-template.test.ts +162 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RoleTemplateMutation = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const type_graphql_1 = require("type-graphql");
|
|
6
|
+
const privilege_js_1 = require("../privilege/privilege.js");
|
|
7
|
+
const role_js_1 = require("../role/role.js");
|
|
8
|
+
const role_template_js_1 = require("./role-template.js");
|
|
9
|
+
let RoleTemplateMutation = class RoleTemplateMutation {
|
|
10
|
+
/**
|
|
11
|
+
* Creates a role from a template — once per name.
|
|
12
|
+
*
|
|
13
|
+
* ── Why an existing name is used rather than replaced ────────────────────────
|
|
14
|
+
* Approval lines store a role's **id** (`worklist` resolves an approver of type `Role`
|
|
15
|
+
* by id). Creating a second QUALITY-INSPECTOR would leave every approval routed at the
|
|
16
|
+
* first one while the administrator edits the second, and nothing would report it: the
|
|
17
|
+
* screen shows two roles and the approvals go to the one nobody is looking at.
|
|
18
|
+
*
|
|
19
|
+
* So an existing role of that name is used as it stands. operato-plant's seed already
|
|
20
|
+
* behaves this way (`seed-demo-org.ts` looks the role up and only saves when absent);
|
|
21
|
+
* this is the same rule, stated once.
|
|
22
|
+
*
|
|
23
|
+
* ── Why privileges are added and never removed ───────────────────────────────
|
|
24
|
+
* Two applications may declare a template with the same role name on purpose, and the
|
|
25
|
+
* privileges are then the union — to a user "quality inspector" is one seat. The union
|
|
26
|
+
* only widens: if a template later drops a privilege it stays on roles already seeded,
|
|
27
|
+
* because the seed runs once and the role belongs to the administrator afterwards.
|
|
28
|
+
*
|
|
29
|
+
* A privilege that is no longer declared anywhere is already handled elsewhere and not
|
|
30
|
+
* here — `isDeprecatedPrivilege` marks it, the role screen strikes it through and refuses
|
|
31
|
+
* to switch it back on, and removing it is the administrator's call. Reclaiming it from
|
|
32
|
+
* this side would mean overwriting, and would be unable to tell a stale grant from one
|
|
33
|
+
* the administrator added on purpose.
|
|
34
|
+
*/
|
|
35
|
+
async createRoleFromTemplate(templateId, name, context) {
|
|
36
|
+
const { domain, user, tx } = context.state;
|
|
37
|
+
const template = findTemplate(templateId);
|
|
38
|
+
if (!template) {
|
|
39
|
+
throw new Error(`There is no role template "${templateId}" in this installation.`);
|
|
40
|
+
}
|
|
41
|
+
const roleName = (name || template.name).trim();
|
|
42
|
+
if (!roleName) {
|
|
43
|
+
throw new Error('A role needs a name.');
|
|
44
|
+
}
|
|
45
|
+
const roleRepository = tx.getRepository(role_js_1.Role);
|
|
46
|
+
let role = await roleRepository.findOne({
|
|
47
|
+
where: { domain: { id: domain.id }, name: roleName },
|
|
48
|
+
relations: ['privileges']
|
|
49
|
+
});
|
|
50
|
+
if (!role) {
|
|
51
|
+
role = await roleRepository.save({
|
|
52
|
+
domain,
|
|
53
|
+
name: roleName,
|
|
54
|
+
description: template.description,
|
|
55
|
+
creator: user,
|
|
56
|
+
updater: user
|
|
57
|
+
});
|
|
58
|
+
role = await roleRepository.findOne({
|
|
59
|
+
where: { id: role.id },
|
|
60
|
+
relations: ['privileges']
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const held = new Set((role.privileges || []).map(privilege => `${privilege.category} ${privilege.name}`));
|
|
64
|
+
const wanted = template.grants.flatMap(grant => grant.axes.map(axis => ({ category: grant.category, name: axis })));
|
|
65
|
+
/*
|
|
66
|
+
* Privileges have no domain — the whole server shares one set and what differs per
|
|
67
|
+
* domain is only which role holds them (`Privilege` has no domain column; auth-base's
|
|
68
|
+
* boot creates them by category and name alone). Filtering by domain finds nothing.
|
|
69
|
+
*/
|
|
70
|
+
const privilegeRepository = tx.getRepository(privilege_js_1.Privilege);
|
|
71
|
+
const added = [];
|
|
72
|
+
for (const one of wanted) {
|
|
73
|
+
if (held.has(`${one.category} ${one.name}`)) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const privilege = await privilegeRepository.findOne({ where: { category: one.category, name: one.name } });
|
|
77
|
+
/*
|
|
78
|
+
* A privilege this installation does not declare is skipped, never created. Creating
|
|
79
|
+
* it here would make a row no `@privilege` asks for: the role would look as if it
|
|
80
|
+
* had been granted something, and open nothing. Granted-but-closed is the worst of
|
|
81
|
+
* the three outcomes. `roleTemplates` reports these as `missingCount` before the
|
|
82
|
+
* administrator picks the template.
|
|
83
|
+
*/
|
|
84
|
+
if (privilege) {
|
|
85
|
+
added.push(privilege);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/*
|
|
89
|
+
* Nothing records that this role came from a template.
|
|
90
|
+
*
|
|
91
|
+
* A `templateVersions` column stood here so a later change to the template could be
|
|
92
|
+
* reported. It bought less than it looked like: what an administrator needs is "these
|
|
93
|
+
* two privileges were added to the template", and that is the template's current
|
|
94
|
+
* grants against the role's current privileges — computed, not stored. Telling them
|
|
95
|
+
* apart from a privilege the administrator removed on purpose would take the seeded
|
|
96
|
+
* content itself, not a version of it, and no screen asks for either yet.
|
|
97
|
+
*
|
|
98
|
+
* So the seed ends here. If reporting template drift is designed later, what to store
|
|
99
|
+
* follows from that design rather than being guessed at now.
|
|
100
|
+
*/
|
|
101
|
+
role.privileges = [...(role.privileges || []), ...added];
|
|
102
|
+
role.updater = user;
|
|
103
|
+
return await roleRepository.save(role);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
exports.RoleTemplateMutation = RoleTemplateMutation;
|
|
107
|
+
tslib_1.__decorate([
|
|
108
|
+
(0, type_graphql_1.Directive)('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)'),
|
|
109
|
+
(0, type_graphql_1.Directive)('@transaction'),
|
|
110
|
+
(0, type_graphql_1.Mutation)(returns => role_js_1.Role, { description: 'To create a role from a role template' }),
|
|
111
|
+
tslib_1.__param(0, (0, type_graphql_1.Arg)('templateId')),
|
|
112
|
+
tslib_1.__param(1, (0, type_graphql_1.Arg)('name', { nullable: true })),
|
|
113
|
+
tslib_1.__param(2, (0, type_graphql_1.Ctx)()),
|
|
114
|
+
tslib_1.__metadata("design:type", Function),
|
|
115
|
+
tslib_1.__metadata("design:paramtypes", [String, String, Object]),
|
|
116
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
117
|
+
], RoleTemplateMutation.prototype, "createRoleFromTemplate", null);
|
|
118
|
+
exports.RoleTemplateMutation = RoleTemplateMutation = tslib_1.__decorate([
|
|
119
|
+
(0, type_graphql_1.Resolver)(role_js_1.Role)
|
|
120
|
+
], RoleTemplateMutation);
|
|
121
|
+
function findTemplate(id) {
|
|
122
|
+
return (0, role_template_js_1.roleTemplateById)(id) || (0, role_template_js_1.computedRoleTemplates)().find(template => template.id === id);
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=role-template-mutation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role-template-mutation.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-mutation.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Mutation, Resolver } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { Privilege } from '../privilege/privilege.js'\nimport { Role } from '../role/role.js'\nimport { roleTemplateById, computedRoleTemplates, RegisteredRoleTemplate } from './role-template.js'\n\n@Resolver(Role)\nexport class RoleTemplateMutation {\n /**\n * Creates a role from a template — once per name.\n *\n * ── Why an existing name is used rather than replaced ────────────────────────\n * Approval lines store a role's **id** (`worklist` resolves an approver of type `Role`\n * by id). Creating a second QUALITY-INSPECTOR would leave every approval routed at the\n * first one while the administrator edits the second, and nothing would report it: the\n * screen shows two roles and the approvals go to the one nobody is looking at.\n *\n * So an existing role of that name is used as it stands. operato-plant's seed already\n * behaves this way (`seed-demo-org.ts` looks the role up and only saves when absent);\n * this is the same rule, stated once.\n *\n * ── Why privileges are added and never removed ───────────────────────────────\n * Two applications may declare a template with the same role name on purpose, and the\n * privileges are then the union — to a user \"quality inspector\" is one seat. The union\n * only widens: if a template later drops a privilege it stays on roles already seeded,\n * because the seed runs once and the role belongs to the administrator afterwards.\n *\n * A privilege that is no longer declared anywhere is already handled elsewhere and not\n * here — `isDeprecatedPrivilege` marks it, the role screen strikes it through and refuses\n * to switch it back on, and removing it is the administrator's call. Reclaiming it from\n * this side would mean overwriting, and would be unable to tell a stale grant from one\n * the administrator added on purpose.\n */\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Directive('@transaction')\n @Mutation(returns => Role, { description: 'To create a role from a role template' })\n async createRoleFromTemplate(\n @Arg('templateId') templateId: string,\n /**\n * What this plant calls the seat. Defaults to the template's name.\n *\n * The template knows which seat exists in a factory running this product; it does not\n * know what the people there call it. And because a role is also what an approval line\n * points at, the same seat is sometimes needed twice — a day shift and a night shift\n * inspector hold the same privileges and take different approvals. Without a name of\n * their own a template could be used exactly once per domain.\n */\n @Arg('name', { nullable: true }) name: string | undefined,\n @Ctx() context: ResolverContext\n ): Promise<Role> {\n const { domain, user, tx } = context.state\n\n const template = findTemplate(templateId)\n\n if (!template) {\n throw new Error(`There is no role template \"${templateId}\" in this installation.`)\n }\n\n const roleName = (name || template.name).trim()\n\n if (!roleName) {\n throw new Error('A role needs a name.')\n }\n\n const roleRepository = tx.getRepository(Role)\n\n let role = await roleRepository.findOne({\n where: { domain: { id: domain.id }, name: roleName },\n relations: ['privileges']\n })\n\n if (!role) {\n role = await roleRepository.save({\n domain,\n name: roleName,\n description: template.description,\n creator: user,\n updater: user\n } as any)\n\n role = await roleRepository.findOne({\n where: { id: role!.id },\n relations: ['privileges']\n })\n }\n\n const held = new Set((role!.privileges || []).map(privilege => `${privilege.category} ${privilege.name}`))\n const wanted = template.grants.flatMap(grant => grant.axes.map(axis => ({ category: grant.category, name: axis })))\n\n /*\n * Privileges have no domain — the whole server shares one set and what differs per\n * domain is only which role holds them (`Privilege` has no domain column; auth-base's\n * boot creates them by category and name alone). Filtering by domain finds nothing.\n */\n const privilegeRepository = tx.getRepository(Privilege)\n const added: Privilege[] = []\n\n for (const one of wanted) {\n if (held.has(`${one.category} ${one.name}`)) {\n continue\n }\n\n const privilege = await privilegeRepository.findOne({ where: { category: one.category, name: one.name } })\n\n /*\n * A privilege this installation does not declare is skipped, never created. Creating\n * it here would make a row no `@privilege` asks for: the role would look as if it\n * had been granted something, and open nothing. Granted-but-closed is the worst of\n * the three outcomes. `roleTemplates` reports these as `missingCount` before the\n * administrator picks the template.\n */\n if (privilege) {\n added.push(privilege)\n }\n }\n\n /*\n * Nothing records that this role came from a template.\n *\n * A `templateVersions` column stood here so a later change to the template could be\n * reported. It bought less than it looked like: what an administrator needs is \"these\n * two privileges were added to the template\", and that is the template's current\n * grants against the role's current privileges — computed, not stored. Telling them\n * apart from a privilege the administrator removed on purpose would take the seeded\n * content itself, not a version of it, and no screen asks for either yet.\n *\n * So the seed ends here. If reporting template drift is designed later, what to store\n * follows from that design rather than being guessed at now.\n */\n role!.privileges = [...(role!.privileges || []), ...added]\n role!.updater = user\n\n return await roleRepository.save(role!)\n }\n}\n\nfunction findTemplate(id: string): RegisteredRoleTemplate | undefined {\n return roleTemplateById(id) || computedRoleTemplates().find(template => template.id === id)\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { RoleTemplateView } from './role-template-types.js';
|
|
2
|
+
export declare class RoleTemplateQuery {
|
|
3
|
+
/**
|
|
4
|
+
* The templates this domain can start a role from.
|
|
5
|
+
*
|
|
6
|
+
* Gated the same way role editing is: choosing a template creates a role, so whoever may
|
|
7
|
+
* do this is whoever may create roles.
|
|
8
|
+
*/
|
|
9
|
+
roleTemplates(context: ResolverContext): Promise<RoleTemplateView[]>;
|
|
10
|
+
roleTemplate(id: string, context: ResolverContext): Promise<RoleTemplateView | null>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RoleTemplateQuery = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const type_graphql_1 = require("type-graphql");
|
|
6
|
+
const shell_1 = require("@things-factory/shell");
|
|
7
|
+
const role_js_1 = require("../role/role.js");
|
|
8
|
+
const role_template_js_1 = require("./role-template.js");
|
|
9
|
+
const role_template_types_js_1 = require("./role-template-types.js");
|
|
10
|
+
let RoleTemplateQuery = class RoleTemplateQuery {
|
|
11
|
+
/**
|
|
12
|
+
* The templates this domain can start a role from.
|
|
13
|
+
*
|
|
14
|
+
* Gated the same way role editing is: choosing a template creates a role, so whoever may
|
|
15
|
+
* do this is whoever may create roles.
|
|
16
|
+
*/
|
|
17
|
+
async roleTemplates(context) {
|
|
18
|
+
const { domain } = context.state;
|
|
19
|
+
/*
|
|
20
|
+
* Read the existing role names once. The alternative — a field resolver asking per
|
|
21
|
+
* template — runs a query per row for an answer the list already has.
|
|
22
|
+
*/
|
|
23
|
+
const existing = new Set((await (0, shell_1.getRepository)(role_js_1.Role)
|
|
24
|
+
.createQueryBuilder('ROLE')
|
|
25
|
+
.select('ROLE.name')
|
|
26
|
+
.where('ROLE.domain = :domain', { domain: domain.id })
|
|
27
|
+
.getMany()).map(role => role.name));
|
|
28
|
+
return (0, role_template_js_1.allRoleTemplates)().map(template => view(template, existing));
|
|
29
|
+
}
|
|
30
|
+
async roleTemplate(id, context) {
|
|
31
|
+
const templates = await this.roleTemplates(context);
|
|
32
|
+
return templates.find(template => template.id === id) || null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
exports.RoleTemplateQuery = RoleTemplateQuery;
|
|
36
|
+
tslib_1.__decorate([
|
|
37
|
+
(0, type_graphql_1.Directive)('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)'),
|
|
38
|
+
(0, type_graphql_1.Query)(returns => [role_template_types_js_1.RoleTemplateView], { description: 'To fetch the role templates this installation offers' }),
|
|
39
|
+
tslib_1.__param(0, (0, type_graphql_1.Ctx)()),
|
|
40
|
+
tslib_1.__metadata("design:type", Function),
|
|
41
|
+
tslib_1.__metadata("design:paramtypes", [Object]),
|
|
42
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
43
|
+
], RoleTemplateQuery.prototype, "roleTemplates", null);
|
|
44
|
+
tslib_1.__decorate([
|
|
45
|
+
(0, type_graphql_1.Directive)('@privilege(category: "user", privilege: "mutation", domainOwnerGranted: true, superUserGranted: true)'),
|
|
46
|
+
(0, type_graphql_1.Query)(returns => role_template_types_js_1.RoleTemplateView, { nullable: true, description: 'To fetch one role template by id' }),
|
|
47
|
+
tslib_1.__param(0, (0, type_graphql_1.Arg)('id')),
|
|
48
|
+
tslib_1.__param(1, (0, type_graphql_1.Ctx)()),
|
|
49
|
+
tslib_1.__metadata("design:type", Function),
|
|
50
|
+
tslib_1.__metadata("design:paramtypes", [String, Object]),
|
|
51
|
+
tslib_1.__metadata("design:returntype", Promise)
|
|
52
|
+
], RoleTemplateQuery.prototype, "roleTemplate", null);
|
|
53
|
+
exports.RoleTemplateQuery = RoleTemplateQuery = tslib_1.__decorate([
|
|
54
|
+
(0, type_graphql_1.Resolver)(role_template_types_js_1.RoleTemplateView)
|
|
55
|
+
], RoleTemplateQuery);
|
|
56
|
+
function view(template, existing) {
|
|
57
|
+
const declared = process['PRIVILEGES'] || {};
|
|
58
|
+
const grants = template.grants.map(grant => {
|
|
59
|
+
const present = grant.axes.filter(axis => declared[`${grant.category} ${axis}`]);
|
|
60
|
+
const missingAxes = grant.axes.filter(axis => !declared[`${grant.category} ${axis}`]);
|
|
61
|
+
return { category: grant.category, axes: present, missingAxes };
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
id: template.id,
|
|
65
|
+
name: template.name,
|
|
66
|
+
description: template.description,
|
|
67
|
+
grants,
|
|
68
|
+
privilegeCount: grants.reduce((count, grant) => count + grant.axes.length, 0),
|
|
69
|
+
missingCount: grants.reduce((count, grant) => count + grant.missingAxes.length, 0),
|
|
70
|
+
defaultNameTaken: existing.has(template.name),
|
|
71
|
+
/*
|
|
72
|
+
* Declared with no grants at all — an approval seat. Distinct from a template whose
|
|
73
|
+
* grants all went missing in this installation, which is a defect and shows a
|
|
74
|
+
* `missingCount` instead.
|
|
75
|
+
*/
|
|
76
|
+
grantsNothingDeliberately: template.grants.length === 0
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=role-template-query.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role-template-query.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-query.ts"],"names":[],"mappings":"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","sourcesContent":["import { Arg, Ctx, Directive, Query, Resolver } from 'type-graphql'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { Role } from '../role/role.js'\nimport { allRoleTemplates, RegisteredRoleTemplate } from './role-template.js'\nimport { RoleTemplateView } from './role-template-types.js'\n\n@Resolver(RoleTemplateView)\nexport class RoleTemplateQuery {\n /**\n * The templates this domain can start a role from.\n *\n * Gated the same way role editing is: choosing a template creates a role, so whoever may\n * do this is whoever may create roles.\n */\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => [RoleTemplateView], { description: 'To fetch the role templates this installation offers' })\n async roleTemplates(@Ctx() context: ResolverContext): Promise<RoleTemplateView[]> {\n const { domain } = context.state\n\n /*\n * Read the existing role names once. The alternative — a field resolver asking per\n * template — runs a query per row for an answer the list already has.\n */\n const existing = new Set(\n (\n await getRepository(Role)\n .createQueryBuilder('ROLE')\n .select('ROLE.name')\n .where('ROLE.domain = :domain', { domain: domain.id })\n .getMany()\n ).map(role => role.name)\n )\n\n return allRoleTemplates().map(template => view(template, existing))\n }\n\n @Directive('@privilege(category: \"user\", privilege: \"mutation\", domainOwnerGranted: true, superUserGranted: true)')\n @Query(returns => RoleTemplateView, { nullable: true, description: 'To fetch one role template by id' })\n async roleTemplate(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<RoleTemplateView | null> {\n const templates = await this.roleTemplates(context)\n\n return templates.find(template => template.id === id) || null\n }\n}\n\nfunction view(template: RegisteredRoleTemplate, existing: Set<string>): RoleTemplateView {\n const declared = process['PRIVILEGES'] || {}\n\n const grants = template.grants.map(grant => {\n const present = grant.axes.filter(axis => declared[`${grant.category} ${axis}`])\n const missingAxes = grant.axes.filter(axis => !declared[`${grant.category} ${axis}`])\n\n return { category: grant.category, axes: present, missingAxes }\n })\n\n return {\n id: template.id,\n name: template.name,\n description: template.description,\n grants,\n privilegeCount: grants.reduce((count, grant) => count + grant.axes.length, 0),\n missingCount: grants.reduce((count, grant) => count + grant.missingAxes.length, 0),\n defaultNameTaken: existing.has(template.name),\n /*\n * Declared with no grants at all — an approval seat. Distinct from a template whose\n * grants all went missing in this installation, which is a defect and shows a\n * `missingCount` instead.\n */\n grantsNothingDeliberately: template.grants.length === 0\n }\n}\n"]}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a template looks like *to this installation*.
|
|
3
|
+
*
|
|
4
|
+
* The declared template and the offered template are not the same thing: an app declares
|
|
5
|
+
* grants across modules it expects to be installed, and a given installation has a subset.
|
|
6
|
+
* operato-plant declares 14 categories; the plant app running on :4000 sees 33 of the 47
|
|
7
|
+
* categories declared across the repository. So the answer has to say which parts of the
|
|
8
|
+
* template this installation can actually honour.
|
|
9
|
+
*/
|
|
10
|
+
export declare class RoleTemplateGrantView {
|
|
11
|
+
category: string;
|
|
12
|
+
axes: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Axes this installation does not declare.
|
|
15
|
+
*
|
|
16
|
+
* Dropping them quietly would hand the administrator a role that does not match the
|
|
17
|
+
* template's name, with no way to tell. They are reported instead, and the screen says
|
|
18
|
+
* "not present in this installation".
|
|
19
|
+
*/
|
|
20
|
+
missingAxes: string[];
|
|
21
|
+
}
|
|
22
|
+
export declare class RoleTemplateView {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
grants: RoleTemplateGrantView[];
|
|
27
|
+
privilegeCount: number;
|
|
28
|
+
missingCount: number;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a role already carries this template's default name in the current domain.
|
|
31
|
+
*
|
|
32
|
+
* The screen warns before creating one, because seeding onto an existing name does not
|
|
33
|
+
* make a second role — approval lines hold a role's id, so a duplicate would leave the
|
|
34
|
+
* approval routed to the role nobody edits. The existing role is used as it stands and
|
|
35
|
+
* the template's privileges are added to it.
|
|
36
|
+
*
|
|
37
|
+
* It says "default name" rather than "seeded" because the administrator may give the
|
|
38
|
+
* role a name of their own, and then nothing here can tell whether this template was
|
|
39
|
+
* used. That is deliberate: provenance is not stored.
|
|
40
|
+
*/
|
|
41
|
+
defaultNameTaken: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* True when the template deliberately opens nothing.
|
|
44
|
+
*
|
|
45
|
+
* A seat that only receives approvals is legitimate — operato-twin declares seven roles
|
|
46
|
+
* with no privileges at all, used as the target an approval line points at. The screen
|
|
47
|
+
* must not draw that as "0 privileges, something is wrong", so the distinction is
|
|
48
|
+
* carried here rather than inferred from an empty list.
|
|
49
|
+
*/
|
|
50
|
+
grantsNothingDeliberately: boolean;
|
|
51
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RoleTemplateView = exports.RoleTemplateGrantView = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const type_graphql_1 = require("type-graphql");
|
|
6
|
+
/**
|
|
7
|
+
* What a template looks like *to this installation*.
|
|
8
|
+
*
|
|
9
|
+
* The declared template and the offered template are not the same thing: an app declares
|
|
10
|
+
* grants across modules it expects to be installed, and a given installation has a subset.
|
|
11
|
+
* operato-plant declares 14 categories; the plant app running on :4000 sees 33 of the 47
|
|
12
|
+
* categories declared across the repository. So the answer has to say which parts of the
|
|
13
|
+
* template this installation can actually honour.
|
|
14
|
+
*/
|
|
15
|
+
let RoleTemplateGrantView = class RoleTemplateGrantView {
|
|
16
|
+
};
|
|
17
|
+
exports.RoleTemplateGrantView = RoleTemplateGrantView;
|
|
18
|
+
tslib_1.__decorate([
|
|
19
|
+
(0, type_graphql_1.Field)({ description: 'The privilege category — what is guarded.' }),
|
|
20
|
+
tslib_1.__metadata("design:type", String)
|
|
21
|
+
], RoleTemplateGrantView.prototype, "category", void 0);
|
|
22
|
+
tslib_1.__decorate([
|
|
23
|
+
(0, type_graphql_1.Field)(type => [String], { description: 'The privilege axes granted on that category.' }),
|
|
24
|
+
tslib_1.__metadata("design:type", Array)
|
|
25
|
+
], RoleTemplateGrantView.prototype, "axes", void 0);
|
|
26
|
+
tslib_1.__decorate([
|
|
27
|
+
(0, type_graphql_1.Field)(type => [String], { description: 'Axes named by the template that this installation does not declare.' }),
|
|
28
|
+
tslib_1.__metadata("design:type", Array)
|
|
29
|
+
], RoleTemplateGrantView.prototype, "missingAxes", void 0);
|
|
30
|
+
exports.RoleTemplateGrantView = RoleTemplateGrantView = tslib_1.__decorate([
|
|
31
|
+
(0, type_graphql_1.ObjectType)({ description: 'One module and the axes a role template grants on it.' })
|
|
32
|
+
], RoleTemplateGrantView);
|
|
33
|
+
let RoleTemplateView = class RoleTemplateView {
|
|
34
|
+
};
|
|
35
|
+
exports.RoleTemplateView = RoleTemplateView;
|
|
36
|
+
tslib_1.__decorate([
|
|
37
|
+
(0, type_graphql_1.Field)({ description: 'Declaration key, namespaced by the declaring application.' }),
|
|
38
|
+
tslib_1.__metadata("design:type", String)
|
|
39
|
+
], RoleTemplateView.prototype, "id", void 0);
|
|
40
|
+
tslib_1.__decorate([
|
|
41
|
+
(0, type_graphql_1.Field)({ description: 'The role name this template creates in a domain.' }),
|
|
42
|
+
tslib_1.__metadata("design:type", String)
|
|
43
|
+
], RoleTemplateView.prototype, "name", void 0);
|
|
44
|
+
tslib_1.__decorate([
|
|
45
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'A description of the seat this template creates.' }),
|
|
46
|
+
tslib_1.__metadata("design:type", String)
|
|
47
|
+
], RoleTemplateView.prototype, "description", void 0);
|
|
48
|
+
tslib_1.__decorate([
|
|
49
|
+
(0, type_graphql_1.Field)(type => [RoleTemplateGrantView], { description: 'The modules and axes this template grants.' }),
|
|
50
|
+
tslib_1.__metadata("design:type", Array)
|
|
51
|
+
], RoleTemplateView.prototype, "grants", void 0);
|
|
52
|
+
tslib_1.__decorate([
|
|
53
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'How many privileges this template would actually grant here.' }),
|
|
54
|
+
tslib_1.__metadata("design:type", Number)
|
|
55
|
+
], RoleTemplateView.prototype, "privilegeCount", void 0);
|
|
56
|
+
tslib_1.__decorate([
|
|
57
|
+
(0, type_graphql_1.Field)(type => type_graphql_1.Int, { description: 'How many privileges the template names that this installation does not have.' }),
|
|
58
|
+
tslib_1.__metadata("design:type", Number)
|
|
59
|
+
], RoleTemplateView.prototype, "missingCount", void 0);
|
|
60
|
+
tslib_1.__decorate([
|
|
61
|
+
(0, type_graphql_1.Field)({ description: "Whether a role already carries this template's default name in the current domain." }),
|
|
62
|
+
tslib_1.__metadata("design:type", Boolean)
|
|
63
|
+
], RoleTemplateView.prototype, "defaultNameTaken", void 0);
|
|
64
|
+
tslib_1.__decorate([
|
|
65
|
+
(0, type_graphql_1.Field)({ description: 'Whether this template grants nothing on purpose, rather than by omission.' }),
|
|
66
|
+
tslib_1.__metadata("design:type", Boolean)
|
|
67
|
+
], RoleTemplateView.prototype, "grantsNothingDeliberately", void 0);
|
|
68
|
+
exports.RoleTemplateView = RoleTemplateView = tslib_1.__decorate([
|
|
69
|
+
(0, type_graphql_1.ObjectType)({ description: 'A role template offered by this installation.' })
|
|
70
|
+
], RoleTemplateView);
|
|
71
|
+
//# sourceMappingURL=role-template-types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"role-template-types.js","sourceRoot":"","sources":["../../../server/service/role-template/role-template-types.ts"],"names":[],"mappings":";;;;AAAA,+CAAqD;AAErD;;;;;;;;GAQG;AAEI,IAAM,qBAAqB,GAA3B,MAAM,qBAAqB;CAgBjC,CAAA;AAhBY,sDAAqB;AAEhC;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,2CAA2C,EAAE,CAAC;;uDACpD;AAGhB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,WAAW,EAAE,8CAA8C,EAAE,CAAC;;mDAC3E;AAUd;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,WAAW,EAAE,qEAAqE,EAAE,CAAC;;0DAC3F;gCAfV,qBAAqB;IADjC,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,uDAAuD,EAAE,CAAC;GACxE,qBAAqB,CAgBjC;AAGM,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;CA4C5B,CAAA;AA5CY,4CAAgB;AAE3B;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,2DAA2D,EAAE,CAAC;;4CAC1E;AAGV;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,kDAAkD,EAAE,CAAC;;8CAC/D;AAGZ;IADC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,kDAAkD,EAAE,CAAC;;qDACvE;AAGpB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,CAAC,qBAAqB,CAAC,EAAE,EAAE,WAAW,EAAE,4CAA4C,EAAE,CAAC;;gDACvE;AAG/B;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,8DAA8D,EAAE,CAAC;;wDAC9E;AAGtB;IADC,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,kBAAG,EAAE,EAAE,WAAW,EAAE,8EAA8E,EAAE,CAAC;;sDAChG;AAepB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,oFAAoF,EAAE,CAAC;;0DACpF;AAWzB;IADC,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,2EAA2E,EAAE,CAAC;;mEAClE;2BA3CvB,gBAAgB;IAD5B,IAAA,yBAAU,EAAC,EAAE,WAAW,EAAE,+CAA+C,EAAE,CAAC;GAChE,gBAAgB,CA4C5B","sourcesContent":["import { Field, Int, ObjectType } from 'type-graphql'\n\n/**\n * What a template looks like *to this installation*.\n *\n * The declared template and the offered template are not the same thing: an app declares\n * grants across modules it expects to be installed, and a given installation has a subset.\n * operato-plant declares 14 categories; the plant app running on :4000 sees 33 of the 47\n * categories declared across the repository. So the answer has to say which parts of the\n * template this installation can actually honour.\n */\n@ObjectType({ description: 'One module and the axes a role template grants on it.' })\nexport class RoleTemplateGrantView {\n @Field({ description: 'The privilege category — what is guarded.' })\n category: string\n\n @Field(type => [String], { description: 'The privilege axes granted on that category.' })\n axes: string[]\n\n /**\n * Axes this installation does not declare.\n *\n * Dropping them quietly would hand the administrator a role that does not match the\n * template's name, with no way to tell. They are reported instead, and the screen says\n * \"not present in this installation\".\n */\n @Field(type => [String], { description: 'Axes named by the template that this installation does not declare.' })\n missingAxes: string[]\n}\n\n@ObjectType({ description: 'A role template offered by this installation.' })\nexport class RoleTemplateView {\n @Field({ description: 'Declaration key, namespaced by the declaring application.' })\n id: string\n\n @Field({ description: 'The role name this template creates in a domain.' })\n name: string\n\n @Field({ nullable: true, description: 'A description of the seat this template creates.' })\n description?: string\n\n @Field(type => [RoleTemplateGrantView], { description: 'The modules and axes this template grants.' })\n grants: RoleTemplateGrantView[]\n\n @Field(type => Int, { description: 'How many privileges this template would actually grant here.' })\n privilegeCount: number\n\n @Field(type => Int, { description: 'How many privileges the template names that this installation does not have.' })\n missingCount: number\n\n /**\n * Whether a role already carries this template's default name in the current domain.\n *\n * The screen warns before creating one, because seeding onto an existing name does not\n * make a second role — approval lines hold a role's id, so a duplicate would leave the\n * approval routed to the role nobody edits. The existing role is used as it stands and\n * the template's privileges are added to it.\n *\n * It says \"default name\" rather than \"seeded\" because the administrator may give the\n * role a name of their own, and then nothing here can tell whether this template was\n * used. That is deliberate: provenance is not stored.\n */\n @Field({ description: \"Whether a role already carries this template's default name in the current domain.\" })\n defaultNameTaken: boolean\n\n /**\n * True when the template deliberately opens nothing.\n *\n * A seat that only receives approvals is legitimate — operato-twin declares seven roles\n * with no privileges at all, used as the target an approval line points at. The screen\n * must not draw that as \"0 privileges, something is wrong\", so the distinction is\n * carried here rather than inferred from an empty list.\n */\n @Field({ description: 'Whether this template grants nothing on purpose, rather than by omission.' })\n grantsNothingDeliberately: boolean\n}\n"]}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Role templates — the seeds an application offers so a new domain does not start with
|
|
3
|
+
* zero roles.
|
|
4
|
+
*
|
|
5
|
+
* ── Why this is a boot registry and not a table ───────────────────────────────
|
|
6
|
+
* A template is a declaration. Copying it into rows per domain gives 100 domains x 3
|
|
7
|
+
* templates = 300 rows, and once the code changes the template there is nowhere to ask
|
|
8
|
+
* which of the two is true: the declaration, or the row that was written from an older
|
|
9
|
+
* one. `@privilege` lives in the boot registry for the same reason.
|
|
10
|
+
*
|
|
11
|
+
* ── What a template is not ───────────────────────────────────────────────────
|
|
12
|
+
* It is a seed, not a role. Once it has been used the resulting role belongs to the
|
|
13
|
+
* administrator: the template is never re-applied and never overwrites. Nothing is written
|
|
14
|
+
* onto the role to remember where it came from — what an administrator would want from
|
|
15
|
+
* that ("the template gained two privileges, here they are") is the template's current
|
|
16
|
+
* grants against the role's current privileges, and that is computed rather than stored.
|
|
17
|
+
*
|
|
18
|
+
* ── A role is one thing seen from three sides ────────────────────────────────
|
|
19
|
+
* operato-plant uses roles as privilege bundles; operato-twin uses them as the seat an
|
|
20
|
+
* approval line points at (`worklist` resolves an approver of type `Role` by id). Those
|
|
21
|
+
* are not two concepts. A role is a seat in the organisation: what it opens, who fills it,
|
|
22
|
+
* and whether approvals route to it. Splitting them would put two QUALITY-INSPECTORs in
|
|
23
|
+
* one plant and force every person into both.
|
|
24
|
+
*/
|
|
25
|
+
/** One module and the axes this template grants on it. Axes are always enumerated. */
|
|
26
|
+
export type RoleTemplateGrant = {
|
|
27
|
+
/** `@privilege(category:)` — what is guarded, not the package name. */
|
|
28
|
+
category: string;
|
|
29
|
+
/**
|
|
30
|
+
* `query` / `mutation` / ... — listed one by one.
|
|
31
|
+
*
|
|
32
|
+
* There is deliberately no `'all'`. A template written as "everything" would widen on
|
|
33
|
+
* its own the day a new axis is declared, with nobody having touched it. The single
|
|
34
|
+
* place that may widen is the framework-computed administrator template, which means
|
|
35
|
+
* "whatever exists now" by definition.
|
|
36
|
+
*/
|
|
37
|
+
axes: string[];
|
|
38
|
+
};
|
|
39
|
+
export type RoleTemplate = {
|
|
40
|
+
/** Declaration key, namespaced by the declaring app: `plant.quality-inspector`. */
|
|
41
|
+
id: string;
|
|
42
|
+
/**
|
|
43
|
+
* The role name created in the domain: `QUALITY-INSPECTOR`.
|
|
44
|
+
*
|
|
45
|
+
* Two apps may declare the same name on purpose — that is one seat, and the privileges
|
|
46
|
+
* become the union. The name is also a stored identifier: approval lines hold the role's
|
|
47
|
+
* id, so renaming here creates a second role and leaves the approval line pointing at
|
|
48
|
+
* the first. Renaming is a migration, not a template edit.
|
|
49
|
+
*/
|
|
50
|
+
name: string;
|
|
51
|
+
/**
|
|
52
|
+
* Plain text, not an i18n key.
|
|
53
|
+
*
|
|
54
|
+
* `createRoleFromTemplate` copies this onto the role it seeds, and the role list draws
|
|
55
|
+
* the role's description as a value — so a key written here reaches the screen as
|
|
56
|
+
* `label.role-template.administrator`. That is exactly what the framework's own two
|
|
57
|
+
* templates did until 2026-09-11.
|
|
58
|
+
*
|
|
59
|
+
* operato-plant had it right from the start: "검사 기준과 처분을 정하는 자리".
|
|
60
|
+
*/
|
|
61
|
+
description?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Required, and may be empty.
|
|
64
|
+
*
|
|
65
|
+
* `[]` is a statement: this seat opens nothing, it exists for approval routing. A
|
|
66
|
+
* missing `grants` is a different thing — someone forgot — and is rejected here, so the
|
|
67
|
+
* screen can tell "opens nothing on purpose" from "nothing was written down".
|
|
68
|
+
*/
|
|
69
|
+
grants: RoleTemplateGrant[];
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Registered form of a template.
|
|
73
|
+
*
|
|
74
|
+
* Identical to the declaration today. It stays a separate name because the registry is
|
|
75
|
+
* what every reader goes through, and giving it its own type keeps a later addition from
|
|
76
|
+
* rippling into every call site.
|
|
77
|
+
*/
|
|
78
|
+
export type RegisteredRoleTemplate = RoleTemplate;
|
|
79
|
+
/**
|
|
80
|
+
* Declares a role template. Call it from the application's server entry.
|
|
81
|
+
*
|
|
82
|
+
* Categories are not checked here: registration runs while modules load, before the
|
|
83
|
+
* schema is built and therefore before `process['PRIVILEGES']` exists. The name check
|
|
84
|
+
* happens at boot instead (`reportUnknownTemplateGrants`).
|
|
85
|
+
*/
|
|
86
|
+
export declare function registerRoleTemplate(template: RoleTemplate): void;
|
|
87
|
+
export declare function roleTemplates(): RegisteredRoleTemplate[];
|
|
88
|
+
export declare function roleTemplateById(id: string): RegisteredRoleTemplate | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* The two templates the framework can build without knowing a single category name.
|
|
91
|
+
*
|
|
92
|
+
* Everything else has to be declared by an application: a job title spans modules
|
|
93
|
+
* (a production lead uses worklist *and* order *and* ops-master) and no one package knows
|
|
94
|
+
* that. These two are different — they are defined over whatever is declared, so they can
|
|
95
|
+
* be computed from the registry `@privilege` fills.
|
|
96
|
+
*/
|
|
97
|
+
export declare function computedRoleTemplates(): RegisteredRoleTemplate[];
|
|
98
|
+
/** Every template this installation offers — declared by apps, plus the computed two. */
|
|
99
|
+
export declare function allRoleTemplates(): RegisteredRoleTemplate[];
|
|
100
|
+
/**
|
|
101
|
+
* Names a template mentions that this installation does not declare.
|
|
102
|
+
*
|
|
103
|
+
* A template that survives a category rename becomes a role that grants nothing, and that
|
|
104
|
+
* is the quietest way this design can fail — the administrator picks "quality inspector",
|
|
105
|
+
* gets a role, and it opens no doors. operato-plant's seed already does this for its own
|
|
106
|
+
* table (`seed-demo-org.ts` warns about grants it could not attach); this is the same
|
|
107
|
+
* check moved to where templates are declared.
|
|
108
|
+
*/
|
|
109
|
+
export declare function unknownTemplateGrants(template: RoleTemplate): string[];
|
|
110
|
+
export declare function reportUnknownTemplateGrants(): void;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerRoleTemplate = registerRoleTemplate;
|
|
4
|
+
exports.roleTemplates = roleTemplates;
|
|
5
|
+
exports.roleTemplateById = roleTemplateById;
|
|
6
|
+
exports.computedRoleTemplates = computedRoleTemplates;
|
|
7
|
+
exports.allRoleTemplates = allRoleTemplates;
|
|
8
|
+
exports.unknownTemplateGrants = unknownTemplateGrants;
|
|
9
|
+
exports.reportUnknownTemplateGrants = reportUnknownTemplateGrants;
|
|
10
|
+
const env_1 = require("@things-factory/env");
|
|
11
|
+
const privilege_axis_js_1 = require("../privilege/privilege-axis.js");
|
|
12
|
+
const REGISTRY = 'ROLE_TEMPLATES';
|
|
13
|
+
process[REGISTRY] = {};
|
|
14
|
+
function registry() {
|
|
15
|
+
return process[REGISTRY];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Declares a role template. Call it from the application's server entry.
|
|
19
|
+
*
|
|
20
|
+
* Categories are not checked here: registration runs while modules load, before the
|
|
21
|
+
* schema is built and therefore before `process['PRIVILEGES']` exists. The name check
|
|
22
|
+
* happens at boot instead (`reportUnknownTemplateGrants`).
|
|
23
|
+
*/
|
|
24
|
+
function registerRoleTemplate(template) {
|
|
25
|
+
if (!template?.id || !template?.name) {
|
|
26
|
+
throw new Error('A role template needs both an id and a name.');
|
|
27
|
+
}
|
|
28
|
+
if (!Array.isArray(template.grants)) {
|
|
29
|
+
throw new Error(`Role template "${template.id}" has no grants. Write \`grants: []\` if this seat is meant to ` +
|
|
30
|
+
'open nothing — leaving it out is indistinguishable from forgetting, and the screen cannot ' +
|
|
31
|
+
'tell an administrator which one it is.');
|
|
32
|
+
}
|
|
33
|
+
for (const grant of template.grants) {
|
|
34
|
+
if (!grant?.category || !Array.isArray(grant.axes) || grant.axes.length === 0) {
|
|
35
|
+
throw new Error(`Role template "${template.id}" has a grant without a category or with no axes. ` +
|
|
36
|
+
'Axes are always listed one by one.');
|
|
37
|
+
}
|
|
38
|
+
/*
|
|
39
|
+
* Across the nine roles operato-plant declares, no role is granted `mutation` on a
|
|
40
|
+
* category without `query` on the same category — you cannot fix what you cannot see.
|
|
41
|
+
* A template that does so is almost certainly a slip, so say it out loud rather than
|
|
42
|
+
* silently granting a half-usable seat.
|
|
43
|
+
*/
|
|
44
|
+
if (grant.axes.includes('mutation') && !grant.axes.includes('query')) {
|
|
45
|
+
env_1.logger.warn(`[role-template] "${template.id}" grants ${grant.category}:mutation without ${grant.category}:query. ` +
|
|
46
|
+
'Someone who may change a thing but not see it can rarely use either.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (registry()[template.id]) {
|
|
50
|
+
throw new Error(`Role template "${template.id}" is declared twice.`);
|
|
51
|
+
}
|
|
52
|
+
registry()[template.id] = { ...template };
|
|
53
|
+
}
|
|
54
|
+
function roleTemplates() {
|
|
55
|
+
return Object.values(registry());
|
|
56
|
+
}
|
|
57
|
+
function roleTemplateById(id) {
|
|
58
|
+
return registry()[id];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The two templates the framework can build without knowing a single category name.
|
|
62
|
+
*
|
|
63
|
+
* Everything else has to be declared by an application: a job title spans modules
|
|
64
|
+
* (a production lead uses worklist *and* order *and* ops-master) and no one package knows
|
|
65
|
+
* that. These two are different — they are defined over whatever is declared, so they can
|
|
66
|
+
* be computed from the registry `@privilege` fills.
|
|
67
|
+
*/
|
|
68
|
+
function computedRoleTemplates() {
|
|
69
|
+
const declared = Object.values(process['PRIVILEGES'] || {});
|
|
70
|
+
const axesOf = new Map();
|
|
71
|
+
for (const [category, axis] of declared) {
|
|
72
|
+
if (!axesOf.has(category)) {
|
|
73
|
+
axesOf.set(category, new Set());
|
|
74
|
+
}
|
|
75
|
+
axesOf.get(category).add(axis);
|
|
76
|
+
}
|
|
77
|
+
const categories = [...axesOf.keys()].sort();
|
|
78
|
+
/*
|
|
79
|
+
* No description on either of these.
|
|
80
|
+
*
|
|
81
|
+
* They carried i18n keys, which the seeding path stored verbatim onto the role and the
|
|
82
|
+
* list then drew as the description. And a static sentence would be worse than what the
|
|
83
|
+
* screen already computes from `grants` — "opens 30 things across 30 modules" is exact
|
|
84
|
+
* and moves when the installation does.
|
|
85
|
+
*/
|
|
86
|
+
/*
|
|
87
|
+
* VIEWER is only offered when someone has said which axes are read-only.
|
|
88
|
+
*
|
|
89
|
+
* This used to match the literal word `query`, which is right in this house and wrong in
|
|
90
|
+
* dssp, where the read axis is spelled `read` and `kpi` has no `query` at all — VIEWER
|
|
91
|
+
* would have quietly dropped that installation's main module.
|
|
92
|
+
*
|
|
93
|
+
* And when nobody has said, the template is **not offered at all** rather than offered
|
|
94
|
+
* empty. An empty VIEWER is the quiet failure this whole design is built to avoid: an
|
|
95
|
+
* administrator picks it, gets a role, no door opens, and the list counts the zero
|
|
96
|
+
* correctly. "We do not know" is not "there are none".
|
|
97
|
+
*/
|
|
98
|
+
const readOnly = (0, privilege_axis_js_1.readOnlyAxes)();
|
|
99
|
+
const viewer = (0, privilege_axis_js_1.readOnlyAxesAreKnown)()
|
|
100
|
+
? {
|
|
101
|
+
id: 'framework.viewer',
|
|
102
|
+
name: 'VIEWER',
|
|
103
|
+
grants: categories
|
|
104
|
+
.map(category => ({ category, axes: [...axesOf.get(category)].filter(axis => readOnly.has(axis)).sort() }))
|
|
105
|
+
.filter(grant => grant.axes.length > 0)
|
|
106
|
+
}
|
|
107
|
+
: undefined;
|
|
108
|
+
/*
|
|
109
|
+
* The one place that widens on purpose.
|
|
110
|
+
*
|
|
111
|
+
* Everywhere else an unenumerated axis is a defect waiting for the day a new axis is
|
|
112
|
+
* declared. Here "everything that exists" is the meaning of the template, so picking up
|
|
113
|
+
* a newly declared axis is correct rather than accidental.
|
|
114
|
+
*/
|
|
115
|
+
const administrator = {
|
|
116
|
+
id: 'framework.administrator',
|
|
117
|
+
name: 'ADMINISTRATOR',
|
|
118
|
+
grants: categories.map(category => ({ category, axes: [...axesOf.get(category)].sort() }))
|
|
119
|
+
};
|
|
120
|
+
return viewer ? [viewer, administrator] : [administrator];
|
|
121
|
+
}
|
|
122
|
+
/** Every template this installation offers — declared by apps, plus the computed two. */
|
|
123
|
+
function allRoleTemplates() {
|
|
124
|
+
return [...computedRoleTemplates(), ...roleTemplates()];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Names a template mentions that this installation does not declare.
|
|
128
|
+
*
|
|
129
|
+
* A template that survives a category rename becomes a role that grants nothing, and that
|
|
130
|
+
* is the quietest way this design can fail — the administrator picks "quality inspector",
|
|
131
|
+
* gets a role, and it opens no doors. operato-plant's seed already does this for its own
|
|
132
|
+
* table (`seed-demo-org.ts` warns about grants it could not attach); this is the same
|
|
133
|
+
* check moved to where templates are declared.
|
|
134
|
+
*/
|
|
135
|
+
function unknownTemplateGrants(template) {
|
|
136
|
+
const declared = process['PRIVILEGES'] || {};
|
|
137
|
+
return template.grants.flatMap(grant => grant.axes.filter(axis => !declared[`${grant.category} ${axis}`]).map(axis => `${grant.category}:${axis}`));
|
|
138
|
+
}
|
|
139
|
+
function reportUnknownTemplateGrants() {
|
|
140
|
+
const unknown = roleTemplates().flatMap(template => unknownTemplateGrants(template).map(grant => `${template.id} → ${grant}`));
|
|
141
|
+
if (unknown.length) {
|
|
142
|
+
env_1.logger.warn(`[role-template] ${unknown.length} grants name privileges this installation does not declare — ` +
|
|
143
|
+
`${unknown.join(', ')}. Privilege definitions come from @privilege at boot, so either the query ` +
|
|
144
|
+
'is not installed here or the name has moved. A role seeded from such a template opens nothing.');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=role-template.js.map
|