@jwilleke/ngdpbase-types 5.43.0 → 5.44.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.
Files changed (45) hide show
  1. package/lib/src/context/ApiContext.d.ts +10 -3
  2. package/lib/src/context/ApiContext.js +36 -3
  3. package/lib/src/ice/runExport.d.ts +8 -3
  4. package/lib/src/ice/searchExportSource.d.ts +44 -0
  5. package/lib/src/ice/types.d.ts +6 -0
  6. package/lib/src/managers/AttachmentManager.d.ts +11 -0
  7. package/lib/src/managers/AuditManager.d.ts +0 -1
  8. package/lib/src/managers/AuthManager.d.ts +38 -0
  9. package/lib/src/managers/AuthManager.js +84 -12
  10. package/lib/src/managers/BackupManager.d.ts +9 -2
  11. package/lib/src/managers/BackupManager.js +42 -19
  12. package/lib/src/managers/BaseManager.d.ts +17 -3
  13. package/lib/src/managers/BaseManager.js +12 -8
  14. package/lib/src/managers/ConfigurationManager.d.ts +13 -0
  15. package/lib/src/managers/ConsentManager.d.ts +1 -1
  16. package/lib/src/managers/DatabaseManager.js +17 -8
  17. package/lib/src/managers/EmailManager.d.ts +1 -1
  18. package/lib/src/managers/ExportManager.d.ts +1 -44
  19. package/lib/src/managers/ExportManager.js +3 -115
  20. package/lib/src/managers/ImportManager.d.ts +7 -0
  21. package/lib/src/managers/PageManager.d.ts +9 -0
  22. package/lib/src/managers/PolicyEvaluator.d.ts +113 -30
  23. package/lib/src/managers/ThemeManager.d.ts +20 -1
  24. package/lib/src/providers/FileLoggingProvider.d.ts +12 -1
  25. package/lib/src/providers/FileLoggingProvider.js +37 -4
  26. package/lib/src/security/PolicyDecisionPoint.d.ts +50 -3
  27. package/lib/src/security/PolicyInformationPoint.d.ts +27 -1
  28. package/lib/src/security/ownedRecords.d.ts +69 -0
  29. package/lib/src/security/signedOutWrites.d.ts +69 -0
  30. package/lib/src/security/signedOutWrites.js +146 -0
  31. package/lib/src/types/Policy.d.ts +26 -30
  32. package/lib/src/utils/addonConfigLayer.d.ts +6 -3
  33. package/lib/src/utils/addonConfigLayer.js +61 -3
  34. package/lib/src/utils/addonHost.d.ts +25 -0
  35. package/lib/src/utils/addonHost.js +29 -1
  36. package/lib/src/utils/availability.d.ts +15 -0
  37. package/lib/src/utils/availability.js +88 -0
  38. package/lib/src/utils/pageTitleConflict.d.ts +32 -0
  39. package/lib/src/utils/pageWritePolicies.d.ts +48 -29
  40. package/lib/src/utils/plainAttention.d.ts +19 -0
  41. package/lib/src/utils/plainAttention.js +18 -0
  42. package/lib/src/utils/policyAttributes.d.ts +113 -22
  43. package/lib/src/utils/policyAttributes.js +228 -43
  44. package/lib/src/utils/roleGrants.d.ts +1 -1
  45. package/package.json +7 -7
@@ -1,7 +1,8 @@
1
1
  import BaseManager from './BaseManager.js';
2
2
  import type { Policy, PolicySubject, PolicyResource, PolicyCondition } from '../types/Policy.js';
3
- import { type DeclaredSubjectAttribute } from '../utils/policyAttributes.js';
3
+ import { type DeclaredEnvironmentAttribute, type DeclaredSubjectAttribute } from '../utils/policyAttributes.js';
4
4
  import { WikiEngine } from '../types/WikiEngine.js';
5
+ import type { ActorContext } from '../context/ActorContext.js';
5
6
  /**
6
7
  * User context for policy evaluation
7
8
  */
@@ -43,28 +44,34 @@ interface AccessContext {
43
44
  readAttribute?: AttributeReader;
44
45
  }
