@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,mCAAmC;AACnC,8DAGwC;AACxC,uDAA+G;AAC/G,qDAAwE;AACxE,mDAAqG;AACrG,qDAA2G;AAC3G,0DAAkF;AAClF,sDAA2G;AAC3G,oDAAuG;AACvG,uDAA8G;AAC9G,kDAA+F;AAC/F,2DAAiF;AACjF,oDAAqG;AACrG,+EAA2G;AAC3G,+CAAsF;AACtF,+CAAsF;AACtF,6DAAqF;AACrF,8DAAsF;AACtF,sDAAwG;AACxG,uDAA4G;AAE5G,yBAAyB;AACzB,yFAA8D;AAC9D,2EAAgD;AAChD,uEAA4C;AAC5C,mEAAwC;AACxC,mEAAwC;AACxC,yDAA8B;AAC9B,yDAA8B;AAC9B,+DAAoC;AACpC,yEAA8C;AAC9C,qEAA0C;AAC1C,uEAA4C;AAC5C,iFAAsD;AACtD,qFAA0D;AAC1D,2EAAgD;AAChD,uFAA4D;AAC5D,uEAA4C;AAC5C,yEAA8C;AAE9C,kBAAkB;AAClB,6EAAkD;AAClD,yEAA8C;AAC9C,6EAAkD;AAClD,uFAA4D;AAC5D,2EAAgD;AAChD,qEAA0C;AAC1C,yEAA8C;AAC9C,+DAAoC;AACpC,+DAAoC;AACpC,6EAAkD;AAErC,QAAA,QAAQ,GAAG;IACtB,cAAc;IACd,GAAG,mBAA0B;IAC7B,GAAG,mBAAoB;IACvB,GAAG,mBAAmB;IACtB,GAAG,mBAAiB;IACpB,GAAG,oBAAiB;IACpB,GAAG,oBAAY;IACf,GAAG,oBAAY;IACf,GAAG,oBAAe;IAClB,GAAG,mBAAmB;IACtB,GAAG,mBAAkB;IACrB,GAAG,oBAAuB;IAC1B,GAAG,oBAAyB;IAC5B,GAAG,mBAAoB;IACvB,GAAG,oBAAyB;IAC5B,GAAG,oBAAkB;IACrB,GAAG,oBAAmB;CACvB,CAAA;AAEY,QAAA,MAAM,GAAG;IACpB,QAAQ,EAAE;QACR,0BAA0B,EAA1B,mDAA0B;KAC3B;IAED,eAAe,EAAE;QACf,sBAAsB;QACtB,GAAG,oBAA2B;QAC9B,GAAG,oBAAqB;QACxB,GAAG,oBAAoB;QACvB,GAAG,oBAAkB;QACrB,GAAG,qBAAkB;QACrB,GAAG,qBAAa;QAChB,GAAG,qBAAa;QAChB,GAAG,qBAAgB;QACnB,GAAG,oBAAmB;QACtB,GAAG,oBAAkB;QACrB,GAAG,oBAAkB;QACrB,GAAG,oBAAuB;QAC1B,GAAG,oBAAoB;QACvB,GAAG,qBAAkB;QACrB,GAAG,qBAAoB;KACxB;IACD,UAAU,EAAE;QACV,SAAS,EAAE,mDAA0B;KACtC;CACF,CAAA","sourcesContent":["/* IMPORT ENTITIES AND RESOLVERS */\nimport {\n entities as UsersAuthProvidersEntities,\n resolvers as UsersAuthProvidersResolvers\n} from './users-auth-providers/index.js'\nimport { entities as AuthProviderEntities, resolvers as AuthProviderResolvers } from './auth-provider/index.js'\nimport { resolvers as AppbindingResolver } from './app-binding/index.js'\nimport { entities as ApplianceEntities, resolvers as ApplianceResolvers } from './appliance/index.js'\nimport { entities as ApplicationEntities, resolvers as ApplicationResolvers } from './application/index.js'\nimport { resolvers as DomainGeneratorResolver } from './domain-generator/index.js'\nimport { entities as GrantedRoleEntities, resolvers as GrantedRoleResolver } from './granted-role/index.js'\nimport { entities as InvitationEntities, resolvers as InvitationResolver } from './invitation/index.js'\nimport { entities as LoginHistoryEntities, resolvers as LoginHistoryResolver } from './login-history/index.js'\nimport { entities as PartnerEntities, resolvers as PartnerResolvers } from './partner/index.js'\nimport { entities as PasswordHistoryEntities } from './password-history/index.js'\nimport { entities as PrivilegeEntities, resolvers as PrivilegeResolvers } from './privilege/index.js'\nimport { privilegeDirectiveResolver, privilegeDirectiveTypeDefs } from './privilege/privilege-directive.js'\nimport { entities as RoleEntities, resolvers as RoleResolvers } from './role/index.js'\nimport { entities as UserEntities, resolvers as UserResolvers } from './user/index.js'\nimport { entities as VerificationTokenEntities } from './verification-token/index.js'\nimport { entities as WebAuthCredentialEntities } from './web-auth-credential/index.js'\nimport { entities as DomainLinkEntities, resolvers as DomainLinkResolver } from './domain-link/index.js'\nimport { entities as DomainOwnerEntities, resolvers as DomainOwnerResolvers } from './domain-owner/index.js'\n\n/* EXPORT ENTITY TYPES */\nexport * from './users-auth-providers/users-auth-providers.js'\nexport * from './auth-provider/auth-provider.js'\nexport * from './application/application.js'\nexport * from './appliance/appliance.js'\nexport * from './privilege/privilege.js'\nexport * from './role/role.js'\nexport * from './user/user.js'\nexport * from './partner/partner.js'\nexport * from './granted-role/granted-role.js'\nexport * from './invitation/invitation.js'\nexport * from './app-binding/app-binding.js'\nexport * from './password-history/password-history.js'\nexport * from './verification-token/verification-token.js'\nexport * from './login-history/login-history.js'\nexport * from './web-auth-credential/web-auth-credential.js'\nexport * from './domain-link/domain-link.js'\nexport * from './domain-owner/domain-owner.js'\n\n/* EXPORT TYPES */\nexport * from './app-binding/app-binding-types.js'\nexport * from './appliance/appliance-types.js'\nexport * from './application/application-types.js'\nexport * from './domain-generator/domain-generator-types.js'\nexport * from './invitation/invitation-types.js'\nexport * from './partner/partner-types.js'\nexport * from './privilege/privilege-types.js'\nexport * from './role/role-types.js'\nexport * from './user/user-types.js'\nexport * from './domain-link/domain-link-types.js'\n\nexport const entities = [\n /* ENTITIES */\n ...UsersAuthProvidersEntities,\n ...AuthProviderEntities,\n ...ApplicationEntities,\n ...ApplianceEntities,\n ...PrivilegeEntities,\n ...RoleEntities,\n ...UserEntities,\n ...PartnerEntities,\n ...GrantedRoleEntities,\n ...InvitationEntities,\n ...PasswordHistoryEntities,\n ...VerificationTokenEntities,\n ...LoginHistoryEntities,\n ...WebAuthCredentialEntities,\n ...DomainLinkEntities,\n ...DomainOwnerEntities\n]\n\nexport const schema = {\n typeDefs: {\n privilegeDirectiveTypeDefs\n },\n\n resolverClasses: [\n /* RESOLVER CLASSES */\n ...UsersAuthProvidersResolvers,\n ...AuthProviderResolvers,\n ...ApplicationResolvers,\n ...ApplianceResolvers,\n ...PrivilegeResolvers,\n ...RoleResolvers,\n ...UserResolvers,\n ...PartnerResolvers,\n ...GrantedRoleResolver,\n ...InvitationResolver,\n ...AppbindingResolver,\n ...DomainGeneratorResolver,\n ...LoginHistoryResolver,\n ...DomainLinkResolver,\n ...DomainOwnerResolvers\n ],\n directives: {\n privilege: privilegeDirectiveResolver\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../server/service/index.ts"],"names":[],"mappings":";;;;AAAA,mCAAmC;AACnC,8DAGwC;AACxC,uDAA+G;AAC/G,qDAAwE;AACxE,mDAAqG;AACrG,qDAA2G;AAC3G,0DAAkF;AAClF,sDAA2G;AAC3G,oDAAuG;AACvG,uDAA8G;AAC9G,kDAA+F;AAC/F,2DAAiF;AACjF,oDAAqG;AACrG,+EAA2G;AAC3G,+CAAsF;AACtF,wDAA6E;AAC7E,+CAAsF;AACtF,6DAAqF;AACrF,8DAAsF;AACtF,sDAAwG;AACxG,uDAA4G;AAE5G,yBAAyB;AACzB,yFAA8D;AAC9D,2EAAgD;AAChD,uEAA4C;AAC5C,mEAAwC;AACxC,mEAAwC;AACxC,yDAA8B;AAC9B,wEAA6C;AAC7C,2EAAgD;AAChD,iFAAsD;AACtD,yDAA8B;AAC9B,+DAAoC;AACpC,yEAA8C;AAC9C,qEAA0C;AAC1C,uEAA4C;AAC5C,iFAAsD;AACtD,qFAA0D;AAC1D,2EAAgD;AAChD,uFAA4D;AAC5D,uEAA4C;AAC5C,yEAA8C;AAE9C,kBAAkB;AAClB,6EAAkD;AAClD,yEAA8C;AAC9C,6EAAkD;AAClD,uFAA4D;AAC5D,2EAAgD;AAChD,qEAA0C;AAC1C,yEAA8C;AAC9C,+DAAoC;AACpC,+DAAoC;AACpC,6EAAkD;AAErC,QAAA,QAAQ,GAAG;IACtB,cAAc;IACd,GAAG,mBAA0B;IAC7B,GAAG,mBAAoB;IACvB,GAAG,mBAAmB;IACtB,GAAG,mBAAiB;IACpB,GAAG,oBAAiB;IACpB,GAAG,oBAAY;IACf,GAAG,oBAAY;IACf,GAAG,oBAAe;IAClB,GAAG,mBAAmB;IACtB,GAAG,mBAAkB;IACrB,GAAG,oBAAuB;IAC1B,GAAG,oBAAyB;IAC5B,GAAG,mBAAoB;IACvB,GAAG,oBAAyB;IAC5B,GAAG,oBAAkB;IACrB,GAAG,oBAAmB;CACvB,CAAA;AAEY,QAAA,MAAM,GAAG;IACpB,QAAQ,EAAE;QACR,0BAA0B,EAA1B,mDAA0B;KAC3B;IAED,eAAe,EAAE;QACf,sBAAsB;QACtB,GAAG,oBAA2B;QAC9B,GAAG,oBAAqB;QACxB,GAAG,oBAAoB;QACvB,GAAG,oBAAkB;QACrB,GAAG,qBAAkB;QACrB,GAAG,qBAAa;QAChB,GAAG,qBAAqB;QACxB,GAAG,qBAAa;QAChB,GAAG,qBAAgB;QACnB,GAAG,oBAAmB;QACtB,GAAG,oBAAkB;QACrB,GAAG,oBAAkB;QACrB,GAAG,oBAAuB;QAC1B,GAAG,oBAAoB;QACvB,GAAG,qBAAkB;QACrB,GAAG,qBAAoB;KACxB;IACD,UAAU,EAAE;QACV,SAAS,EAAE,mDAA0B;KACtC;CACF,CAAA","sourcesContent":["/* IMPORT ENTITIES AND RESOLVERS */\nimport {\n entities as UsersAuthProvidersEntities,\n resolvers as UsersAuthProvidersResolvers\n} from './users-auth-providers/index.js'\nimport { entities as AuthProviderEntities, resolvers as AuthProviderResolvers } from './auth-provider/index.js'\nimport { resolvers as AppbindingResolver } from './app-binding/index.js'\nimport { entities as ApplianceEntities, resolvers as ApplianceResolvers } from './appliance/index.js'\nimport { entities as ApplicationEntities, resolvers as ApplicationResolvers } from './application/index.js'\nimport { resolvers as DomainGeneratorResolver } from './domain-generator/index.js'\nimport { entities as GrantedRoleEntities, resolvers as GrantedRoleResolver } from './granted-role/index.js'\nimport { entities as InvitationEntities, resolvers as InvitationResolver } from './invitation/index.js'\nimport { entities as LoginHistoryEntities, resolvers as LoginHistoryResolver } from './login-history/index.js'\nimport { entities as PartnerEntities, resolvers as PartnerResolvers } from './partner/index.js'\nimport { entities as PasswordHistoryEntities } from './password-history/index.js'\nimport { entities as PrivilegeEntities, resolvers as PrivilegeResolvers } from './privilege/index.js'\nimport { privilegeDirectiveResolver, privilegeDirectiveTypeDefs } from './privilege/privilege-directive.js'\nimport { entities as RoleEntities, resolvers as RoleResolvers } from './role/index.js'\nimport { resolvers as RoleTemplateResolvers } from './role-template/index.js'\nimport { entities as UserEntities, resolvers as UserResolvers } from './user/index.js'\nimport { entities as VerificationTokenEntities } from './verification-token/index.js'\nimport { entities as WebAuthCredentialEntities } from './web-auth-credential/index.js'\nimport { entities as DomainLinkEntities, resolvers as DomainLinkResolver } from './domain-link/index.js'\nimport { entities as DomainOwnerEntities, resolvers as DomainOwnerResolvers } from './domain-owner/index.js'\n\n/* EXPORT ENTITY TYPES */\nexport * from './users-auth-providers/users-auth-providers.js'\nexport * from './auth-provider/auth-provider.js'\nexport * from './application/application.js'\nexport * from './appliance/appliance.js'\nexport * from './privilege/privilege.js'\nexport * from './role/role.js'\nexport * from './privilege/privilege-axis.js'\nexport * from './role-template/role-template.js'\nexport * from './role-template/role-template-types.js'\nexport * from './user/user.js'\nexport * from './partner/partner.js'\nexport * from './granted-role/granted-role.js'\nexport * from './invitation/invitation.js'\nexport * from './app-binding/app-binding.js'\nexport * from './password-history/password-history.js'\nexport * from './verification-token/verification-token.js'\nexport * from './login-history/login-history.js'\nexport * from './web-auth-credential/web-auth-credential.js'\nexport * from './domain-link/domain-link.js'\nexport * from './domain-owner/domain-owner.js'\n\n/* EXPORT TYPES */\nexport * from './app-binding/app-binding-types.js'\nexport * from './appliance/appliance-types.js'\nexport * from './application/application-types.js'\nexport * from './domain-generator/domain-generator-types.js'\nexport * from './invitation/invitation-types.js'\nexport * from './partner/partner-types.js'\nexport * from './privilege/privilege-types.js'\nexport * from './role/role-types.js'\nexport * from './user/user-types.js'\nexport * from './domain-link/domain-link-types.js'\n\nexport const entities = [\n /* ENTITIES */\n ...UsersAuthProvidersEntities,\n ...AuthProviderEntities,\n ...ApplicationEntities,\n ...ApplianceEntities,\n ...PrivilegeEntities,\n ...RoleEntities,\n ...UserEntities,\n ...PartnerEntities,\n ...GrantedRoleEntities,\n ...InvitationEntities,\n ...PasswordHistoryEntities,\n ...VerificationTokenEntities,\n ...LoginHistoryEntities,\n ...WebAuthCredentialEntities,\n ...DomainLinkEntities,\n ...DomainOwnerEntities\n]\n\nexport const schema = {\n typeDefs: {\n privilegeDirectiveTypeDefs\n },\n\n resolverClasses: [\n /* RESOLVER CLASSES */\n ...UsersAuthProvidersResolvers,\n ...AuthProviderResolvers,\n ...ApplicationResolvers,\n ...ApplianceResolvers,\n ...PrivilegeResolvers,\n ...RoleResolvers,\n ...RoleTemplateResolvers,\n ...UserResolvers,\n ...PartnerResolvers,\n ...GrantedRoleResolver,\n ...InvitationResolver,\n ...AppbindingResolver,\n ...DomainGeneratorResolver,\n ...LoginHistoryResolver,\n ...DomainLinkResolver,\n ...DomainOwnerResolvers\n ],\n directives: {\n privilege: privilegeDirectiveResolver\n }\n}\n"]}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an installation gets when no module says otherwise.
|
|
3
|
+
*
|
|
4
|
+
* The two travel together: a house running on the default vocabulary is also running on the
|
|
5
|
+
* default answer to "which of these is read-only". Splitting them would leave the common case
|
|
6
|
+
* — nobody declared anything — with a vocabulary and no idea which half is safe.
|
|
7
|
+
*/
|
|
8
|
+
export declare const DEFAULT_AXES: string[];
|
|
9
|
+
export declare const DEFAULT_READ_ONLY_AXES: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Declare the axes a module uses, and which of them are read-only. Additive.
|
|
12
|
+
*
|
|
13
|
+
* Additive rather than one installation-wide list because a single installation genuinely
|
|
14
|
+
* mixes vocabularies: in dssp, `project` is declared with query/mutation while `kpi` uses
|
|
15
|
+
* eight words of its own. Each module says what it uses, and says it in the same call that
|
|
16
|
+
* says which are read-only — a separate call for the second half could drift out of step with
|
|
17
|
+
* the first, and drift like that is silent.
|
|
18
|
+
*
|
|
19
|
+
* Declaring an axis that is already known is not an error. Two modules reaching for `read`
|
|
20
|
+
* independently is the case this is built for, not a conflict.
|
|
21
|
+
*
|
|
22
|
+
* ── `readOnly` means "holding only this axis changes nothing" ────────────────
|
|
23
|
+
* It does **not** mean `@Query`. The two come apart, and where they do the axis is right and
|
|
24
|
+
* the operation type is the loose one:
|
|
25
|
+
*
|
|
26
|
+
* @Mutation(returns => ExportResult)
|
|
27
|
+
* @Directive('@privilege(category: "label-studio", privilege: "query")')
|
|
28
|
+
* async exportLabelStudioAnnotations(…)
|
|
29
|
+
*
|
|
30
|
+
* That calls an external API's export and hands back what it returns. Nothing in our database
|
|
31
|
+
* moves. It is declared `@Mutation` and the axis is `query`, and **the axis is the correct
|
|
32
|
+
* one** — a role holding only `label-studio:query` still changes nothing by running it.
|
|
33
|
+
*
|
|
34
|
+
* Six declarations in this house disagree this way, and all six are deliberate. Do not
|
|
35
|
+
* "fix" them to match their operation type.
|
|
36
|
+
*/
|
|
37
|
+
export declare function declareAxes(axes: string[], options?: {
|
|
38
|
+
readOnly?: string[];
|
|
39
|
+
}): void;
|
|
40
|
+
/**
|
|
41
|
+
* The axes this installation accepts.
|
|
42
|
+
*
|
|
43
|
+
* Falls back to the default rather than to "no check": an empty registry means nobody
|
|
44
|
+
* declared, not that everything is allowed.
|
|
45
|
+
*/
|
|
46
|
+
export declare function declaredAxes(): Set<string>;
|
|
47
|
+
/** Whether a value may be used as an axis here. */
|
|
48
|
+
export declare function isDeclaredAxis(axis: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* The axes that open nothing a role can change.
|
|
51
|
+
*
|
|
52
|
+
* Empty is a real answer, and it is different from the default. A house that declared its own
|
|
53
|
+
* vocabulary and never said which half is read-only has not told us — see
|
|
54
|
+
* `readOnlyAxesAreKnown`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function readOnlyAxes(): Set<string>;
|
|
57
|
+
/**
|
|
58
|
+
* Whether anyone has said which axes are read-only.
|
|
59
|
+
*
|
|
60
|
+
* `computedRoleTemplates` asks this before offering VIEWER. Guessing would mean matching the
|
|
61
|
+
* literal word `query`, which is what this replaces: it is right in this house and wrong in
|
|
62
|
+
* dssp, where the read axis is spelled `read`.
|
|
63
|
+
*/
|
|
64
|
+
export declare function readOnlyAxesAreKnown(): boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Say what the vocabulary ended up being.
|
|
67
|
+
*
|
|
68
|
+
* The list is not in the SDL and cannot be, since it differs per installation. A line at boot
|
|
69
|
+
* is the honest substitute — and it is the only way to tell "nobody declared, so these are the
|
|
70
|
+
* defaults" from "someone declared exactly these".
|
|
71
|
+
*/
|
|
72
|
+
export declare function reportDeclaredAxes(): void;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_READ_ONLY_AXES = exports.DEFAULT_AXES = void 0;
|
|
4
|
+
exports.declareAxes = declareAxes;
|
|
5
|
+
exports.declaredAxes = declaredAxes;
|
|
6
|
+
exports.isDeclaredAxis = isDeclaredAxis;
|
|
7
|
+
exports.readOnlyAxes = readOnlyAxes;
|
|
8
|
+
exports.readOnlyAxesAreKnown = readOnlyAxesAreKnown;
|
|
9
|
+
exports.reportDeclaredAxes = reportDeclaredAxes;
|
|
10
|
+
const env_1 = require("@things-factory/env");
|
|
11
|
+
/**
|
|
12
|
+
* The axis vocabulary this installation uses.
|
|
13
|
+
*
|
|
14
|
+
* ── ⚠ Call `declareAxes` at the top of the module's `server/index.ts` ────────
|
|
15
|
+
* Not from a bootstrap hook. The check runs while the schema is assembled, and the schema is
|
|
16
|
+
* assembled before every hook a module can reach:
|
|
17
|
+
*
|
|
18
|
+
* shell/server/server.ts:115 emitHook('bootstrap-module-subscription')
|
|
19
|
+
* shell/server/server.ts:117 await schema() ← the axis check runs in here
|
|
20
|
+
* shell/server/server.ts:295 emitHook('bootstrap-module-start')
|
|
21
|
+
*
|
|
22
|
+
* `bootstrap-module-start` is where a module registers most things, role templates included,
|
|
23
|
+
* and it is 178 lines too late for this one.
|
|
24
|
+
*
|
|
25
|
+
* ── What an axis is ──────────────────────────────────────────────────────────
|
|
26
|
+
* The `privilege:` half of `@privilege(category:, privilege:)`. It does **not** name a
|
|
27
|
+
* GraphQL operation. It names which of a category's gates a role has to hold, and the values
|
|
28
|
+
* this house uses happen to borrow transport words.
|
|
29
|
+
*
|
|
30
|
+
* ── Why the framework does not own the list ──────────────────────────────────
|
|
31
|
+
* Closing the axis to `query | mutation` was ruled and then withdrawn (architect,
|
|
32
|
+
* 2026-09-15). It was measured first:
|
|
33
|
+
*
|
|
34
|
+
* things-factory + operato-application 792 declarations, all query or mutation
|
|
35
|
+
* dssp 65 declarations, 40 of them neither
|
|
36
|
+
* kpi: read · input · assessment · sentiment
|
|
37
|
+
* finalize · recalculate · auto-collect
|
|
38
|
+
* basic-info
|
|
39
|
+
*
|
|
40
|
+
* A framework enum would have made dssp's upgrade a 40-declaration migration, and the cost
|
|
41
|
+
* would fall on people who were not in the room. The framework cannot know which words are
|
|
42
|
+
* right; it does not own the modules that use them.
|
|
43
|
+
*
|
|
44
|
+
* ── What this buys ───────────────────────────────────────────────────────────
|
|
45
|
+
* A typo stops being a silent gate. `@privilege(category: "board", privilege: "mutaiton")`
|
|
46
|
+
* used to register a pair in `process['PRIVILEGES']` that nobody would ever be granted — a
|
|
47
|
+
* door permanently shut, with no error anywhere.
|
|
48
|
+
*/
|
|
49
|
+
const REGISTRY = 'PRIVILEGE_AXES';
|
|
50
|
+
/**
|
|
51
|
+
* What an installation gets when no module says otherwise.
|
|
52
|
+
*
|
|
53
|
+
* The two travel together: a house running on the default vocabulary is also running on the
|
|
54
|
+
* default answer to "which of these is read-only". Splitting them would leave the common case
|
|
55
|
+
* — nobody declared anything — with a vocabulary and no idea which half is safe.
|
|
56
|
+
*/
|
|
57
|
+
exports.DEFAULT_AXES = ['query', 'mutation'];
|
|
58
|
+
exports.DEFAULT_READ_ONLY_AXES = ['query'];
|
|
59
|
+
function registry() {
|
|
60
|
+
if (!process[REGISTRY]) {
|
|
61
|
+
process[REGISTRY] = { axes: new Set(), readOnly: new Set(), declared: false };
|
|
62
|
+
}
|
|
63
|
+
return process[REGISTRY];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Declare the axes a module uses, and which of them are read-only. Additive.
|
|
67
|
+
*
|
|
68
|
+
* Additive rather than one installation-wide list because a single installation genuinely
|
|
69
|
+
* mixes vocabularies: in dssp, `project` is declared with query/mutation while `kpi` uses
|
|
70
|
+
* eight words of its own. Each module says what it uses, and says it in the same call that
|
|
71
|
+
* says which are read-only — a separate call for the second half could drift out of step with
|
|
72
|
+
* the first, and drift like that is silent.
|
|
73
|
+
*
|
|
74
|
+
* Declaring an axis that is already known is not an error. Two modules reaching for `read`
|
|
75
|
+
* independently is the case this is built for, not a conflict.
|
|
76
|
+
*
|
|
77
|
+
* ── `readOnly` means "holding only this axis changes nothing" ────────────────
|
|
78
|
+
* It does **not** mean `@Query`. The two come apart, and where they do the axis is right and
|
|
79
|
+
* the operation type is the loose one:
|
|
80
|
+
*
|
|
81
|
+
* @Mutation(returns => ExportResult)
|
|
82
|
+
* @Directive('@privilege(category: "label-studio", privilege: "query")')
|
|
83
|
+
* async exportLabelStudioAnnotations(…)
|
|
84
|
+
*
|
|
85
|
+
* That calls an external API's export and hands back what it returns. Nothing in our database
|
|
86
|
+
* moves. It is declared `@Mutation` and the axis is `query`, and **the axis is the correct
|
|
87
|
+
* one** — a role holding only `label-studio:query` still changes nothing by running it.
|
|
88
|
+
*
|
|
89
|
+
* Six declarations in this house disagree this way, and all six are deliberate. Do not
|
|
90
|
+
* "fix" them to match their operation type.
|
|
91
|
+
*/
|
|
92
|
+
function declareAxes(axes, options = {}) {
|
|
93
|
+
if (!Array.isArray(axes) || axes.length === 0) {
|
|
94
|
+
throw new Error('declareAxes needs at least one axis. An empty list would refuse every declaration in ' +
|
|
95
|
+
'the installation, which is never what the caller meant.');
|
|
96
|
+
}
|
|
97
|
+
for (const axis of axes) {
|
|
98
|
+
if (typeof axis !== 'string' || !axis.trim()) {
|
|
99
|
+
throw new Error(`declareAxes got ${JSON.stringify(axis)} as an axis. Axes are non-empty strings.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const readOnly = options.readOnly || [];
|
|
103
|
+
/*
|
|
104
|
+
* A read-only axis has to be one of the axes named in the same call.
|
|
105
|
+
*
|
|
106
|
+
* Otherwise a typo here is the quietest kind: `readOnly: ['querry']` would mark nothing,
|
|
107
|
+
* VIEWER would silently lose every category of this module, and the screen would count the
|
|
108
|
+
* shortfall correctly and show it as fact.
|
|
109
|
+
*/
|
|
110
|
+
for (const axis of readOnly) {
|
|
111
|
+
if (!axes.includes(axis)) {
|
|
112
|
+
throw new Error(`declareAxes was told "${axis}" is read-only, but that axis is not in the same call. ` +
|
|
113
|
+
`Declared here: ${axes.join(', ')}.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const store = registry();
|
|
117
|
+
for (const axis of axes) {
|
|
118
|
+
store.axes.add(axis);
|
|
119
|
+
}
|
|
120
|
+
for (const axis of readOnly) {
|
|
121
|
+
store.readOnly.add(axis);
|
|
122
|
+
}
|
|
123
|
+
store.declared = true;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The axes this installation accepts.
|
|
127
|
+
*
|
|
128
|
+
* Falls back to the default rather than to "no check": an empty registry means nobody
|
|
129
|
+
* declared, not that everything is allowed.
|
|
130
|
+
*/
|
|
131
|
+
function declaredAxes() {
|
|
132
|
+
const store = registry();
|
|
133
|
+
return store.declared ? store.axes : new Set(exports.DEFAULT_AXES);
|
|
134
|
+
}
|
|
135
|
+
/** Whether a value may be used as an axis here. */
|
|
136
|
+
function isDeclaredAxis(axis) {
|
|
137
|
+
return declaredAxes().has(axis);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The axes that open nothing a role can change.
|
|
141
|
+
*
|
|
142
|
+
* Empty is a real answer, and it is different from the default. A house that declared its own
|
|
143
|
+
* vocabulary and never said which half is read-only has not told us — see
|
|
144
|
+
* `readOnlyAxesAreKnown`.
|
|
145
|
+
*/
|
|
146
|
+
function readOnlyAxes() {
|
|
147
|
+
const store = registry();
|
|
148
|
+
return store.declared ? store.readOnly : new Set(exports.DEFAULT_READ_ONLY_AXES);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Whether anyone has said which axes are read-only.
|
|
152
|
+
*
|
|
153
|
+
* `computedRoleTemplates` asks this before offering VIEWER. Guessing would mean matching the
|
|
154
|
+
* literal word `query`, which is what this replaces: it is right in this house and wrong in
|
|
155
|
+
* dssp, where the read axis is spelled `read`.
|
|
156
|
+
*/
|
|
157
|
+
function readOnlyAxesAreKnown() {
|
|
158
|
+
return readOnlyAxes().size > 0;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Say what the vocabulary ended up being.
|
|
162
|
+
*
|
|
163
|
+
* The list is not in the SDL and cannot be, since it differs per installation. A line at boot
|
|
164
|
+
* is the honest substitute — and it is the only way to tell "nobody declared, so these are the
|
|
165
|
+
* defaults" from "someone declared exactly these".
|
|
166
|
+
*/
|
|
167
|
+
function reportDeclaredAxes() {
|
|
168
|
+
const store = registry();
|
|
169
|
+
const axes = [...declaredAxes()].sort().join(', ');
|
|
170
|
+
const readOnly = [...readOnlyAxes()].sort().join(', ');
|
|
171
|
+
if (!store.declared) {
|
|
172
|
+
env_1.logger.info(`[privilege] no module declared privilege axes; using the defaults: ${axes} (read-only: ${readOnly})`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
env_1.logger.info(`[privilege] axes declared by this installation: ${axes}`);
|
|
176
|
+
if (readOnlyAxesAreKnown()) {
|
|
177
|
+
env_1.logger.info(`[privilege] read-only axes: ${readOnly}`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
/*
|
|
181
|
+
* Loud, because the consequence is a template quietly going missing from the role screen.
|
|
182
|
+
* "We do not know" is not "there are none", and folding the two would hand an administrator
|
|
183
|
+
* a VIEWER that opens nothing.
|
|
184
|
+
*/
|
|
185
|
+
env_1.logger.warn('[privilege] no module said which axes are read-only, so the VIEWER template is not offered. ' +
|
|
186
|
+
"Add it where the axes are declared: declareAxes([...], { readOnly: ['query'] }).");
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=privilege-axis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"privilege-axis.js","sourceRoot":"","sources":["../../../server/service/privilege/privilege-axis.ts"],"names":[],"mappings":";;;AA0FA,kCA2CC;AAQD,oCAIC;AAGD,wCAEC;AASD,oCAIC;AASD,oDAEC;AASD,gDA0BC;AAjND,6CAA4C;AAE5C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,MAAM,QAAQ,GAAG,gBAAgB,CAAA;AAEjC;;;;;;GAMG;AACU,QAAA,YAAY,GAAG,CAAC,OAAO,EAAE,UAAU,CAAC,CAAA;AACpC,QAAA,sBAAsB,GAAG,CAAC,OAAO,CAAC,CAAA;AAI/C,SAAS,QAAQ;IACf,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACvB,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,GAAG,EAAU,EAAE,QAAQ,EAAE,IAAI,GAAG,EAAU,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAA;IAC/F,CAAC;IAED,OAAO,OAAO,CAAC,QAAQ,CAAC,CAAA;AAC1B,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,SAAgB,WAAW,CAAC,IAAc,EAAE,UAAmC,EAAE;IAC/E,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9C,MAAM,IAAI,KAAK,CACb,uFAAuF;YACrF,yDAAyD,CAC5D,CAAA;IACH,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YAC7C,MAAM,IAAI,KAAK,CAAC,mBAAmB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,0CAA0C,CAAC,CAAA;QACpG,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAA;IAEvC;;;;;;OAMG;IACH,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CACb,yBAAyB,IAAI,yDAAyD;gBACpF,kBAAkB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CACvC,CAAA;QACH,CAAC;IACH,CAAC;IAED,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAA;IAExB,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IACtB,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;IAC1B,CAAC;IAED,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAA;AACvB,CAAC;AAED;;;;;GAKG;AACH,SAAgB,YAAY;IAC1B,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAA;IAExB,OAAO,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,oBAAY,CAAC,CAAA;AAC5D,CAAC;AAED,mDAAmD;AACnD,SAAgB,cAAc,CAAC,IAAY;IACzC,OAAO,YAAY,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;AACjC,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,YAAY;IAC1B,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAA;IAExB,OAAO,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,8BAAsB,CAAC,CAAA;AAC1E,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,oBAAoB;IAClC,OAAO,YAAY,EAAE,CAAC,IAAI,GAAG,CAAC,CAAA;AAChC,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,kBAAkB;IAChC,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAA;IACxB,MAAM,IAAI,GAAG,CAAC,GAAG,YAAY,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAClD,MAAM,QAAQ,GAAG,CAAC,GAAG,YAAY,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAEtD,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QACpB,YAAM,CAAC,IAAI,CAAC,sEAAsE,IAAI,gBAAgB,QAAQ,GAAG,CAAC,CAAA;QAClH,OAAM;IACR,CAAC;IAED,YAAM,CAAC,IAAI,CAAC,mDAAmD,IAAI,EAAE,CAAC,CAAA;IAEtE,IAAI,oBAAoB,EAAE,EAAE,CAAC;QAC3B,YAAM,CAAC,IAAI,CAAC,+BAA+B,QAAQ,EAAE,CAAC,CAAA;QACtD,OAAM;IACR,CAAC;IAED;;;;OAIG;IACH,YAAM,CAAC,IAAI,CACT,8FAA8F;QAC5F,kFAAkF,CACrF,CAAA;AACH,CAAC","sourcesContent":["import { logger } from '@things-factory/env'\n\n/**\n * The axis vocabulary this installation uses.\n *\n * ── ⚠ Call `declareAxes` at the top of the module's `server/index.ts` ────────\n * Not from a bootstrap hook. The check runs while the schema is assembled, and the schema is\n * assembled before every hook a module can reach:\n *\n * shell/server/server.ts:115 emitHook('bootstrap-module-subscription')\n * shell/server/server.ts:117 await schema() ← the axis check runs in here\n * shell/server/server.ts:295 emitHook('bootstrap-module-start')\n *\n * `bootstrap-module-start` is where a module registers most things, role templates included,\n * and it is 178 lines too late for this one.\n *\n * ── What an axis is ──────────────────────────────────────────────────────────\n * The `privilege:` half of `@privilege(category:, privilege:)`. It does **not** name a\n * GraphQL operation. It names which of a category's gates a role has to hold, and the values\n * this house uses happen to borrow transport words.\n *\n * ── Why the framework does not own the list ──────────────────────────────────\n * Closing the axis to `query | mutation` was ruled and then withdrawn (architect,\n * 2026-09-15). It was measured first:\n *\n * things-factory + operato-application 792 declarations, all query or mutation\n * dssp 65 declarations, 40 of them neither\n * kpi: read · input · assessment · sentiment\n * finalize · recalculate · auto-collect\n * basic-info\n *\n * A framework enum would have made dssp's upgrade a 40-declaration migration, and the cost\n * would fall on people who were not in the room. The framework cannot know which words are\n * right; it does not own the modules that use them.\n *\n * ── What this buys ───────────────────────────────────────────────────────────\n * A typo stops being a silent gate. `@privilege(category: \"board\", privilege: \"mutaiton\")`\n * used to register a pair in `process['PRIVILEGES']` that nobody would ever be granted — a\n * door permanently shut, with no error anywhere.\n */\n\nconst REGISTRY = 'PRIVILEGE_AXES'\n\n/**\n * What an installation gets when no module says otherwise.\n *\n * The two travel together: a house running on the default vocabulary is also running on the\n * default answer to \"which of these is read-only\". Splitting them would leave the common case\n * — nobody declared anything — with a vocabulary and no idea which half is safe.\n */\nexport const DEFAULT_AXES = ['query', 'mutation']\nexport const DEFAULT_READ_ONLY_AXES = ['query']\n\ntype Registry = { axes: Set<string>; readOnly: Set<string>; declared: boolean }\n\nfunction registry(): Registry {\n if (!process[REGISTRY]) {\n process[REGISTRY] = { axes: new Set<string>(), readOnly: new Set<string>(), declared: false }\n }\n\n return process[REGISTRY]\n}\n\n/**\n * Declare the axes a module uses, and which of them are read-only. Additive.\n *\n * Additive rather than one installation-wide list because a single installation genuinely\n * mixes vocabularies: in dssp, `project` is declared with query/mutation while `kpi` uses\n * eight words of its own. Each module says what it uses, and says it in the same call that\n * says which are read-only — a separate call for the second half could drift out of step with\n * the first, and drift like that is silent.\n *\n * Declaring an axis that is already known is not an error. Two modules reaching for `read`\n * independently is the case this is built for, not a conflict.\n *\n * ── `readOnly` means \"holding only this axis changes nothing\" ────────────────\n * It does **not** mean `@Query`. The two come apart, and where they do the axis is right and\n * the operation type is the loose one:\n *\n * @Mutation(returns => ExportResult)\n * @Directive('@privilege(category: \"label-studio\", privilege: \"query\")')\n * async exportLabelStudioAnnotations(…)\n *\n * That calls an external API's export and hands back what it returns. Nothing in our database\n * moves. It is declared `@Mutation` and the axis is `query`, and **the axis is the correct\n * one** — a role holding only `label-studio:query` still changes nothing by running it.\n *\n * Six declarations in this house disagree this way, and all six are deliberate. Do not\n * \"fix\" them to match their operation type.\n */\nexport function declareAxes(axes: string[], options: { readOnly?: string[] } = {}): void {\n if (!Array.isArray(axes) || axes.length === 0) {\n throw new Error(\n 'declareAxes needs at least one axis. An empty list would refuse every declaration in ' +\n 'the installation, which is never what the caller meant.'\n )\n }\n\n for (const axis of axes) {\n if (typeof axis !== 'string' || !axis.trim()) {\n throw new Error(`declareAxes got ${JSON.stringify(axis)} as an axis. Axes are non-empty strings.`)\n }\n }\n\n const readOnly = options.readOnly || []\n\n /*\n * A read-only axis has to be one of the axes named in the same call.\n *\n * Otherwise a typo here is the quietest kind: `readOnly: ['querry']` would mark nothing,\n * VIEWER would silently lose every category of this module, and the screen would count the\n * shortfall correctly and show it as fact.\n */\n for (const axis of readOnly) {\n if (!axes.includes(axis)) {\n throw new Error(\n `declareAxes was told \"${axis}\" is read-only, but that axis is not in the same call. ` +\n `Declared here: ${axes.join(', ')}.`\n )\n }\n }\n\n const store = registry()\n\n for (const axis of axes) {\n store.axes.add(axis)\n }\n\n for (const axis of readOnly) {\n store.readOnly.add(axis)\n }\n\n store.declared = true\n}\n\n/**\n * The axes this installation accepts.\n *\n * Falls back to the default rather than to \"no check\": an empty registry means nobody\n * declared, not that everything is allowed.\n */\nexport function declaredAxes(): Set<string> {\n const store = registry()\n\n return store.declared ? store.axes : new Set(DEFAULT_AXES)\n}\n\n/** Whether a value may be used as an axis here. */\nexport function isDeclaredAxis(axis: string): boolean {\n return declaredAxes().has(axis)\n}\n\n/**\n * The axes that open nothing a role can change.\n *\n * Empty is a real answer, and it is different from the default. A house that declared its own\n * vocabulary and never said which half is read-only has not told us — see\n * `readOnlyAxesAreKnown`.\n */\nexport function readOnlyAxes(): Set<string> {\n const store = registry()\n\n return store.declared ? store.readOnly : new Set(DEFAULT_READ_ONLY_AXES)\n}\n\n/**\n * Whether anyone has said which axes are read-only.\n *\n * `computedRoleTemplates` asks this before offering VIEWER. Guessing would mean matching the\n * literal word `query`, which is what this replaces: it is right in this house and wrong in\n * dssp, where the read axis is spelled `read`.\n */\nexport function readOnlyAxesAreKnown(): boolean {\n return readOnlyAxes().size > 0\n}\n\n/**\n * Say what the vocabulary ended up being.\n *\n * The list is not in the SDL and cannot be, since it differs per installation. A line at boot\n * is the honest substitute — and it is the only way to tell \"nobody declared, so these are the\n * defaults\" from \"someone declared exactly these\".\n */\nexport function reportDeclaredAxes(): void {\n const store = registry()\n const axes = [...declaredAxes()].sort().join(', ')\n const readOnly = [...readOnlyAxes()].sort().join(', ')\n\n if (!store.declared) {\n logger.info(`[privilege] no module declared privilege axes; using the defaults: ${axes} (read-only: ${readOnly})`)\n return\n }\n\n logger.info(`[privilege] axes declared by this installation: ${axes}`)\n\n if (readOnlyAxesAreKnown()) {\n logger.info(`[privilege] read-only axes: ${readOnly}`)\n return\n }\n\n /*\n * Loud, because the consequence is a template quietly going missing from the role screen.\n * \"We do not know\" is not \"there are none\", and folding the two would hand an administrator\n * a VIEWER that opens nothing.\n */\n logger.warn(\n '[privilege] no module said which axes are read-only, so the VIEWER template is not offered. ' +\n \"Add it where the axes are declared: declareAxes([...], { readOnly: ['query'] }).\"\n )\n}\n"]}
|
|
@@ -1,49 +1,18 @@
|
|
|
1
1
|
import { GraphQLSchema } from 'graphql';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* ⚠ **다음에 목록 인자를 붙이는 사람에게.**
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* 여기 `legacy` 인자가 있었다 — 권한 이름을 옮기는 동안 옛 이름도 받아 주는 별칭이었다.
|
|
6
|
+
* **걷어냈다.** 이 저장소는 대개조 중이고 **호환 계층을 끼우지 않는다**: 이름을 바꾸면 바꾼
|
|
7
|
+
* 것이고, 부여는 관리자가 다시 놓는다. 별칭은 「한동안」이 영원이 되는 길이었다.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* 곧 필요해지는 목록 인자는 **AND** 다(`docs/design/auth-rebuild.md` §1-(3)): 외부에서
|
|
10
|
+
* 당겨오면서 남의 데이터를 앉히는 호출은 `execute` **와** 그 모듈의 `mutation` 을 **둘 다**
|
|
11
|
+
* 요구해야 한다.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* 편집만 필요한 역할에서 나중에 그것을 뺄 때 누가 실제로 필요했는지 알 방법이 없다.
|
|
17
|
-
*
|
|
18
|
-
* 그래서 행은 건드리지 않고 **문에서 한동안 받아 준다.** 그러면 누가 이 문에 기대고 있는지
|
|
19
|
-
* 로그에 쌓이고, 짐작이 아니라 **잰 것으로** 부여할 수 있다.
|
|
20
|
-
*
|
|
21
|
-
* ── 지울 시점은 눈에 보인다 ─────────────────────────────────────────────────
|
|
22
|
-
* **옛 쌍은 `process['PRIVILEGES']` 에 등록하지 않는다.** 그래서 옛 권한 행이
|
|
23
|
-
* `isDeprecatedPrivilege` 기준으로 유령이 되어 관리자 목록에 취소선으로 나타난다
|
|
24
|
-
* (`privilege-deprecation.ts`). 별칭을 지울 때가 됐다는 신호를 새로 만들 필요가 없다.
|
|
25
|
-
*
|
|
26
|
-
* ⚠ **조건만으로 지우지 않는다.** 「N일간 legacy 통과 0건」은 그 업무를 아무도 안 한 조용한
|
|
27
|
-
* 한 주로 충족된다. **고정 날짜**를 받아 그날 지운다.
|
|
28
|
-
*
|
|
29
|
-
* ── 푸는 순서 ───────────────────────────────────────────────────────────────
|
|
30
|
-
* 지금은 둘이다. 모듈 기본값 시임이 서면 그 사이에 한 단이 더 들어간다
|
|
31
|
-
* (`docs/design/auth-rebuild.md` §6-4).
|
|
32
|
-
*
|
|
33
|
-
* ```
|
|
34
|
-
* 지금 필드 선언 → 필드의 legacy → 거절
|
|
35
|
-
* 뒤에 필드 선언 → 필드의 legacy → 모듈 기본값 → DENY
|
|
36
|
-
* ```
|
|
37
|
-
*
|
|
38
|
-
* ── ⚠ 다음에 목록 인자를 붙이는 사람에게 ────────────────────────────────────
|
|
39
|
-
* **`legacy` 는 OR 다** — 새 이름이나 옛 이름, 어느 쪽이든 통과한다.
|
|
40
|
-
*
|
|
41
|
-
* 곧 필요해지는 것은 **AND** 다(`docs/design/auth-rebuild.md` §1-(3)): 외부에서 당겨오면서
|
|
42
|
-
* 남의 데이터를 앉히는 호출은 `execute` **와** 그 모듈의 `mutation` 을 **둘 다** 요구해야 한다.
|
|
43
|
-
*
|
|
44
|
-
* 같은 모양의 인자가 정반대 뜻을 가지므로, 그것을 `privileges: [...]` 처럼 중립적으로 이름
|
|
45
|
-
* 지으면 다음 사람이 「이 중 하나」로 읽는다. 그렇게 쓰는 날 게이트가 **오류 없이** 열린다.
|
|
46
|
-
* 그러니 `requiresAll: [...]` 처럼 **이름이 AND 라고 말하게** 한다.
|
|
13
|
+
* 그것을 `privileges: [...]` 처럼 중립적으로 이름 지으면 다음 사람이 「이 중 하나」로 읽고,
|
|
14
|
+
* 그렇게 쓰는 날 게이트가 **오류 없이** 열린다. `requiresAll: [...]` 처럼 **이름이 AND 라고
|
|
15
|
+
* 말하게** 한다.
|
|
47
16
|
*/
|
|
48
17
|
export declare const privilegeDirectiveTypeDefs: import("graphql").DocumentNode;
|
|
49
18
|
export declare const privilegeDirectiveResolver: (schema: GraphQLSchema) => GraphQLSchema;
|
|
@@ -5,56 +5,25 @@ const tslib_1 = require("tslib");
|
|
|
5
5
|
const graphql_1 = require("graphql");
|
|
6
6
|
const graphql_tag_1 = tslib_1.__importDefault(require("graphql-tag"));
|
|
7
7
|
const utils_1 = require("@graphql-tools/utils");
|
|
8
|
-
const env_1 = require("@things-factory/env");
|
|
9
8
|
const check_permission_js_1 = require("../../utils/check-permission.js");
|
|
9
|
+
const privilege_axis_js_1 = require("./privilege-axis.js");
|
|
10
10
|
const privilege_rejection_js_1 = require("./privilege-rejection.js");
|
|
11
11
|
process['PRIVILEGES'] = {};
|
|
12
12
|
const DIRECTIVE = 'privilege';
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* ⚠ **다음에 목록 인자를 붙이는 사람에게.**
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* 여기 `legacy` 인자가 있었다 — 권한 이름을 옮기는 동안 옛 이름도 받아 주는 별칭이었다.
|
|
17
|
+
* **걷어냈다.** 이 저장소는 대개조 중이고 **호환 계층을 끼우지 않는다**: 이름을 바꾸면 바꾼
|
|
18
|
+
* 것이고, 부여는 관리자가 다시 놓는다. 별칭은 「한동안」이 영원이 되는 길이었다.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* 곧 필요해지는 목록 인자는 **AND** 다(`docs/design/auth-rebuild.md` §1-(3)): 외부에서
|
|
21
|
+
* 당겨오면서 남의 데이터를 앉히는 호출은 `execute` **와** 그 모듈의 `mutation` 을 **둘 다**
|
|
22
|
+
* 요구해야 한다.
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* 편집만 필요한 역할에서 나중에 그것을 뺄 때 누가 실제로 필요했는지 알 방법이 없다.
|
|
28
|
-
*
|
|
29
|
-
* 그래서 행은 건드리지 않고 **문에서 한동안 받아 준다.** 그러면 누가 이 문에 기대고 있는지
|
|
30
|
-
* 로그에 쌓이고, 짐작이 아니라 **잰 것으로** 부여할 수 있다.
|
|
31
|
-
*
|
|
32
|
-
* ── 지울 시점은 눈에 보인다 ─────────────────────────────────────────────────
|
|
33
|
-
* **옛 쌍은 `process['PRIVILEGES']` 에 등록하지 않는다.** 그래서 옛 권한 행이
|
|
34
|
-
* `isDeprecatedPrivilege` 기준으로 유령이 되어 관리자 목록에 취소선으로 나타난다
|
|
35
|
-
* (`privilege-deprecation.ts`). 별칭을 지울 때가 됐다는 신호를 새로 만들 필요가 없다.
|
|
36
|
-
*
|
|
37
|
-
* ⚠ **조건만으로 지우지 않는다.** 「N일간 legacy 통과 0건」은 그 업무를 아무도 안 한 조용한
|
|
38
|
-
* 한 주로 충족된다. **고정 날짜**를 받아 그날 지운다.
|
|
39
|
-
*
|
|
40
|
-
* ── 푸는 순서 ───────────────────────────────────────────────────────────────
|
|
41
|
-
* 지금은 둘이다. 모듈 기본값 시임이 서면 그 사이에 한 단이 더 들어간다
|
|
42
|
-
* (`docs/design/auth-rebuild.md` §6-4).
|
|
43
|
-
*
|
|
44
|
-
* ```
|
|
45
|
-
* 지금 필드 선언 → 필드의 legacy → 거절
|
|
46
|
-
* 뒤에 필드 선언 → 필드의 legacy → 모듈 기본값 → DENY
|
|
47
|
-
* ```
|
|
48
|
-
*
|
|
49
|
-
* ── ⚠ 다음에 목록 인자를 붙이는 사람에게 ────────────────────────────────────
|
|
50
|
-
* **`legacy` 는 OR 다** — 새 이름이나 옛 이름, 어느 쪽이든 통과한다.
|
|
51
|
-
*
|
|
52
|
-
* 곧 필요해지는 것은 **AND** 다(`docs/design/auth-rebuild.md` §1-(3)): 외부에서 당겨오면서
|
|
53
|
-
* 남의 데이터를 앉히는 호출은 `execute` **와** 그 모듈의 `mutation` 을 **둘 다** 요구해야 한다.
|
|
54
|
-
*
|
|
55
|
-
* 같은 모양의 인자가 정반대 뜻을 가지므로, 그것을 `privileges: [...]` 처럼 중립적으로 이름
|
|
56
|
-
* 지으면 다음 사람이 「이 중 하나」로 읽는다. 그렇게 쓰는 날 게이트가 **오류 없이** 열린다.
|
|
57
|
-
* 그러니 `requiresAll: [...]` 처럼 **이름이 AND 라고 말하게** 한다.
|
|
24
|
+
* 그것을 `privileges: [...]` 처럼 중립적으로 이름 지으면 다음 사람이 「이 중 하나」로 읽고,
|
|
25
|
+
* 그렇게 쓰는 날 게이트가 **오류 없이** 열린다. `requiresAll: [...]` 처럼 **이름이 AND 라고
|
|
26
|
+
* 말하게** 한다.
|
|
58
27
|
*/
|
|
59
28
|
exports.privilegeDirectiveTypeDefs = (0, graphql_tag_1.default) `
|
|
60
29
|
directive @privilege(
|
|
@@ -62,7 +31,6 @@ exports.privilegeDirectiveTypeDefs = (0, graphql_tag_1.default) `
|
|
|
62
31
|
privilege: String
|
|
63
32
|
domainOwnerGranted: Boolean
|
|
64
33
|
superUserGranted: Boolean
|
|
65
|
-
legacy: String
|
|
66
34
|
) on FIELD_DEFINITION
|
|
67
35
|
`;
|
|
68
36
|
const privilegeDirectiveResolver = (schema) => (0, utils_1.mapSchema)(schema, {
|
|
@@ -73,19 +41,61 @@ const privilegeDirectiveResolver = (schema) => (0, utils_1.mapSchema)(schema, {
|
|
|
73
41
|
if (!args) {
|
|
74
42
|
throw new Error(`Unexpected Error. args should be defined in @privilege directive for field ${fieldName}.`);
|
|
75
43
|
}
|
|
76
|
-
const { domainOwnerGranted, superUserGranted, category, privilege
|
|
77
|
-
|
|
78
|
-
|
|
44
|
+
const { domainOwnerGranted, superUserGranted, category, privilege } = privilegeDirective;
|
|
45
|
+
const where = `${typeName}.${fieldName}`;
|
|
46
|
+
/*
|
|
47
|
+
* A declaration has to name something.
|
|
48
|
+
*
|
|
49
|
+
* `category` and `privilege` are one thing, not two: together they name a row in the
|
|
50
|
+
* privilege table, and either alone names no row, so `User.hasPrivilege` is never
|
|
51
|
+
* reached and the field falls back on ownership flags it did not set.
|
|
52
|
+
*/
|
|
53
|
+
if (!category !== !privilege) {
|
|
54
|
+
throw new Error(`@privilege on ${where} gives ${category ? 'a category with no privilege' : 'a privilege with no category'}. ` +
|
|
55
|
+
`The pair names the privilege to look for; one half names none.`);
|
|
56
|
+
}
|
|
57
|
+
/*
|
|
58
|
+
* `@privilege()` with nothing in it reads like a gate and guards nothing. Until
|
|
59
|
+
* 2026-09-15 what it did depended on where the caller sat: `checkPermission` answered
|
|
60
|
+
* false from a safe address and refused everyone, true from an unsafe one and let
|
|
61
|
+
* everyone through. Both are false now, so the shape is merely useless rather than
|
|
62
|
+
* dangerous — and a field that is open should say so by carrying no directive, not by
|
|
63
|
+
* carrying an empty one.
|
|
64
|
+
*
|
|
65
|
+
* It is also the shape someone reaches for wanting "being signed in is enough". That
|
|
66
|
+
* is a real thing to want and this directive does not express it; the answer settled
|
|
67
|
+
* on 2026-09-15 is a category for the published side of a module, `<module>-published`.
|
|
68
|
+
*
|
|
69
|
+
* Throwing stops the boot, which is what §6-2 of docs/design/auth-rebuild.md settled
|
|
70
|
+
* for a resolver that declares nothing. A decorator that declares nothing is that
|
|
71
|
+
* same resolver wearing one.
|
|
72
|
+
*/
|
|
73
|
+
if (!category && !privilege && !domainOwnerGranted && !superUserGranted) {
|
|
74
|
+
throw new Error(`@privilege on ${where} declares nothing. ` +
|
|
75
|
+
`Name (category, privilege) for a privilege gate, or domainOwnerGranted / superUserGranted for an ` +
|
|
76
|
+
`ownership gate. A field open to everyone carries no directive at all.`);
|
|
79
77
|
}
|
|
80
78
|
/*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
79
|
+
* The axis has to be one this installation declared.
|
|
80
|
+
*
|
|
81
|
+
* Without this, `privilege: "mutaiton"` registered a pair nobody would ever be granted
|
|
82
|
+
* — a gate shut forever, with no error to read. The list is not the framework's: see
|
|
83
|
+
* `privilege-axis.ts` for why, and for the fact that it must be declared at module
|
|
84
|
+
* import time rather than on a bootstrap hook.
|
|
83
85
|
*/
|
|
86
|
+
if (privilege && !(0, privilege_axis_js_1.isDeclaredAxis)(privilege)) {
|
|
87
|
+
throw new Error(`@privilege on ${where} uses axis "${privilege}", which this installation does not declare. ` +
|
|
88
|
+
`Declared axes: ${[...(0, privilege_axis_js_1.declaredAxes)()].sort().join(', ')}. ` +
|
|
89
|
+
`Fix the spelling, or call declareAxes(['${privilege}']) at the top level of the module's ` +
|
|
90
|
+
`server/index.ts — a bootstrap hook runs after the schema is built and is too late.`);
|
|
91
|
+
}
|
|
92
|
+
if (category && privilege) {
|
|
93
|
+
process['PRIVILEGES'][`${category} ${privilege}`] = [category, privilege];
|
|
94
|
+
}
|
|
84
95
|
// 필드의 기존 description 가져오기
|
|
85
96
|
const existingDescription = fieldConfig.description || '';
|
|
86
97
|
// 권한 정보를 포함한 새로운 description 생성
|
|
87
98
|
const privilegeDescription = `\n\n🔒 Requires privilege: ${category}:${privilege}` +
|
|
88
|
-
(legacy ? ` (or ${legacy}:${privilege} while it is being moved)` : '') +
|
|
89
99
|
(domainOwnerGranted ? ', Domain ownership' : '') +
|
|
90
100
|
(superUserGranted ? ', System ownership' : '');
|
|
91
101
|
// 기존 description과 결합
|
|
@@ -102,33 +112,11 @@ const privilegeDirectiveResolver = (schema) => (0, utils_1.mapSchema)(schema, {
|
|
|
102
112
|
return await resolve.call(this, source, args, context, info);
|
|
103
113
|
}
|
|
104
114
|
/*
|
|
105
|
-
*
|
|
115
|
+
* 안 열렸으면 **거기서 끝이다.**
|
|
106
116
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
117
|
+
* 여기 옛 이름으로 한 번 더 보는 길이 있었다. 그것을 걷어냈다 — 호환 계층을 끼우지
|
|
118
|
+
* 않는다. 이름을 옮기면 부여도 옮기는 것이고, 그 일은 관리자가 역할 화면에서 한다.
|
|
109
119
|
*
|
|
110
|
-
* **금지 목록에 새 쌍이 있으면 옛 이름도 보지 않는다.** 금지는 관리자가 「이 IP 에서는
|
|
111
|
-
* 이것을 못 한다」고 명시적으로 적어 둔 것인데, 금지 목록은 옛 이름을 모른다. 별칭이
|
|
112
|
-
* 금지를 되돌리면 그 설정이 조용히 무력해진다.
|
|
113
|
-
*/
|
|
114
|
-
const prohibited = (prohibitedPrivileges || []).some(pp => pp.category == category && pp.privilege == privilege);
|
|
115
|
-
if (legacy && category && privilege && !prohibited) {
|
|
116
|
-
const byLegacy = await (0, check_permission_js_1.checkPermission)({ category: legacy, privilege }, user, domain, unsafeIP, prohibitedPrivileges);
|
|
117
|
-
if (byLegacy) {
|
|
118
|
-
/*
|
|
119
|
-
* **누가 이 문에 기대고 있나.** 이 목록이 부여의 근거가 된다 — 짐작으로 얹지 않는다.
|
|
120
|
-
*
|
|
121
|
-
* 어느 역할이 그것을 줬는지는 남기지 않는다. 그것을 알려면 매 요청 부여 경로를
|
|
122
|
-
* 되짚는 질의가 하나 더 붙고, 여기는 모든 요청이 지나는 자리다. 사람과 리졸버가
|
|
123
|
-
* 있으면 관리자가 역할 화면에서 찾을 수 있다.
|
|
124
|
-
*/
|
|
125
|
-
env_1.logger.warn(`privilege moved: ${typeName}.${fieldName} passed on the legacy name ` +
|
|
126
|
-
`${legacy}:${privilege} (new name ${category}:${privilege}) for ${user?.email || user?.id}`);
|
|
127
|
-
return await resolve.call(this, source, args, context, info);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
/*
|
|
131
|
-
* 거절은 **새 이름**으로 말한다 — 옛 이름을 말하면 관리자가 사라질 권한을 부여한다.
|
|
132
120
|
* 문장 만들기는 `privilege-rejection.ts` 가 소유한다.
|
|
133
121
|
*/
|
|
134
122
|
throw (0, privilege_rejection_js_1.privilegeRejection)(context, category, privilege);
|