@jwilleke/ngdpbase-types 5.42.0 → 5.43.0
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/lib/src/context/ApiContext.js +1 -1
- package/lib/src/context/BaseContext.d.ts +20 -20
- package/lib/src/context/BaseContext.js +41 -24
- package/lib/src/context/JobContext.d.ts +31 -0
- package/lib/src/context/JobContext.js +34 -1
- package/lib/src/managers/AddonsManager.d.ts +0 -28
- package/lib/src/managers/AgentTokenManager.d.ts +8 -0
- package/lib/src/managers/ConfigurationManager.d.ts +15 -0
- package/lib/src/managers/PageManager.d.ts +185 -23
- package/lib/src/managers/PolicyEvaluator.d.ts +99 -24
- package/lib/src/managers/ShareManager.d.ts +3 -2
- package/lib/src/managers/UserManager.d.ts +19 -5
- package/lib/src/managers/ValidationManager.d.ts +3 -1
- package/lib/src/providers/AgentTokenAuthProvider.js +1 -1
- package/lib/src/providers/BaseAuthProvider.d.ts +2 -0
- package/lib/src/security/PolicyDecisionPoint.d.ts +57 -1
- package/lib/src/security/PolicyInformationPoint.d.ts +86 -6
- package/lib/src/security/permissions.generated.d.ts +8 -0
- package/lib/src/types/Page.d.ts +0 -7
- package/lib/src/types/Policy.d.ts +67 -6
- package/lib/src/types/Policy.js +6 -0
- package/lib/src/types/Provider.d.ts +12 -0
- package/lib/src/types/User.d.ts +6 -0
- package/lib/src/utils/addonPageSync.d.ts +0 -12
- package/lib/src/utils/auditEventNames.generated.d.ts +2 -0
- package/lib/src/utils/auditEventNames.generated.js +2 -0
- package/lib/src/utils/auditEvents.d.ts +2 -0
- package/lib/src/utils/auditEvents.js +3 -1
- package/lib/src/utils/frontmatterAccess.d.ts +15 -24
- package/lib/src/utils/legacyStoreKeys.d.ts +16 -0
- package/lib/src/utils/pageWritePolicies.d.ts +55 -0
- package/lib/src/utils/permissionCatalog.d.ts +2 -0
- package/lib/src/utils/permissionCatalog.js +19 -0
- package/lib/src/utils/policyAttributes.d.ts +141 -0
- package/lib/src/utils/policyAttributes.js +219 -0
- package/lib/src/utils/privateStoreAccess.d.ts +24 -4
- package/lib/src/utils/privateStoreAccess.js +19 -4
- package/lib/src/utils/username.d.ts +13 -2
- package/lib/src/utils/username.js +18 -3
- package/package.json +1 -1
|
@@ -30,6 +30,8 @@ import { type RoleGrant } from '../utils/roleGrants.js';
|
|
|
30
30
|
import { type ActorContext } from '../context/ActorContext.js';
|
|
31
31
|
export declare const POLICIES_KEY = "ngdpbase.access.policies";
|
|
32
32
|
export declare const POLICIES_ENABLED_KEY = "ngdpbase.access.policies.enabled";
|
|
33
|
+
/** Whether an administrator may enter another person's unencrypted vault (#2255). */
|
|
34
|
+
export declare const VAULTS_ADMIN_ACCESS_KEY = "ngdpbase.vaults.admin-access";
|
|
33
35
|
/**
|
|
34
36
|
* What `decide`, `permits` and `holds` take (#2022): the caller's own context
|
|
35
37
|
* — a request's `PermissionSubject` or a job's `JobContext` — or a subject
|
|
@@ -256,7 +258,20 @@ export declare class PolicyDecisionPoint extends BaseManager {
|
|
|
256
258
|
* own sign-in, once policy has said yes.
|
|
257
259
|
*/
|
|
258
260
|
private proxyCeiling;
|
|
259
|
-
/**
|
|
261
|
+
/**
|
|
262
|
+
* Does the named actor HOLD the action (policy alone, live)? Step-up is the
|
|
263
|
+
* proxy subject's question, not theirs. Asked about the same page when the
|
|
264
|
+
* request names one (#2255), so an actor who may edit only some categories
|
|
265
|
+
* is asked about this page's.
|
|
266
|
+
*
|
|
267
|
+
* Inside a vault or a shared folder the actor is asked the bare capability,
|
|
268
|
+
* as before #2255 (#2267): the `vault` and `shared-folder` matchers assume
|
|
269
|
+
* the container rule proved the subject may be there, and for the actor it
|
|
270
|
+
* did not — the person represented passed it. Asked with the container, a
|
|
271
|
+
* reader actor would hold everything `vault-owner` holds in someone else's
|
|
272
|
+
* vault. The door (`permitsInVault`), the page door and the list filter all
|
|
273
|
+
* come through here, so they agree.
|
|
274
|
+
*/
|
|
260
275
|
private actorHolds;
|
|
261
276
|
/**
|
|
262
277
|
* Decide. The order is load-bearing:
|
|
@@ -277,5 +292,46 @@ export declare class PolicyDecisionPoint extends BaseManager {
|
|
|
277
292
|
decide(subject: DecisionInput, request: DecisionRequest): Promise<Decision>;
|
|
278
293
|
/** `decide`, with the step-up rule applied or not — {@link holds} is the only caller that leaves it out. */
|
|
279
294
|
private evaluate;
|
|
295
|
+
/**
|
|
296
|
+
* The first declared system category on which the policies permit a page
|
|
297
|
+
* write (#2255, decision 10), or null. Asked once per category, so a deny
|
|
298
|
+
* on one still wins there. Owner conditions are unknown here (no page), so
|
|
299
|
+
* a grant that needs the author does not count. Only enabled categories
|
|
300
|
+
* that are not bound to a location count (#2268): `shared` is a shared
|
|
301
|
+
* folder's, decided with the folder, and every account holds `vault-owner`,
|
|
302
|
+
* so counting it would give a reader every page write as a capability.
|
|
303
|
+
*/
|
|
304
|
+
private onSomeCategory;
|
|
305
|
+
/**
|
|
306
|
+
* Every system category the catalogue declares, core and add-ons, enabled
|
|
307
|
+
* or not, lowercased; with `inUse`, only those enabled and not bound to a
|
|
308
|
+
* shared folder (`storageLocation.sharedfolder`).
|
|
309
|
+
*/
|
|
310
|
+
private declaredCategories;
|
|
311
|
+
/**
|
|
312
|
+
* May an administrator enter `owner`'s vault (#2255,
|
|
313
|
+
* `ngdpbase.vaults.admin-access`)? Asked only after the container rule has
|
|
314
|
+
* refused someone who is not the owner. Yes when all hold:
|
|
315
|
+
*
|
|
316
|
+
* - the key is true, read now, on every request (shipped true);
|
|
317
|
+
* - the caller is a signed-in session or a job, not a share link, a proxy
|
|
318
|
+
* or an agent token;
|
|
319
|
+
* - the caller holds `admin-system`;
|
|
320
|
+
* - the vault is not encrypted: an encrypted vault always needs its key,
|
|
321
|
+
* whatever the key says, so it is not entered at all.
|
|
322
|
+
*
|
|
323
|
+
* `record` true (a door) writes `vault-admin-access` with the administrator
|
|
324
|
+
* as the actor and the owner, vault and page named; an affordance passes
|
|
325
|
+
* false and records nothing. Lists and search never ask this, so an
|
|
326
|
+
* administrator does not see other people's vault pages listed.
|
|
327
|
+
*/
|
|
328
|
+
adminMayEnterVault(ctx: ActorContext | null | undefined, owner: string, vault: string, resource: string, record?: boolean): Promise<boolean>;
|
|
329
|
+
/**
|
|
330
|
+
* The declared system categories on which an allow policy grants `action`
|
|
331
|
+
* through the `system-category` resource type (#2255). The sitemap asks it
|
|
332
|
+
* with `pushed-pages-edit` to leave pushed pages out. A summary of the
|
|
333
|
+
* policies, never an answer to "may this subject".
|
|
334
|
+
*/
|
|
335
|
+
categoriesGranting(action: string): string[];
|
|
280
336
|
}
|
|
281
337
|
export default PolicyDecisionPoint;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import BaseManager from '../managers/BaseManager.js';
|
|
2
|
+
import { type AalFacts } from '../utils/policyAttributes.js';
|
|
2
3
|
import { type ActorContext } from '../context/ActorContext.js';
|
|
3
4
|
import { WikiEngine } from '../types/WikiEngine.js';
|
|
4
5
|
import type { AgentTokenGrant, ProxyClaim } from '../managers/UserManager.js';
|
|
@@ -52,6 +53,10 @@ interface UserContext {
|
|
|
52
53
|
* form. They convert it themselves, through {@link asSubject}.
|
|
53
54
|
*/
|
|
54
55
|
type PageQuestionCaller = ActorContext | UserContext | null | undefined;
|
|
56
|
+
/** What a subject attribute is read from (#2261): who, and what bears on `subject:aal`. */
|
|
57
|
+
export type SubjectFacts = AalFacts & {
|
|
58
|
+
username?: string;
|
|
59
|
+
};
|
|
55
60
|
/**
|
|
56
61
|
* Access decision log entry
|
|
57
62
|
*/
|
|
@@ -61,8 +66,24 @@ interface AccessDecisionLog {
|
|
|
61
66
|
action?: string;
|
|
62
67
|
allowed?: boolean;
|
|
63
68
|
reason?: string;
|
|
69
|
+
/** The policies that decided (#2255), recorded on a denial. */
|
|
70
|
+
policyIds?: string[];
|
|
71
|
+
/** Allows that matched but whose condition was not met (#2288), recorded on a denial. */
|
|
72
|
+
conditionNotMet?: string[];
|
|
64
73
|
context?: Record<string, unknown>;
|
|
65
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* What the policies may read about a page (#2255), resolved by the PIP from
|
|
77
|
+
* the page itself. `attributes` holds what a resource type matches on
|
|
78
|
+
* (`system-category`), read eagerly; `read` answers a condition's
|
|
79
|
+
* `resource:<name>` for an attribute declared on the `page` resource type,
|
|
80
|
+
* lazily, so a lookup (the owner's account) is made only when a policy needs
|
|
81
|
+
* it. `undefined` is a missing attribute.
|
|
82
|
+
*/
|
|
83
|
+
export interface PageAttributes {
|
|
84
|
+
attributes: Record<string, unknown>;
|
|
85
|
+
read(name: string): unknown;
|
|
86
|
+
}
|
|
66
87
|
/**
|
|
67
88
|
* PolicyInformationPoint — what a page decision needs to know about the page
|
|
68
89
|
* (#1431; was ACLManager until step 8).
|
|
@@ -353,10 +374,12 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
353
374
|
* Tier 1 is skipped for a vault page: `audience` grants nothing there.
|
|
354
375
|
* - __Tier 0.5, author-lock__, `edit` only. It only denies: an author, or a
|
|
355
376
|
* subject holding `admin-system` (#1431 7b), falls through to Tier 1+.
|
|
356
|
-
* - __Tier 1, frontmatter__ `
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
377
|
+
* - __Tier 1, frontmatter__ `audience`. Page-level, and decides a view
|
|
378
|
+
* when it states a rule (#1054). An `audience` covers every action, and a
|
|
379
|
+
* subject holding `admin-system` is its one exception (#1964). `access`
|
|
380
|
+
* is no longer read (#2255).
|
|
381
|
+
* - A page in a shared folder is decided by the folder's permission, and a
|
|
382
|
+
* change also by the `shared` category's policy and author-lock (#2255).
|
|
360
383
|
* - __Tier 2.__ For a share visitor the share IS the policy (#1222); for
|
|
361
384
|
* anyone else, global policy.
|
|
362
385
|
* - __Default deny.__ Nothing below Tier 2 grants — JSPWiki's page-body ACL
|
|
@@ -477,14 +500,65 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
477
500
|
*/
|
|
478
501
|
canUserAccessMediaItem(caller: PageQuestionCaller, item: MediaItem): Promise<boolean>;
|
|
479
502
|
/**
|
|
480
|
-
* Check front matter audience
|
|
503
|
+
* Check the front matter audience (Tier 1).
|
|
481
504
|
* Returns a decision object; decided=false means no front matter restriction — fall through.
|
|
482
505
|
*
|
|
483
506
|
* An audience that leaves the subject out refuses every action, unless the
|
|
484
507
|
* subject holds `admin-system` (#1964, operator 2026-10-23): asked only
|
|
485
|
-
* then. `access
|
|
508
|
+
* then. `access` is no longer read (#2255).
|
|
486
509
|
*/
|
|
487
510
|
private checkFrontmatterAccess;
|
|
511
|
+
/**
|
|
512
|
+
* A page's attributes for the policies (#2255, XACML's PIP): the PDP asks
|
|
513
|
+
* this from the page id, so no route passes them. `knownMetadata` is the
|
|
514
|
+
* frontmatter when this PIP already holds it (the page door); otherwise the
|
|
515
|
+
* page is read with the caller's own context, so a sealed page stays closed
|
|
516
|
+
* to anyone without its key (#1422). A page that cannot be read has no
|
|
517
|
+
* attributes: no category policy matches it and every condition on it is
|
|
518
|
+
* unknown.
|
|
519
|
+
*/
|
|
520
|
+
pageAttributes(caller: PageQuestionCaller, pageName: string, knownMetadata?: Record<string, unknown> | null): Promise<PageAttributes>;
|
|
521
|
+
/**
|
|
522
|
+
* {@link pageAttributes} from frontmatter already in hand (the list
|
|
523
|
+
* filter). Attributes are those declared on the `page` resource type in
|
|
524
|
+
* `ngdpbase.access.resource-types`, each read from its `from` source; the
|
|
525
|
+
* sources are code (`frontmatter:<field>` today).
|
|
526
|
+
*
|
|
527
|
+
* - `system-category` is lowercased; a page with none is in the default
|
|
528
|
+
* category (the entry with `default: true`); a page in a shared folder is
|
|
529
|
+
* `shared`, whatever its frontmatter says (#2255).
|
|
530
|
+
* - An `identity` attribute (`owner`, from `author`) is compared by the
|
|
531
|
+
* account it names: an @id or a username not yet converted both read as
|
|
532
|
+
* that account's username, an orphan as missing (#2097).
|
|
533
|
+
*/
|
|
534
|
+
pageAttributesFrom(pageName: string, metadata: PageFrontmatter | null | undefined): PageAttributes;
|
|
535
|
+
/**
|
|
536
|
+
* A subject attribute a condition reads (#2255, #2261): `subject:<name>`,
|
|
537
|
+
* declared in `ngdpbase.access.subject-attributes` (`id` always is) and read
|
|
538
|
+
* from its `from` source here, the only place a subject attribute is read. A
|
|
539
|
+
* request cannot send one. Missing when it is not declared, cannot be read, or
|
|
540
|
+
* the subject has none (`subject:aal` of a visitor who is not signed in, of a
|
|
541
|
+
* token made before it was stored); the evaluator then fails closed. Returns
|
|
542
|
+
* the value, or a Promise of it for a field read from the account (the
|
|
543
|
+
* evaluator awaits every read).
|
|
544
|
+
*/
|
|
545
|
+
subjectAttribute(subject: SubjectFacts | null | undefined, name: string): unknown;
|
|
546
|
+
/** One read of a declared subject attribute from its source. A value of the wrong type, or a source that throws, is missing. */
|
|
547
|
+
private readSubjectSource;
|
|
548
|
+
/** The page's category for the policies (#2255): lowercased; the default when it has none; `shared` in a shared folder. */
|
|
549
|
+
private pageCategory;
|
|
550
|
+
/** The attributes declared on the `page` resource type (#2255). */
|
|
551
|
+
private declaredPageAttributes;
|
|
552
|
+
/** An identity reference as the policies compare it: the username of the account it names, or missing (#2097). */
|
|
553
|
+
private identityValue;
|
|
554
|
+
/** Pages already warned about for an `access:` that is not the legacy stamp (#2255): once per page per process. */
|
|
555
|
+
private readonly warnedAccess;
|
|
556
|
+
/**
|
|
557
|
+
* `access:` frontmatter no longer has any effect (#2255). The stamp install
|
|
558
|
+
* wrote (`{ edit: [admin] }`) is left alone silently; any other value is
|
|
559
|
+
* someone's rule that is now ignored, so it is said once per page.
|
|
560
|
+
*/
|
|
561
|
+
private warnLegacyAccess;
|
|
488
562
|
/**
|
|
489
563
|
* Does the page's `audience` admit this subject (#1964)? True when the page
|
|
490
564
|
* names no audience, the subject matches it, or the subject holds
|
|
@@ -501,6 +575,12 @@ declare class PolicyInformationPoint extends BaseManager {
|
|
|
501
575
|
* Accepts either a single object or positional args for backward compatibility.
|
|
502
576
|
*/
|
|
503
577
|
logAccessDecision(userOrObj: UserContext | AccessDecisionLog, pageName?: string, action?: string, allowed?: boolean, reason?: string, _context?: Record<string, unknown>): void;
|
|
578
|
+
/**
|
|
579
|
+
* Record a page write a door refused on a capability alone (#2272): the
|
|
580
|
+
* route's `page-edit` / `page-create` check asks the PDP, which records
|
|
581
|
+
* nothing. Same record as every other page denial, `policyIds` included.
|
|
582
|
+
*/
|
|
583
|
+
recordPageRefusal(subject: UserContext | null | undefined, pageName: string, action: string, reason: string, policyIds?: readonly string[], conditionNotMet?: readonly string[]): Promise<void>;
|
|
504
584
|
/** The name a decision is recorded under: the subject's, or `anonymous`. */
|
|
505
585
|
private static recordedUser;
|
|
506
586
|
/**
|
|
@@ -30,6 +30,8 @@ export type CorePermission =
|
|
|
30
30
|
| 'identity-read'
|
|
31
31
|
/** Browse and view the media library (not delegated to anonymous visitors) */
|
|
32
32
|
| 'media-read'
|
|
33
|
+
/** Set or change a page's system-category (#2255). A page created in a shared folder gets the shared category without it */
|
|
34
|
+
| 'page-categorize'
|
|
33
35
|
/** Create new pages */
|
|
34
36
|
| 'page-create'
|
|
35
37
|
/** Delete pages */
|
|
@@ -50,6 +52,12 @@ export type CorePermission =
|
|
|
50
52
|
| 'profile-manage'
|
|
51
53
|
/** Let another person act for you, within permissions you hold, and revoke it (#1762). Every signed-in role except demo-admin; an administrator making or revoking a grant for someone else also needs user-edit, and that grant never carries a copy of the person's vault key. A grant that includes proxy-grant lets the proxy give others access too. Asks for a fresh sign-in */
|
|
52
54
|
| 'proxy-grant'
|
|
55
|
+
/** Delete pushed pages (#2255) */
|
|
56
|
+
| 'pushed-pages-delete'
|
|
57
|
+
/** Edit pushed pages: pages shipped by the system or an add-on (the categories a policy grants it on; system, documentation and addon as shipped) (#2255) */
|
|
58
|
+
| 'pushed-pages-edit'
|
|
59
|
+
/** Rename pushed pages (#2255) */
|
|
60
|
+
| 'pushed-pages-rename'
|
|
53
61
|
/** Search pages */
|
|
54
62
|
| 'search-page'
|
|
55
63
|
/** Search users */
|
package/lib/src/types/Page.d.ts
CHANGED
|
@@ -95,13 +95,6 @@ export interface PageFrontmatter {
|
|
|
95
95
|
* in Slice E (v3.7.0). All datasets migrated; this field is the only source of truth.
|
|
96
96
|
*/
|
|
97
97
|
private?: boolean;
|
|
98
|
-
/** Per-action principal lists — overrides audience for the named action */
|
|
99
|
-
access?: {
|
|
100
|
-
view?: string[];
|
|
101
|
-
edit?: string[];
|
|
102
|
-
delete?: string[];
|
|
103
|
-
[key: string]: string[] | undefined;
|
|
104
|
-
};
|
|
105
98
|
/** Additional custom metadata */
|
|
106
99
|
[key: string]: unknown;
|
|
107
100
|
}
|
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
* again because a `.d.ts` may not import a `.ts`. Four copies of one shape is
|
|
13
13
|
* how a field gets added to one and missed in the others.
|
|
14
14
|
*/
|
|
15
|
-
/** Who a policy grants to. The evaluator matches `role` and `
|
|
15
|
+
/** Who a policy grants to. The evaluator matches `role`, `job` (#1999), `authenticated` and `anyone` (#2261), its `SUBJECT_MATCHERS`. */
|
|
16
16
|
export interface PolicySubject {
|
|
17
|
-
/** `role`,
|
|
17
|
+
/** `role`, `job` for a scheduled job acting as itself (#1999), or `authenticated` / `anyone` (#2261). */
|
|
18
18
|
type: string;
|
|
19
|
-
|
|
19
|
+
/** The role or the job id. `authenticated` and `anyone` take none. */
|
|
20
|
+
value?: string;
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
23
|
* What a policy covers.
|
|
@@ -28,6 +29,45 @@ export interface PolicyResource {
|
|
|
28
29
|
type: string;
|
|
29
30
|
pattern: string;
|
|
30
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* A comparison of one attribute with a value (#2255, #2261). A string starting
|
|
34
|
+
* `subject:`, `resource:` or `environment:` names an attribute; anything else
|
|
35
|
+
* is a literal. Exactly one operator key beside `attribute`:
|
|
36
|
+
* `equals`, `not-equals`, `in`, `greater-or-equal`, `less-or-equal`.
|
|
37
|
+
*/
|
|
38
|
+
export type LeafCondition = {
|
|
39
|
+
attribute: string;
|
|
40
|
+
} & ({
|
|
41
|
+
equals: unknown;
|
|
42
|
+
} | {
|
|
43
|
+
'not-equals': unknown;
|
|
44
|
+
} | {
|
|
45
|
+
in: unknown[] | string;
|
|
46
|
+
} | {
|
|
47
|
+
'greater-or-equal': number | string;
|
|
48
|
+
} | {
|
|
49
|
+
'less-or-equal': number | string;
|
|
50
|
+
});
|
|
51
|
+
/**
|
|
52
|
+
* A policy's condition: a {@link LeafCondition}, or `any-of` / `all-of` /
|
|
53
|
+
* `not` over other conditions, nested at most 4 deep.
|
|
54
|
+
*
|
|
55
|
+
* Each node is true, false or unknown. A leaf reading an attribute that is
|
|
56
|
+
* missing is unknown; in `any-of` a true branch decides, in `all-of` a false
|
|
57
|
+
* one does, and `not` of unknown is unknown. An allow applies only when the
|
|
58
|
+
* condition is true; a deny applies when it is true or unknown.
|
|
59
|
+
*/
|
|
60
|
+
export type PolicyCondition = LeafCondition | {
|
|
61
|
+
'any-of': PolicyCondition[];
|
|
62
|
+
} | {
|
|
63
|
+
'all-of': PolicyCondition[];
|
|
64
|
+
} | {
|
|
65
|
+
not: PolicyCondition;
|
|
66
|
+
};
|
|
67
|
+
/** The prefixes that mark a condition value as an attribute rather than a literal (#2255). */
|
|
68
|
+
export declare const ATTRIBUTE_CATEGORIES: readonly ["subject", "resource", "environment"];
|
|
69
|
+
/** True when `value` names an attribute (`subject:id`, `resource:owner`), not a literal. */
|
|
70
|
+
export declare function isAttributeName(value: unknown): value is string;
|
|
31
71
|
/** One entry of `ngdpbase.access.policies`. */
|
|
32
72
|
export interface Policy {
|
|
33
73
|
id: string;
|
|
@@ -44,6 +84,8 @@ export interface Policy {
|
|
|
44
84
|
subjects?: PolicySubject[];
|
|
45
85
|
resources?: PolicyResource[];
|
|
46
86
|
actions?: string[];
|
|
87
|
+
/** Applies only when this holds (#2255); see {@link PolicyCondition}. */
|
|
88
|
+
condition?: PolicyCondition;
|
|
47
89
|
[key: string]: unknown;
|
|
48
90
|
}
|
|
49
91
|
/**
|
|
@@ -88,11 +130,18 @@ export interface DecisionRequest {
|
|
|
88
130
|
action: string;
|
|
89
131
|
resource?: DecisionResource;
|
|
90
132
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* `system-category
|
|
133
|
+
* Where a page or file sits, for the resource types that match on it: the
|
|
134
|
+
* `vault` (#1539) or the `shared-folder` (#2125). A page's own attributes —
|
|
135
|
+
* its `system-category`, its `owner` — are never passed: the PDP asks the
|
|
136
|
+
* PIP for them from the page id (#2255).
|
|
94
137
|
*/
|
|
95
138
|
attributes?: Record<string, unknown>;
|
|
139
|
+
/**
|
|
140
|
+
* The page's frontmatter when the PIP itself already holds it (the page
|
|
141
|
+
* door, the list filter), so the PDP's lookup reads no page twice. Only the
|
|
142
|
+
* PIP sets it; a route never does (#2255).
|
|
143
|
+
*/
|
|
144
|
+
pageMetadata?: Record<string, unknown> | null;
|
|
96
145
|
/**
|
|
97
146
|
* Whether a share's `resources` cover the thing being asked about.
|
|
98
147
|
*
|
|
@@ -128,6 +177,18 @@ export interface Decision {
|
|
|
128
177
|
*/
|
|
129
178
|
applicable: boolean;
|
|
130
179
|
reason: string;
|
|
180
|
+
/**
|
|
181
|
+
* The ids of the policies that decided (#2255): the denies that matched,
|
|
182
|
+
* or, when none did, the allows that matched. Empty when no policy matched
|
|
183
|
+
* or something other than policy decided (a ceiling, step-up).
|
|
184
|
+
*/
|
|
185
|
+
policyIds: string[];
|
|
186
|
+
/**
|
|
187
|
+
* The ids of allow policies that matched subject, resource and action but
|
|
188
|
+
* whose condition was false or unknown (#2288), so a refusal can say which
|
|
189
|
+
* allow was not met. Ids only, never attribute values. Absent when none.
|
|
190
|
+
*/
|
|
191
|
+
conditionNotMet?: string[];
|
|
131
192
|
}
|
|
132
193
|
/**
|
|
133
194
|
* The reason a deny carries when the subject holds the permission but its
|
package/lib/src/types/Policy.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
/** The prefixes that mark a condition value as an attribute rather than a literal (#2255). */
|
|
3
|
+
export const ATTRIBUTE_CATEGORIES = ['subject', 'resource', 'environment'];
|
|
4
|
+
/** True when `value` names an attribute (`subject:id`, `resource:owner`), not a literal. */
|
|
5
|
+
export function isAttributeName(value) {
|
|
6
|
+
return typeof value === 'string' && ATTRIBUTE_CATEGORIES.some((c) => value.startsWith(`${c}:`));
|
|
7
|
+
}
|
|
2
8
|
/**
|
|
3
9
|
* The reason a deny carries when the subject holds the permission but its
|
|
4
10
|
* sign-in is not fresh enough to act (#1635). Declared once: the PDP writes it
|
|
@@ -319,6 +319,18 @@ export interface PageProvider extends BaseProvider {
|
|
|
319
319
|
reason: string;
|
|
320
320
|
detail?: string;
|
|
321
321
|
}>;
|
|
322
|
+
/**
|
|
323
|
+
* Where a trashed page would come back to, and its frontmatter, so the
|
|
324
|
+
* write door can decide the restore before it happens (#1815, security
|
|
325
|
+
* review L5): its title, or its vault name when it was deleted from a
|
|
326
|
+
* vault. Null when the uuid is not in the trash.
|
|
327
|
+
*
|
|
328
|
+
* @param uuid - Page UUID
|
|
329
|
+
*/
|
|
330
|
+
deletedPageForRestore?(uuid: string): Promise<{
|
|
331
|
+
name: string;
|
|
332
|
+
metadata: PageFrontmatter;
|
|
333
|
+
} | null>;
|
|
322
334
|
/**
|
|
323
335
|
* Get all page titles
|
|
324
336
|
* @returns Sorted array of page titles
|
package/lib/src/types/User.d.ts
CHANGED
|
@@ -56,6 +56,12 @@ export interface UserPreferences {
|
|
|
56
56
|
/** Additional custom preferences */
|
|
57
57
|
[key: string]: unknown;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The account fields whose change is recorded on `user-edit` with the field name
|
|
61
|
+
* only, and that a policy condition may never read as `account:` or `profile:`
|
|
62
|
+
* (#2261): the password, the email, the roles and the like.
|
|
63
|
+
*/
|
|
64
|
+
export declare const SENSITIVE_USER_FIELDS: readonly ["password", "roles", "isActive", "isExternal", "isTest", "email", "profileLocked", "username"];
|
|
59
65
|
/**
|
|
60
66
|
* User object
|
|
61
67
|
*
|
|
@@ -55,15 +55,3 @@ export interface SyncRowState {
|
|
|
55
55
|
* @param userModifiedFlag - The live copy is explicitly marked edited
|
|
56
56
|
*/
|
|
57
57
|
export declare function syncRowState(seedStatus: SeededAddonPageStatus, userModifiedFlag?: boolean): SyncRowState;
|
|
58
|
-
/**
|
|
59
|
-
* Default edit access for a page ngdpbase ships (#971, #1411): a `general`
|
|
60
|
-
* page is instance-owned and gets no stamp; any other category — system,
|
|
61
|
-
* documentation, addon, unclassified — is administrator-edit only. A source
|
|
62
|
-
* that declares its own `access` overrides this.
|
|
63
|
-
*
|
|
64
|
-
* @param category - The page's resolved `system-category`
|
|
65
|
-
* @returns The access object to stamp, or undefined to leave the page unprotected
|
|
66
|
-
*/
|
|
67
|
-
export declare function defaultShippedPageAccess(category: unknown): {
|
|
68
|
-
edit: string[];
|
|
69
|
-
} | undefined;
|
|
@@ -224,6 +224,8 @@ export declare const AUDIT_EVENT: {
|
|
|
224
224
|
readonly USER_EDIT: "user-edit";
|
|
225
225
|
/** User profile read; not recorded, read volume */
|
|
226
226
|
readonly USER_READ: "user-read";
|
|
227
|
+
/** An administrator read or changed a page inside another person's vault that is not encrypted (ngdpbase.vaults.admin-access, #2255): the administrator is the actor, and the vault's owner, the vault and the page are named */
|
|
228
|
+
readonly VAULT_ADMIN_ACCESS: "vault-admin-access";
|
|
227
229
|
/** Not an action: read from a proxy grant at the vault door; the grant itself is recorded as proxy-grant */
|
|
228
230
|
readonly VAULT_PROXY: "vault-proxy";
|
|
229
231
|
/** A vault page's leftovers moved into its vault (#1842): its shared-pool files (moved, or copied where other public pages use them) and its footnotes and comments from the public folders. Names the owner, vault and page uuid and the counts, never a title or file name. Run by the vault's owner, or for a vault that isn't sealed by someone holding admin-system */
|
|
@@ -231,6 +231,8 @@ export const AUDIT_EVENT = {
|
|
|
231
231
|
USER_EDIT: 'user-edit',
|
|
232
232
|
/** User profile read; not recorded, read volume */
|
|
233
233
|
USER_READ: 'user-read',
|
|
234
|
+
/** An administrator read or changed a page inside another person's vault that is not encrypted (ngdpbase.vaults.admin-access, #2255): the administrator is the actor, and the vault's owner, the vault and the page are named */
|
|
235
|
+
VAULT_ADMIN_ACCESS: 'vault-admin-access',
|
|
234
236
|
/** Not an action: read from a proxy grant at the vault door; the grant itself is recorded as proxy-grant */
|
|
235
237
|
VAULT_PROXY: 'vault-proxy',
|
|
236
238
|
/** A vault page's leftovers moved into its vault (#1842): its shared-pool files (moved, or copied where other public pages use them) and its footnotes and comments from the public folders. Names the owner, vault and page uuid and the counts, never a title or file name. Run by the vault's owner, or for a vault that isn't sealed by someone holding admin-system */
|
|
@@ -142,6 +142,8 @@ export interface TokenInput extends CommonInput {
|
|
|
142
142
|
previousExpiresAt?: string;
|
|
143
143
|
/** Only meaningful for `revoke`; ignored otherwise. */
|
|
144
144
|
revokedBy?: string;
|
|
145
|
+
/** Only meaningful for `mint` (#2287): the sign-in level stored with the token; absent when it stored none. */
|
|
146
|
+
aal?: 1 | 2 | 3;
|
|
145
147
|
}
|
|
146
148
|
/**
|
|
147
149
|
* What an attachment record says about the file. A file in an ENCRYPTED store
|
|
@@ -289,7 +289,7 @@ export async function recordAuditEvent(sink, event, onError) {
|
|
|
289
289
|
* every event is useless as a filter.
|
|
290
290
|
*/
|
|
291
291
|
export function buildTokenAuditEvent(input) {
|
|
292
|
-
const { op, id, owner, ownerUser, name, scopes, expiresAt, previousExpiresAt, revokedBy, viaToken } = input;
|
|
292
|
+
const { op, id, owner, ownerUser, name, scopes, expiresAt, previousExpiresAt, revokedBy, viaToken, aal } = input;
|
|
293
293
|
const who = whoOf(input);
|
|
294
294
|
const metadata = {
|
|
295
295
|
id,
|
|
@@ -302,6 +302,8 @@ export function buildTokenAuditEvent(input) {
|
|
|
302
302
|
if (op === 'mint') {
|
|
303
303
|
metadata.scopes = [...(scopes ?? [])];
|
|
304
304
|
metadata.expiresAt = expiresAt;
|
|
305
|
+
// #2287: the level the token holds, or null when it stored none; the record that answers "was this minted after a second factor?"
|
|
306
|
+
metadata.aal = aal ?? null;
|
|
305
307
|
}
|
|
306
308
|
if (op === 'extend') {
|
|
307
309
|
metadata.previousExpiresAt = previousExpiresAt;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* frontmatterAccess — the single implementation of ACL Tier 1 (#1054).
|
|
3
3
|
*
|
|
4
|
-
* A page's frontmatter can restrict who may act on it
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* A page's frontmatter can restrict who may act on it with `audience`, which
|
|
5
|
+
* covers every action (#1964). The `access[<action>]` lists it once honoured
|
|
6
|
+
* were removed in #2255: what a user may do on a page is decided by its
|
|
7
|
+
* system-category and policy. Two subsystems needed the audience answer and
|
|
8
|
+
* each had its own version of it, which is how they came to disagree:
|
|
7
9
|
*
|
|
8
10
|
* - `PolicyInformationPoint.checkFrontmatterAccess` decided a request, correctly.
|
|
9
11
|
* - `VersioningFileProvider.getRecentChanges` decided a listing, and only
|
|
@@ -20,8 +22,6 @@
|
|
|
20
22
|
*/
|
|
21
23
|
/** The frontmatter fields Tier 1 reads. */
|
|
22
24
|
export interface AccessMetadataLike {
|
|
23
|
-
/** `{ view: ['role'], edit: [...] }` — per-action principal lists. */
|
|
24
|
-
access?: unknown;
|
|
25
25
|
/** Who may act on the page at all; administrators excepted (#1964). */
|
|
26
26
|
audience?: unknown;
|
|
27
27
|
}
|
|
@@ -39,11 +39,6 @@ export interface FrontmatterDecision {
|
|
|
39
39
|
* neither read nor edit the page.
|
|
40
40
|
*/
|
|
41
41
|
export declare function audiencePrincipals(metadata: AccessMetadataLike | null | undefined): string[] | null;
|
|
42
|
-
/**
|
|
43
|
-
* The page's `access[action]` list, or null when it states none. It applies
|
|
44
|
-
* to that action only, and to everyone — administrators included.
|
|
45
|
-
*/
|
|
46
|
-
export declare function accessPrincipals(metadata: AccessMetadataLike | null | undefined, action: string): string[] | null;
|
|
47
42
|
/**
|
|
48
43
|
* The entry `addon:<slug>` (#1964): "people who turned on the per-person
|
|
49
44
|
* add-on <slug>". It is matched only against the viewer's enabled add-ons,
|
|
@@ -54,11 +49,10 @@ export declare const ADDON_PRINCIPAL_PREFIX = "addon:";
|
|
|
54
49
|
* Whether deciding `action` on this page needs the viewer's enabled add-ons —
|
|
55
50
|
* so a caller asks for them only when the page names one (#1964).
|
|
56
51
|
*/
|
|
57
|
-
export declare function namesAddonPrincipal(metadata: AccessMetadataLike | null | undefined,
|
|
52
|
+
export declare function namesAddonPrincipal(metadata: AccessMetadataLike | null | undefined, _action: string): boolean;
|
|
58
53
|
/**
|
|
59
|
-
* Every add-on slug the page names in `audience
|
|
60
|
-
*
|
|
61
|
-
* nobody (#1964).
|
|
54
|
+
* Every add-on slug the page names in `audience`, once each, in the order
|
|
55
|
+
* first named — for the warning about a slug that can match nobody (#1964).
|
|
62
56
|
*/
|
|
63
57
|
export declare function addonAudienceSlugs(metadata: AccessMetadataLike | null | undefined): string[];
|
|
64
58
|
/**
|
|
@@ -75,16 +69,13 @@ export declare const AUDIENCE_ADMIN_MATCH = "admin-system";
|
|
|
75
69
|
* plus their username. Callers holding a `UserContext` should pass
|
|
76
70
|
* `[...roles, username]`; the provider layer already works in these terms.
|
|
77
71
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* only lets them through to `access` and then policy.
|
|
86
|
-
* - __`access[action]`__ applies to that action, to everyone, administrators
|
|
87
|
-
* included. When the page states it, it decides.
|
|
72
|
+
* `audience` applies to every action (#1964, operator 2026-10-23). Someone it
|
|
73
|
+
* leaves out may neither read nor edit the page: every action is refused. The
|
|
74
|
+
* one exception is an administrator — a viewer holding `admin-system`, which
|
|
75
|
+
* the caller decides and passes as `isAdmin` (this module never reads a role
|
|
76
|
+
* for it). For `view`, a viewer the audience admits is allowed; for any other
|
|
77
|
+
* action it only lets them through to policy. A page's `access` field is not
|
|
78
|
+
* read (#2255).
|
|
88
79
|
*
|
|
89
80
|
* An `addon:<slug>` entry matches only when `<slug>` is in `enabledAddons`,
|
|
90
81
|
* the viewer's enabled per-person add-ons (#1964); a caller that does not
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** One leftover key and what to tell the operator about it. */
|
|
2
|
+
export interface LegacyStoreKey {
|
|
3
|
+
key: string;
|
|
4
|
+
kind: string;
|
|
5
|
+
setting: 'owner' | 'encrypt';
|
|
6
|
+
/** One sentence: the key, that it has no effect, and what replaces it. */
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Every legacy per-kind key in the operator's custom config, in file order.
|
|
11
|
+
* `categories` is the merged `ngdpbase.system-category` map, used to name the
|
|
12
|
+
* entry that declares the kind's vault (the `default` vault is `general`'s).
|
|
13
|
+
*/
|
|
14
|
+
export declare function legacyStoreKeys(customConfig: Record<string, unknown> | null | undefined, categories: unknown): LegacyStoreKey[];
|
|
15
|
+
/** The needs-attention reason for a non-empty list: every key, in one sentence. */
|
|
16
|
+
export declare function legacyStoreKeysReason(found: LegacyStoreKey[]): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page-write policies after #2255, and the migration of a site's stored
|
|
3
|
+
* copies of the old ones.
|
|
4
|
+
*
|
|
5
|
+
* Until #2255 the shipped role policies granted `page-edit`, `page-delete` and
|
|
6
|
+
* `page-rename` on every page (`page: *`), and an `access: { edit: [admin] }`
|
|
7
|
+
* stamp on each shipped page was the only thing keeping editors out of the
|
|
8
|
+
* system and documentation pages. Those grants now sit on the
|
|
9
|
+
* `system-category` resource type, one policy per group of categories, and the
|
|
10
|
+
* stamp is gone.
|
|
11
|
+
*
|
|
12
|
+
* A site that ever saved `ngdpbase.access.policies` (the configuration editor,
|
|
13
|
+
* the Shared folders screen) holds a full copy of the old role policies, and a
|
|
14
|
+
* stored entry replaces the shipped one with the same id whole
|
|
15
|
+
* (`src/utils/configFiles.ts`). Its `page-edit` on `page: *` would reopen every
|
|
16
|
+
* pushed page to editors. So, before the merge, in memory:
|
|
17
|
+
*
|
|
18
|
+
* - a stored entry with a shipped id below loses the page writes it held on
|
|
19
|
+
* `page: *`; any it held that the shipped category policies do not give that
|
|
20
|
+
* role is kept, on the general category only, as a new policy
|
|
21
|
+
* `<id>-page-write`;
|
|
22
|
+
* - every other policy that grants a page write on a `page` resource to a role
|
|
23
|
+
* other than admin, and every deny policy (a deny now overrides an allow
|
|
24
|
+
* whatever its priority), is reported and left as it is: it is the site's
|
|
25
|
+
* own choice.
|
|
26
|
+
*
|
|
27
|
+
* Pure: the caller logs and reports. The site's file is not rewritten; saving
|
|
28
|
+
* the policy list from the configuration editor writes the migrated list.
|
|
29
|
+
*/
|
|
30
|
+
import type { Policy } from '../types/Policy.js';
|
|
31
|
+
/** What the migration did and found, one sentence each. */
|
|
32
|
+
export interface PageWritePolicyReport {
|
|
33
|
+
/** Stored shipped policies rewritten in memory. */
|
|
34
|
+
rewritten: string[];
|
|
35
|
+
/** Policies left alone that now decide differently, or reopen pushed pages. */
|
|
36
|
+
review: string[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Rewrite the stored copies of the moved shipped policies, in `policies` (the
|
|
40
|
+
* custom layer's `ngdpbase.access.policies`), and return what was done and
|
|
41
|
+
* what an administrator should review. Returns the list to store in place of
|
|
42
|
+
* the input; the input is not changed.
|
|
43
|
+
*/
|
|
44
|
+
export declare function migrateStoredPageWritePolicies(policies: unknown): {
|
|
45
|
+
policies: unknown;
|
|
46
|
+
report: PageWritePolicyReport;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Policies in force that an administrator should review after #2255: an
|
|
50
|
+
* allow that grants a page write on a `page` resource to a role other than
|
|
51
|
+
* admin (it reaches pushed pages), an allow that grants anything but
|
|
52
|
+
* page-read to `anyone` or `authenticated` (#2261, #2283), and every deny (a deny now overrides any
|
|
53
|
+
* allow, whatever the priorities).
|
|
54
|
+
*/
|
|
55
|
+
export declare function pageWritePoliciesToReview(policies: readonly Policy[]): string[];
|
|
@@ -25,6 +25,8 @@ export declare function grantablePermissions<T>(definitions: Record<string, T> |
|
|
|
25
25
|
export declare const RETIRED_PERMISSIONS: ReadonlyMap<string, readonly string[]>;
|
|
26
26
|
/** The names with every retired permission replaced by what it stood for, de-duplicated, order kept. */
|
|
27
27
|
export declare function withRetiredExpanded(names: readonly string[]): string[];
|
|
28
|
+
/** The names a policy may grant `action` under (#2255): a page write and its pushed-pages twin, else the action alone. */
|
|
29
|
+
export declare function pageWriteAlternatives(action: string): string[];
|
|
28
30
|
/**
|
|
29
31
|
* What the standard OpenID Connect sign-in scopes let an app see, in plain
|
|
30
32
|
* words (#1572). They are not permissions, so they are not in the catalog;
|