45
46
  /**
46
- * What is wrong with a policy's condition, one sentence each (#2255, #2261);
47
- * empty when it is fine. Each sentence says what is wrong, then what to
48
- * change. A node is a comparison (`attribute` and one operator) or `any-of`,
49
- * `all-of` or `not` over other conditions, nested at most
50
- * {@link MAX_CONDITION_DEPTH} deep, at most {@link MAX_CONDITION_NODES} nodes
51
- * in all, an `in` list of at most {@link MAX_IN_ITEMS} and an `any-of` or
52
- * `all-of` of at most {@link MAX_GROUP_CHILDREN}. Each attribute a comparison names must be
53
- * declared: a `subject:<name>` in `ngdpbase.access.subject-attributes`
54
- * (`id` always, `aal` as shipped), or a `resource:<name>` declared on the `page`
55
- * resource type. The types of the two sides must agree. A condition with a
56
- * problem never holds: an allow with it does not apply and a deny with it
57
- * does, as for a missing attribute.
47
+ * What is wrong with a policy's condition, one sentence each (#2255, #2261,
48
+ * #2330); empty when it is fine. Each sentence says what is wrong, then what to
49
+ * change. A node is one XACML function in lowercase: a comparison such as
50
+ * `string-equal` over two arguments, or `and`, `or` or `not` over other
51
+ * conditions, nested at most {@link MAX_CONDITION_DEPTH} deep, at most
52
+ * {@link MAX_CONDITION_NODES} function nodes in all, an `is-in` list of at most
53
+ * {@link MAX_IN_ITEMS} and an `and` or `or` of at most {@link MAX_GROUP_CHILDREN}.
54
+ * An argument is an attribute (`attributeid`, `datatype`) or a literal (`value`,
55
+ * `datatype`); `datatype` is required, must be the type the attribute is declared with
56
+ * and the type the function takes. Each attribute must be declared: a
57
+ * `subject:<name>` in `ngdpbase.access.subject-attributes` (`id` always, `aal`
58
+ * as shipped), or a `resource:<name>` declared on the resource type the policy names (the
59
+ * `page` type when it names a page, or that add-on's type when it names only
60
+ * that type, #1823), or an `environment:<name>` in `ngdpbase.access.environment-attributes`. The old
61
+ * shape (`attribute` with `equals` and the like) is a problem that shows the
62
+ * new one. A condition with a problem never holds: an allow with it does not
63
+ * apply and a deny with it does, as for a missing attribute.
58
64
  */
59
65
  export declare function conditionProblems(condition: unknown, pageAttributes: Record<string, {
60
66
  type?: string;
61
- }>, subjectAttributes?: Record<string, DeclaredSubjectAttribute>): string[];
67
+ }>, subjectAttributes?: Record<string, DeclaredSubjectAttribute>, environmentAttributes?: Record<string, DeclaredEnvironmentAttribute>, resourceTypeLabel?: string): string[];
62
68
  /**
63
69
  * Is the condition true, false or unknown (#2255, #2261)? `true` or `false`,
64
70
  * or `undefined` when it cannot be told because an attribute it reads is
65
- * missing; the caller fails closed on that. Each node is judged on its own:
66
- * a missing attribute makes its leaf unknown, a true branch decides `any-of`,
67
- * a false branch decides `all-of`, and `not` of unknown is unknown.
71
+ * missing, or an argument lacks the datatype its function takes; the caller
72
+ * fails closed on that. Each node is judged on its own: a missing attribute
73
+ * makes its comparison unknown, a true branch decides `or`, a false branch
74
+ * decides `and`, and `not` of unknown is unknown.
68
75
  */
69
76
  export declare function conditionHolds(condition: PolicyCondition | undefined, read: AttributeReader | undefined, depth?: number): Promise<boolean | undefined>;
70
77
  /**
@@ -87,6 +94,25 @@ export declare const SUBJECT_MATCHERS: Record<string, (value: string | undefined
87
94
  * on one is refused rather than ignored.
88
95
  */
89
96
  export declare const VALUELESS_SUBJECT_TYPES: readonly string[];
97
+ /**
98
+ * Which resource attributes a policy's condition may read (#1823).
99
+ *
100
+ * A policy that names exactly one add-on type (no page matcher) uses that
101
+ * type's attributes. A policy on a core type, including `system-category`,
102
+ * still reads the `page` attributes: those conditions are about the page.
103
+ */
104
+ export declare function conditionAttributeSource(resources: readonly {
105
+ type?: string;
106
+ }[] | undefined, types: Record<string, {
107
+ attributes?: Record<string, {
108
+ type?: string;
109
+ }>;
110
+ }> | null): {
111
+ attributes: Record<string, {
112
+ type?: string;
113
+ }>;
114
+ label: string;
115
+ };
90
116
  /**
91
117
  * The resource types configuration declares (#1504), for the evaluator and the
92
118
  * validator alike. When the key is not set at all — a configuration older than
@@ -112,8 +138,8 @@ interface EvaluationResult {
112
138
  *
113
139
  * PolicyEvaluator mimics how JSPWiki uses Java's built-in security framework
114
140
  * (java.security) to load and evaluate security policies from a policy file.
115
- * Among the policies that match, a deny overrides any allow whatever its
116
- * priority, and no match is a deny (#2255). Each decision names the policies
141
+ * Among the policies that match, a deny overrides any allow, the
142
+ * rule-combining algorithm `deny-overrides` (#2330), and no match is a deny (#2255). Each decision names the policies
117
143
  * that decided it.
118
144
  *
119
145
  * @class PolicyEvaluator
@@ -136,6 +162,17 @@ interface EvaluationResult {
136
162
  */
137
163
  declare class PolicyEvaluator extends BaseManager {
138
164
  private configManager;
165
+ /** The headline of the access-policies item now up, so marking the last deny takes down this one and no other (#2355). */
166
+ private reportedReason;
167
+ /**
168
+ * Each condition checked and compiled, by the condition object configuration
169
+ * holds (#2279), kept while `ConfigurationManager.getRevision()` is
170
+ * `revision`. Any change to the configuration (a save, a reload, a reset, a
171
+ * restore, an add-on's runtime value) changes the revision, and the next
172
+ * decision starts again from nothing. The policies themselves are still read
173
+ * live for every decision; only a condition's compiled form is reused.
174
+ */
175
+ private preparedConditions;
139
176
  /**
140
177
  * The policies in force NOW. Only the PDP interprets them (#1431 step 14b);
141
178
  * this evaluator is the PDP's matching engine and asks it for the list, read
@@ -163,29 +200,67 @@ declare class PolicyEvaluator extends BaseManager {
163
200
  */
164
201
  initialize(): Promise<void>;
165
202
  /**
166
- * Say at boot what an administrator should look at in the policies after
167
- * #2255, one warning each, and one needs-attention entry for them all:
168
- * stored policies the upgrade rewrote, policies it left that now decide
169
- * differently (a deny) or reach pushed pages, conditions that never hold,
170
- * and stored categories no policy lets anyone change (their pages are
171
- * changed by nobody, admin included).
203
+ * Say at boot what a developer should look at in the policies after #2255,
204
+ * one warning each: stored policies the upgrade rewrote, policies it left
205
+ * that now decide differently (a deny) or reach pushed pages, and conditions
206
+ * that never hold. These are log lines only; categories no policy lets anyone
207
+ * change are an upgrade check (`upgradeChecks/uneditablePages.ts`).
172
208
  */
173
209
  private reportPageWritePolicies;
210
+ /** The stored marks whose rule is still a deny in force (#2355). A mark for a rule since removed is not kept: the same rule added later must be shown again. */
211
+ private marksOfExistingDenies;
212
+ /** Save the marks without those of rules that are gone (#2355); nothing is written when none is. */
213
+ private dropMarksOfGoneRules;
214
+ /**
215
+ * An administrator marks a deny as checked (#2355). Kept as the rule's id and a fingerprint of
216
+ * its content in `ngdpbase.access.checked-denies`, written through ConfigurationManager with the
217
+ * person's context, which records the change. It hides the item and nothing else: no decision
218
+ * reads it. Answers `changed` when the rule is no longer what the page showed (nothing is
219
+ * saved) and `missing` when no deny has that id.
220
+ */
221
+ markDenyChecked(ctx: ActorContext, id: string, fingerprint: string): Promise<'checked' | 'changed' | 'missing'>;
174
222
  /**
175
223
  * Evaluates all relevant policies to make an access decision (#2255).
176
224
  *
177
- * Deny overrides allow: if any matching deny applies, the answer is deny,
178
- * whatever the priorities; otherwise any matching allow permits; otherwise
179
- * no policy decided. A policy with a `condition` applies only when it holds;
225
+ * Deny overrides allow (the rule-combining algorithm, #2330): if any matching
226
+ * deny applies, the answer is deny; otherwise any matching allow permits;
227
+ * otherwise no policy decided. A policy with a `condition` applies only when it holds;
180
228
  * an unknown condition (a missing attribute) keeps an allow from applying
181
229
  * and lets a deny apply, so a missing attribute fails closed.
182
230
  */
183
231
  evaluateAccess(context: AccessContext): Promise<EvaluationResult>;
184
232
  /**
185
- * Checks and compiles each policy's condition (#2261). `compile()` does it
186
- * once for a whole listing; a single decision does it for its few matches.
233
+ * The policies with their conditions checked and compiled (#2279): a
234
+ * condition already compiled under the configuration in force is reused,
235
+ * and `prepare` runs only for the rest. With no configuration revision to
236
+ * key on, everything is prepared again.
237
+ */
238
+ private prepared;
239
+ /**
240
+ * Checks and compiles each policy's condition (#2261). `prepared()` keeps
241
+ * the result until the configuration changes (#2279).
187
242
  */
188
243
  private prepare;
244
+ /**
245
+ * Decide one add-on record type by id (#1823).
246
+ *
247
+ * Only policies that name this type are considered, and a policy with no
248
+ * resources does not match: a page grant cannot cover the type, and a
249
+ * blanket policy cannot either. The id is matched with the same globs as a
250
+ * page name, a leading dot included. A core type (`page`, `system-category`, `vault`,
251
+ * `shared-folder`) is not decided here.
252
+ */
253
+ evaluateDeclaredResource(context: {
254
+ resourceType: string;
255
+ id: string;
256
+ action: string;
257
+ actions?: readonly string[];
258
+ userContext?: UserContext;
259
+ readAttribute?: AttributeReader;
260
+ }): Promise<EvaluationResult>;
261
+ /** True when a policy resource names this type and its pattern covers the id. No resources is not a match. */
262
+ private namesDeclaredType;
263
+ private resourceTypeMap;
189
264
  /**
190
265
  * Deny overrides allow, over policies that already match subject, resource
191
266
  * and action (#2255). Every matching deny whose condition is true or
@@ -264,6 +339,14 @@ declare class PolicyEvaluator extends BaseManager {
264
339
  }>;
265
340
  /** The subject attributes a condition may read (#2261): `id` always, and those declared in `ngdpbase.access.subject-attributes`. */
266
341
  declaredSubjectAttributes(): Record<string, DeclaredSubjectAttribute>;
342
+ /** The environment attributes a condition may read (#2261): those declared in `ngdpbase.access.environment-attributes`. */
343
+ declaredEnvironmentAttributes(): Record<string, DeclaredEnvironmentAttribute>;
344
+ /**
345
+ * One line for each declared `schedule:` attribute whose schedule is not in
346
+ * `ngdpbase.schedules` (the attribute is missing and a condition on it fails
347
+ * closed) or has no rules (it reads as always open), for the boot warning.
348
+ */
349
+ private scheduleProblems;
267
350
  /** Each policy in force whose condition has a problem, with the problems (#2255): for the boot warning. */
268
351
  conditionProblemsInForce(): Array<{
269
352
  id: string;
@@ -5,6 +5,12 @@ export interface ThemeInfo {
5
5
  author: string;
6
6
  /** Optional Google Fonts (or other) stylesheet URLs to inject in <head> */
7
7
  fonts?: string[];
8
+ /**
9
+ * Optional CSS filenames in this theme's css/ directory. Loaded after
10
+ * core.css so a site theme can adjust layout without editing the shared
11
+ * sheet. Basenames only; paths are rejected.
12
+ */
13
+ stylesheets?: string[];
8
14
  }
9
15
  export interface ThemePaths {
10
16
  /** Active theme name (folder name under themes/) */
@@ -23,6 +29,8 @@ export interface ThemePaths {
23
29
  themeInfo: ThemeInfo | null;
24
30
  /** Optional font stylesheet URLs from theme.json fonts[] */
25
31
  fontUrls: string[];
32
+ /** Theme CSS files to load after core.css. Public URLs. */
33
+ themeStylesheets: string[];
26
34
  }
27
35
  /**
28
36
  * Returns a cached ThemeManager for the given active theme + themes directory.
@@ -44,11 +52,22 @@ export declare class ThemeManager {
44
52
  constructor(activeTheme: string, themesDir: string);
45
53
  private loadThemeInfo;
46
54
  private buildPaths;
55
+ /**
56
+ * Publish theme.json stylesheets[] as /themes/<theme>/css/<file>.
57
+ * Only a basename ending in .css that exists in that directory is kept,
58
+ * so a theme cannot point the browser at another path.
59
+ */
60
+ private resolveThemeStylesheets;
47
61
  /** Resolve variables.css path — falls back to empty placeholder if theme has no custom vars */
48
62
  private resolveVariablesCssPath;
49
63
  /** Resolve favicon path: prefer favicon.svg → favicon.png → public fallback */
50
64
  private resolveFaviconPath;
51
- /** Resolve logo path: prefer logo.svg → logo.png → favicon.svg → favicon.png → public fallback */
65
+ /**
66
+ * Resolve the header logo path: prefer logo-header.svg → logo-header.png →
67
+ * logo.svg → logo.png → favicon.svg → favicon.png → public fallback.
68
+ * logo-header.* lets a theme show a different copy in the header than the
69
+ * logo.* file that pages may link to.
70
+ */
52
71
  private resolveLogoPath;
53
72
  get paths(): ThemePaths;
54
73
  /** List all available themes (subdirs of themes/ that have a theme.json) */
@@ -21,6 +21,15 @@ declare class FileLoggingProvider extends BaseLoggingProvider {
21
21
  */
22
22
  createTransports(config: LoggingProviderConfig): Transport[];
23
23
  createFormat(): Logform.Format;
24
+ /** CR and LF as the visible two-character sequences `\r` and `\n`, so a value cannot start a line of its own (#2294). */
25
+ private static oneLine;
26
+ /**
27
+ * A stack keeps one frame per line, but its message portion (`Name: message`, which can span lines and can
28
+ * hold a caller-sent value) must not start a log line of its own (#2294). Only a line that begins with
29
+ * optional whitespace and `at ` starts a new output line; every other line is joined to the one before it
30
+ * with a visible `\n`.
31
+ */
32
+ private static stackLines;
24
33
  /**
25
34
  * Render whatever a caller passed as the second argument to `logger.*`.
26
35
  *
@@ -30,7 +39,9 @@ declare class FileLoggingProvider extends BaseLoggingProvider {
30
39
  * line would be noise at best.
31
40
  *
32
41
  * A `stack` is emitted on its own lines because a one-line stack is
33
- * unreadable, and reading the stack is the entire reason this exists.
42
+ * unreadable, and reading the stack is the entire reason this exists. Its
43
+ * frames keep their own lines; CR and LF in its message portion are escaped,
44
+ * as are those in the other keys and their string values.
34
45
  *
35
46
  * @param info - The winston info object for one log record
36
47
  * @returns The rendered metadata, or an empty string when there is none
@@ -45,7 +45,17 @@ class FileLoggingProvider extends BaseLoggingProvider {
45
45
  // #1461: private page titles never reach a log line or /admin/logs.
46
46
  redactPrivateNamesFormat(), format.printf((info) => {
47
47
  const ts = typeof info.timestamp === 'string' ? info.timestamp : JSON.stringify(info.timestamp);
48
- const msg = typeof info.message === 'string' ? info.message : JSON.stringify(info.message);
48
+ // #2294: a caller-sent value (a page name) in the message could carry a CR or LF and start a log line of
49
+ // its own. Escaped here, once, for every transport and every call site; the escape is visible, not removed.
50
+ // Never throws: a logger that throws breaks its caller. Any non-string message (undefined, symbol, function,
51
+ // a circular object) is coerced with String(), and a formatting error falls back to a fixed marker.
52
+ let msg;
53
+ try {
54
+ msg = FileLoggingProvider.oneLine(typeof info.message === 'string' ? info.message : String(info.message));
55
+ }
56
+ catch {
57
+ msg = '[unprintable log message]';
58
+ }
49
59
  // #1141: render the metadata too. This printf used to emit only
50
60
  // timestamp/level/message, so the second argument of every
51
61
  // `logger.error(msg, { error, stack })` call in the codebase was
@@ -57,6 +67,27 @@ class FileLoggingProvider extends BaseLoggingProvider {
57
67
  return meta ? `${ts} [${info.level}]: ${msg} ${meta}` : `${ts} [${info.level}]: ${msg}`;
58
68
  }));
59
69
  }
70
+ /** CR and LF as the visible two-character sequences `\r` and `\n`, so a value cannot start a line of its own (#2294). */
71
+ static oneLine(text) {
72
+ return text.replace(/[\r\n]/g, (c) => (c === '\r' ? '\\r' : '\\n'));
73
+ }
74
+ /**
75
+ * A stack keeps one frame per line, but its message portion (`Name: message`, which can span lines and can
76
+ * hold a caller-sent value) must not start a log line of its own (#2294). Only a line that begins with
77
+ * optional whitespace and `at ` starts a new output line; every other line is joined to the one before it
78
+ * with a visible `\n`.
79
+ */
80
+ static stackLines(stack) {
81
+ const out = [];
82
+ stack.split('\n').forEach((raw, i) => {
83
+ const line = FileLoggingProvider.oneLine(raw);
84
+ if (i === 0 || /^\s*at /.test(raw))
85
+ out.push(line);
86
+ else
87
+ out[out.length - 1] += `\\n${line}`;
88
+ });
89
+ return out.join('\n');
90
+ }
60
91
  /**
61
92
  * Render whatever a caller passed as the second argument to `logger.*`.
62
93
  *
@@ -66,7 +97,9 @@ class FileLoggingProvider extends BaseLoggingProvider {
66
97
  * line would be noise at best.
67
98
  *
68
99
  * A `stack` is emitted on its own lines because a one-line stack is
69
- * unreadable, and reading the stack is the entire reason this exists.
100
+ * unreadable, and reading the stack is the entire reason this exists. Its
101
+ * frames keep their own lines; CR and LF in its message portion are escaped,
102
+ * as are those in the other keys and their string values.
70
103
  *
71
104
  * @param info - The winston info object for one log record
72
105
  * @returns The rendered metadata, or an empty string when there is none
@@ -82,10 +115,10 @@ class FileLoggingProvider extends BaseLoggingProvider {
82
115
  if (value === undefined)
83
116
  continue;
84
117
  if (key === 'stack' && typeof value === 'string') {
85
- stack = value;
118
+ stack = FileLoggingProvider.stackLines(value);
86
119
  continue;
87
120
  }
88
- parts.push(`${key}=${typeof value === 'string' ? value : JSON.stringify(value)}`);
121
+ parts.push(`${FileLoggingProvider.oneLine(key)}=${typeof value === 'string' ? FileLoggingProvider.oneLine(value) : JSON.stringify(value)}`);
89
122
  }
90
123
  const inline = parts.length > 0 ? parts.join(' ') : '';
91
124
  if (stack) {
@@ -28,6 +28,7 @@ import type { WikiEngine } from '../types/WikiEngine.js';
28
28
  import type { Decision, DecisionRequest, Policy } from '../types/Policy.js';
29
29
  import { type RoleGrant } from '../utils/roleGrants.js';
30
30
  import { type ActorContext } from '../context/ActorContext.js';
31
+ import { type OwnedListAsk, type OwnedListDecision, type OwnedRecordAsk } from './ownedRecords.js';
31
32
  export declare const POLICIES_KEY = "ngdpbase.access.policies";
32
33
  export declare const POLICIES_ENABLED_KEY = "ngdpbase.access.policies.enabled";
33
34
  /** Whether an administrator may enter another person's unencrypted vault (#2255). */
@@ -50,7 +51,8 @@ export declare class PolicyDecisionPoint extends BaseManager {
50
51
  /** Warn, once per policy and name, that a policy names a retired permission and what it is read as (#2038). */
51
52
  private warnRetiredOnce;
52
53
  /**
53
- * The policies in force right now, highest priority first.
54
+ * The policies in force right now, in configuration order. Order carries no
55
+ * meaning: the rule-combining algorithm is `deny-overrides` (#2330).
54
56
  *
55
57
  * - `ngdpbase.access.policies.enabled` false — its default when unset —
56
58
  * means none;
@@ -73,8 +75,8 @@ export declare class PolicyDecisionPoint extends BaseManager {
73
75
  * What each role permits: role name → the actions its holders are granted.
74
76
  *
75
77
  * It mirrors the evaluator's ROLE matching — an allow policy naming a role
76
- * grants its actions, a deny policy naming it takes them away, lower
77
- * priority first so a higher-priority deny lands last and wins. It does not
78
+ * grants its actions, a deny policy naming it takes them away, whatever the
79
+ * order (deny-overrides, #2330). It does not
78
80
  * evaluate resource patterns or the ceilings, so it is a summary for a human
79
81
  * reading a table (the admin Security Policy Summary, a profile), never an
80
82
  * authorisation answer — that is {@link permits} with the caller's subject.
@@ -332,6 +334,51 @@ export declare class PolicyDecisionPoint extends BaseManager {
332
334
  * with `pushed-pages-edit` to leave pushed pages out. A summary of the
333
335
  * policies, never an answer to "may this subject".
334
336
  */
337
+ /**
338
+ * May this subject act on one stored add-on record (#1823)?
339
+ *
340
+ * The manager passes attributes it read from the row. `owner` is the
341
+ * identity @id. A type that declares `owner` is allowed only when that @id
342
+ * is the subject's (the person a proxy grant represents, not the actor),
343
+ * and policy also allows the action. A role does not bypass the owner. A
344
+ * type with no `owner` is policy alone. A core type (`page`, `vault`, …)
345
+ * is not decided here. Missing data throws {@link OwnedRecordError}; the
346
+ * caller fails closed. This records nothing.
347
+ */
348
+ decideOwnedRecord(input: DecisionInput, request: OwnedRecordAsk): Promise<Decision>;
349
+ /**
350
+ * May this subject list a type, and which rows (#1823)?
351
+ *
352
+ * An allow for a type that declares `owner` carries one filter, `owner`
353
+ * equals the subject's identity @id. The manager binds that value; it is
354
+ * not SQL. An allow for a type with no owner carries an empty filter: policy
355
+ * alone, no row constraint. A refusal is `{ decision: 'deny' }`, never an
356
+ * empty filter and never an empty row set from this method. Missing data
357
+ * throws {@link OwnedRecordError}. This records nothing: the list is logged
358
+ * once by the manager that applies it.
359
+ */
360
+ decideOwnedList(input: DecisionInput, request: OwnedListAsk): Promise<OwnedListDecision>;
361
+ /**
362
+ * The ceilings, the declared type, and a reader for conditions. A ceiling
363
+ * denial is the whole answer. Anything that cannot be decided throws.
364
+ */
365
+ private prepareOwned;
366
+ private ownedTypeEntry;
367
+ private typeDeclaresOwner;
368
+ /** The owner the manager read. Missing on an owner-typed record cannot be decided. */
369
+ private storedOwner;
370
+ private ownerMismatch;
371
+ /** The subject's identity @id, or a denial when nobody is signed in. A signed-in subject with no identity cannot be decided. */
372
+ private subjectOwnerId;
373
+ private ownedUserContext;
374
+ /**
375
+ * Condition values (#1823). A `resource:` attribute is the value the manager
376
+ * read from the stored row, never a request field. Subject and environment
377
+ * attributes come from the PIP.
378
+ */
379
+ private ownedReader;
380
+ private ownedTypeResult;
381
+ private decisionFrom;
335
382
  categoriesGranting(action: string): string[];
336
383
  }
337
384
  export default PolicyDecisionPoint;
@@ -138,6 +138,7 @@ export interface PageAttributes {
138
138
  * if (await pip.checkPagePermissionWithContext(wikiContext, 'edit')) { ... }
139
139
  */
140
140
  declare class PolicyInformationPoint extends BaseManager {
141
+ private readonly clock;
141
142
  /** Read at the moment of use: this manager starts before PolicyEvaluator (#1431 step 13). */
142
143
  private get policyEvaluator();
143
144
  /**
@@ -145,8 +146,9 @@ declare class PolicyInformationPoint extends BaseManager {
145
146
  *
146
147
  * @constructor
147
148
  * @param {WikiEngine} engine - The wiki engine instance
149
+ * @param {() => Date} [clock] - The time `environment:` attributes are read at; injectable for tests
148
150
  */
149
- constructor(engine: WikiEngine);
151
+ constructor(engine: WikiEngine, clock?: () => Date);
150
152
  /**
151
153
  * Initializes the PolicyInformationPoint by loading policies and configurations
152
154
  *
@@ -543,6 +545,23 @@ declare class PolicyInformationPoint extends BaseManager {
543
545
  * evaluator awaits every read).
544
546
  */
545
547
  subjectAttribute(subject: SubjectFacts | null | undefined, name: string): unknown;
548
+ /**
549
+ * An environment attribute a condition reads (#2261): `environment:<name>`,
550
+ * declared in `ngdpbase.access.environment-attributes` and read from its
551
+ * `from` source here, the only place one is read. `schedule:<name>` is whether
552
+ * that schedule in `ngdpbase.schedules` is open at `now`; `config:<key>` is
553
+ * the boolean stored at that configuration key, any key, secrets included.
554
+ * Missing when it is not declared, the schedule is absent, the stored value is
555
+ * not a boolean, or the read fails; the evaluator then fails closed. The value
556
+ * is returned to the evaluator and goes nowhere else: it is never logged.
557
+ */
558
+ environmentAttribute(name: string, now?: Date): boolean | undefined;
559
+ /**
560
+ * A reader of environment attributes for one decision or one listing
561
+ * (#2261): the clock is read once, on the first attribute asked for, and
562
+ * each attribute once, so every policy and every page sees one instant.
563
+ */
564
+ environmentReader(): (name: string) => unknown;
546
565
  /** One read of a declared subject attribute from its source. A value of the wrong type, or a source that throws, is missing. */
547
566
  private readSubjectSource;
548
567
  /** The page's category for the policies (#2255): lowercased; the default when it has none; `shared` in a shared folder. */
@@ -581,6 +600,13 @@ declare class PolicyInformationPoint extends BaseManager {
581
600
  * nothing. Same record as every other page denial, `policyIds` included.
582
601
  */
583
602
  recordPageRefusal(subject: UserContext | null | undefined, pageName: string, action: string, reason: string, policyIds?: readonly string[], conditionNotMet?: readonly string[]): Promise<void>;
603
+ /**
604
+ * Record a write the signed-out write gate refused before any route ran
605
+ * (#2298). The resource is the route as asked (`POST /save/Home`): method
606
+ * and path, never the query or the body, which can carry a password or a
607
+ * token. Standard tier and best-effort, like every other denial.
608
+ */
609
+ recordRouteRefusal(subject: UserContext | null | undefined, route: string): Promise<void>;
584
610
  /** The name a decision is recorded under: the subject's, or `anonymous`. */
585
611
  private static recordedUser;
586
612
  /**
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Add-on record decisions (#1823).
3
+ *
4
+ * The add-on's manager is the enforcement point. It reads `owner` from the
5
+ * stored row and asks {@link PolicyDecisionPoint.decideOwnedRecord} or
6
+ * {@link PolicyDecisionPoint.decideOwnedList}. The answer is never null.
7
+ * A type that declares `owner` (an identity @id) is refused to anyone but
8
+ * that owner: under a proxy grant the subject is the person represented, so
9
+ * the delegate is allowed and a role is not. A type with no `owner` is
10
+ * decided by policy alone. A list that is refused is a denial, not an empty
11
+ * page of rows.
12
+ */
13
+ /** The PDP cannot decide. The caller fails closed; this is not a deny. */
14
+ export declare class OwnedRecordError extends Error {
15
+ constructor(message: string);
16
+ }
17
+ /** One record, with the attributes the manager read from its stored row. */
18
+ export interface OwnedRecordAsk {
19
+ action: string;
20
+ resource: {
21
+ type: string;
22
+ id: string;
23
+ /** From the stored row. `owner`, when the type declares it, is the identity @id. */
24
+ attributes?: Record<string, unknown>;
25
+ };
26
+ }
27
+ /** A list of one type. There is no owner argument: the filter comes from the subject. */
28
+ export interface OwnedListAsk {
29
+ action: string;
30
+ resource: {
31
+ type: string;
32
+ };
33
+ }
34
+ /**
35
+ * A constraint the manager turns into a bound parameter. Never SQL.
36
+ * `equals` means the column must equal the one value.
37
+ */
38
+ export interface OwnedRecordFilter {
39
+ attribute: string;
40
+ operator: 'equals';
41
+ values: readonly string[];
42
+ }
43
+ /**
44
+ * The resource a refusal is audited against (#1823). `owner` is the identity
45
+ * @id from the stored row, `ownerUser` that person's username. Both come from
46
+ * storage, never from the request body.
47
+ */
48
+ export interface OwnedAuditResource {
49
+ type: string;
50
+ id?: string;
51
+ owner?: string;
52
+ ownerUser?: string;
53
+ }
54
+ /** An explicit list answer (#1823). `filter` is empty only when the type has no owner. */
55
+ export type OwnedListDecision = {
56
+ decision: 'allow';
57
+ filter: readonly OwnedRecordFilter[];
58
+ } | {
59
+ decision: 'deny';
60
+ reason: string;
61
+ policyIds: readonly string[];
62
+ };
63
+ /**
64
+ * What is wrong with a resource type's `owner` declaration, or null (#1823).
65
+ * `owner`, when present at all, must be exactly `{ "type": "identity" }`.
66
+ * Anything else, `null` included, would quietly make an owner type
67
+ * policy-alone, so the decision throws and start-up refuses the config.
68
+ */
69
+ export declare function ownerDeclarationProblem(type: string, entry: unknown): string | null;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The signed-out write list (#2298): the one place that names which routes may
3
+ * be written to by someone who is not signed in.
4
+ *
5
+ * Every request from a caller who is not signed in, other than GET, HEAD and
6
+ * OPTIONS, is refused by `signedOutWriteGate` (src/middleware/signedOutWriteGate.ts),
7
+ * before any route runs, unless the route is on this list. A new write route
8
+ * is therefore closed to signed-out callers until someone adds it here with a
9
+ * reason, which is the point (#2294 came from routes that were open until
10
+ * their author remembered to close them).
11
+ *
12
+ * What belongs here: a route a person reaches BEFORE they are signed in (the
13
+ * steps of signing in, registering, recovering a password, accepting an
14
+ * invitation), a link followed from an email in any browser, and a public form.
15
+ * What does not: anything that asks for a permission a signed-out caller
16
+ * never holds. A route that answers its own token (a Bearer token, which the
17
+ * gate counts as signed in) needs no entry.
18
+ *
19
+ * Core's routes are listed below. An add-on lists its own through the add-on
20
+ * host, `host.allowSignedOutWrite(...)`, which accepts only a route under the
21
+ * add-on's own `/api/<slug>/` or `/addons/<slug>/`, and refuses a slug that
22
+ * names one of core's own `/api/` or `/addons/` prefixes. Those are the only
23
+ * ways onto the list, and it is sealed once the add-ons have loaded; outside
24
+ * tests (`NODE_ENV=test`) the seal cannot be undone.
25
+ *
26
+ * Permission to write is still asked by each route, as always: an entry only
27
+ * lets the request reach the route, it grants nothing.
28
+ */
29
+ export type WriteMethod = 'POST' | 'PUT' | 'PATCH' | 'DELETE';
30
+ export interface SignedOutWriteRoute {
31
+ method: WriteMethod;
32
+ /** An Express path: literal segments and `:name` parameters only. */
33
+ path: string;
34
+ /** Why a caller who is not signed in must reach it. Required. */
35
+ reason: string;
36
+ }
37
+ /** Whether the method may change things, and so is held by the gate: anything but GET, HEAD and OPTIONS. */
38
+ export declare function isWriteMethod(method: string): boolean;
39
+ /**
40
+ * Slugs an add-on may not use to list a route, because core already answers
41
+ * under `/api/<slug>/` or `/addons/<slug>/`: an add-on in a directory named
42
+ * `admin` must not open `/api/admin/...`. Kept in step with core's route
43
+ * files; `signedOutWriteGate.test.ts` checks it against the route inventory.
44
+ */
45
+ export declare const RESERVED_ADDON_SLUGS: ReadonlySet<string>;
46
+ /**
47
+ * Core's list. Each reason says why the route cannot wait for a sign-in.
48
+ * Routes that need a signed-in session by their nature (consent, the
49
+ * account-setup password, the second-factor enrolment, reauthentication) are
50
+ * not here: their callers are signed in when they reach them.
51
+ */
52
+ export declare const CORE_SIGNED_OUT_WRITES: readonly SignedOutWriteRoute[];
53
+ /**
54
+ * An add-on's declaration of a route a signed-out caller may write to.
55
+ * Called through `AddonHost.allowSignedOutWrite`, bound to the add-on's slug.
56
+ * Throws on anything outside the add-on's own paths, so one add-on can never
57
+ * open another's, or core's.
58
+ */
59
+ export declare function declareAddonSignedOutWrite(slug: string, route: SignedOutWriteRoute): void;
60
+ /** Close the list to add-ons. Called once, after they have loaded. */
61
+ export declare function sealSignedOutWrites(): void;
62
+ /** Test support: forget add-on declarations and reopen the list. Refuses outside `NODE_ENV=test`, so a sealed list stays sealed. */
63
+ export declare function resetSignedOutWrites(): void;
64
+ /** Every route on the list, core's then the add-ons'. */
65
+ export declare function signedOutWriteRoutes(): Array<SignedOutWriteRoute & {
66
+ owner: string;
67
+ }>;
68
+ /** The reason the route is on the list, or null when a signed-out caller may not write to it. */
69
+ export declare function signedOutWriteReason(method: string, path: string): string | null;