@checkstack/healthcheck-backend 1.20.1 → 1.21.1
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 +169 -0
- package/package.json +22 -22
- package/src/ai/shell-env-table.test.ts +4 -2
- package/src/assignment-access.test.ts +332 -0
- package/src/assignment-access.ts +205 -0
- package/src/collector-script-test.test.ts +37 -0
- package/src/collector-script-test.ts +54 -16
- package/src/queue-executor.test.ts +57 -3
- package/src/queue-executor.ts +6 -2
- package/src/router-assignment-access.test.ts +260 -0
- package/src/router.ts +102 -3
- package/src/service.ts +82 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,174 @@
|
|
|
1
1
|
# @checkstack/healthcheck-backend
|
|
2
2
|
|
|
3
|
+
## 1.21.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [6540703]
|
|
8
|
+
- Updated dependencies [099045f]
|
|
9
|
+
- @checkstack/ai-backend@0.11.1
|
|
10
|
+
- @checkstack/automation-backend@0.11.5
|
|
11
|
+
- @checkstack/catalog-backend@1.9.1
|
|
12
|
+
- @checkstack/incident-backend@1.13.3
|
|
13
|
+
- @checkstack/secrets-backend@0.3.7
|
|
14
|
+
- @checkstack/satellite-backend@0.9.1
|
|
15
|
+
- @checkstack/status-page-backend@0.6.3
|
|
16
|
+
|
|
17
|
+
## 1.21.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- a74fa01: Relocate health-check assignment management from the catalog-entered,
|
|
22
|
+
system-centric Assignment IDE into the check editor itself, so a check's
|
|
23
|
+
settings AND its assignments (with their per-system settings) are managed in
|
|
24
|
+
one place. Users think in terms of "Health Checks", not the catalog - the
|
|
25
|
+
old flow was discovered through a catalog system row and inverted that mental
|
|
26
|
+
model.
|
|
27
|
+
|
|
28
|
+
- **Check editor Assignment section** (edit mode): lists every assigned
|
|
29
|
+
system as a tree group with the per-assignment panels (General, Thresholds,
|
|
30
|
+
Retention, Execution with satellites + environment fan-out, Notifications)
|
|
31
|
+
plus an "Assign to system..." picker that only offers systems the caller
|
|
32
|
+
can manage. The `AssignmentIDENodeSlot`/`AssignmentIDEPanelSlot` extension
|
|
33
|
+
points keep their names and context shape - extension node ids are
|
|
34
|
+
namespaced per system internally so config-keyed ids (e.g. the anomaly
|
|
35
|
+
panels) no longer collide across systems.
|
|
36
|
+
- **New procedure `getConfigurationAssignments`** (config → systems, the
|
|
37
|
+
inverse of `getSystemAssociations`), handler-authorized fail-closed: global
|
|
38
|
+
configuration read or a team grant on the configuration sees every row;
|
|
39
|
+
otherwise rows filter to systems the caller may read.
|
|
40
|
+
- **`getConfiguration` relaxed** (handler-authorized): a reader of an
|
|
41
|
+
ASSIGNED system may load the (redacted) configuration - the same exposure
|
|
42
|
+
`getSystemConfigurations` already allowed - so system managers can open the
|
|
43
|
+
editor. Unauthorized callers still get the same `undefined` as a missing id.
|
|
44
|
+
- **RLAC**: the edit and config routes now declare
|
|
45
|
+
`manageCapability.parentType: catalog.system`, so a pure system manager
|
|
46
|
+
reaches the editor for its Assignment section; the config side renders
|
|
47
|
+
read-only for them (Save disabled, strategies/collectors/access-control
|
|
48
|
+
gated per-node) while their systems' assignment panels stay writable.
|
|
49
|
+
GitOps-locked systems lock exactly their own assignment nodes.
|
|
50
|
+
- **Catalog wayfinding**: the per-system row button is now a
|
|
51
|
+
"Manage health checks" link opening the Health Checks list pre-filtered to
|
|
52
|
+
that system (`?system=<id>`); the filtered list loads via the
|
|
53
|
+
system-read-gated `getSystemConfigurations`, so it also works for system
|
|
54
|
+
managers without healthcheck grants.
|
|
55
|
+
|
|
56
|
+
BREAKING CHANGES: the standalone system-centric assignment page is removed -
|
|
57
|
+
the `healthcheck` plugin's `assignments` route (`/assignments/:systemId`) no
|
|
58
|
+
longer exists and `healthcheckRoutes.routes.assignments` is gone from
|
|
59
|
+
`@checkstack/healthcheck-common`. Deep links to the old page now 404; use the
|
|
60
|
+
check editor's Assignment section (or the filtered Health Checks list)
|
|
61
|
+
instead.
|
|
62
|
+
|
|
63
|
+
- 4568dcc: Expose health check environment resolution to cross-plugin callers via a new
|
|
64
|
+
`resolveEnqueueEnvironments({ configId, systemId })` procedure. It returns the
|
|
65
|
+
effective environment ids a one-off run should enqueue for (or `[null]` for an
|
|
66
|
+
env-less system) - the same fan-out the `run_now` automation and the recurring
|
|
67
|
+
scheduler use. Gated by any healthcheck read capability (`typeScoped` read),
|
|
68
|
+
consistent with the other utility reads.
|
|
69
|
+
|
|
70
|
+
This lets a cross-plugin health trigger enqueue exactly the environment slices
|
|
71
|
+
the run executor accepts. Previously such a caller could only enqueue an env-less
|
|
72
|
+
run (`environmentId: null`), which the executor drops as stale for a system that
|
|
73
|
+
has effective environments - so the trigger was a silent no-op for env-assigned
|
|
74
|
+
systems. The log-stream fast-path health trigger is the first consumer (covered
|
|
75
|
+
by the existing log streams changeset).
|
|
76
|
+
|
|
77
|
+
- d00e099: Make a catalog System's free-form `metadata` (custom fields) genuinely usable
|
|
78
|
+
end to end, mirroring how Environment custom fields already work. Previously a
|
|
79
|
+
System's `metadata` column was writable but nothing consumed it - it did not
|
|
80
|
+
surface in templating, could not be set via GitOps, and had no UI editor, so
|
|
81
|
+
models (and users) had no way to understand what it was for.
|
|
82
|
+
|
|
83
|
+
Now a system's custom fields are surfaced everywhere an environment's already
|
|
84
|
+
are:
|
|
85
|
+
|
|
86
|
+
- **Config templating**: a system's fields render as
|
|
87
|
+
`{{ system.metadata.<key> }}` in templatable health-check config (e.g. an
|
|
88
|
+
HTTP URL). They are namespaced under `.metadata` so a field named `id`/`name`
|
|
89
|
+
can never shadow the structural `{{ system.id }}` / `{{ system.name }}`.
|
|
90
|
+
- **Satellites**: the fields ride the satellite assignment
|
|
91
|
+
(`SatelliteAssignment.systemMetadata`) so satellite runs template
|
|
92
|
+
`{{ system.metadata.<key> }}` identically to local runs.
|
|
93
|
+
- **UI**: the System editor gains a free-form key/value custom-fields editor
|
|
94
|
+
(extracted into a shared `CustomFieldsEditor` used by both the System and
|
|
95
|
+
Environment editors).
|
|
96
|
+
- **GitOps**: the `System` kind accepts optional `spec.fields`, replaced on
|
|
97
|
+
every reconcile (same shape as the `Environment` kind).
|
|
98
|
+
- **Script collectors**: inline TS collectors read `context.system.metadata`
|
|
99
|
+
(SDK editor types updated), and shell collectors get one
|
|
100
|
+
`CHECKSTACK_SYSTEM_<FIELD>` env var per field, mirroring
|
|
101
|
+
`CHECKSTACK_ENV_<FIELD>`. A field that normalizes to a reserved name
|
|
102
|
+
(`CHECKSTACK_SYSTEM_ID`/`_NAME`) is now skipped with a warning rather than
|
|
103
|
+
clobbering the built-in; the same reserved-name guard was added to the
|
|
104
|
+
environment shell-env builder (previously a custom field named `id`/`name`
|
|
105
|
+
could shadow the structural var).
|
|
106
|
+
- **Editor autocomplete/preview**: the health-check editor offers
|
|
107
|
+
`{{ system.metadata.<key> }}` completions and previews their values when a
|
|
108
|
+
concrete system is in context.
|
|
109
|
+
|
|
110
|
+
The AI assistant is corrected on two fronts:
|
|
111
|
+
|
|
112
|
+
- The catalog create/update-system (and create-environment) tool schemas now
|
|
113
|
+
`.describe()` their `metadata` field, so a model knows it is free-form custom
|
|
114
|
+
fields that surface in templating - not a tagging/labeling mechanism - and
|
|
115
|
+
should only set keys the user explicitly asks for.
|
|
116
|
+
- A new "Acting on requests" chat system-prompt rule tells the assistant to
|
|
117
|
+
perform a requested change via its tool instead of deflecting to a manual
|
|
118
|
+
GitOps/UI how-to, and to name the missing permission when a tool is genuinely
|
|
119
|
+
unavailable. (This entry also covers the regenerated docs index reflecting the
|
|
120
|
+
updated GitOps/templating docs.)
|
|
121
|
+
|
|
122
|
+
State & scale: a system's metadata continues to live solely in the
|
|
123
|
+
`catalog.systems.metadata` Postgres column and is read via the existing
|
|
124
|
+
`getSystem` RPC, so every pod reads the same value. The satellite assignment
|
|
125
|
+
carries a per-dispatch snapshot for the duration of that run (ephemeral,
|
|
126
|
+
re-read on the next dispatch), not a second source of truth. No new table or
|
|
127
|
+
migration.
|
|
128
|
+
|
|
129
|
+
### Patch Changes
|
|
130
|
+
|
|
131
|
+
- Updated dependencies [4568dcc]
|
|
132
|
+
- Updated dependencies [a74fa01]
|
|
133
|
+
- Updated dependencies [a74fa01]
|
|
134
|
+
- Updated dependencies [d9f2771]
|
|
135
|
+
- Updated dependencies [4568dcc]
|
|
136
|
+
- Updated dependencies [4568dcc]
|
|
137
|
+
- Updated dependencies [4568dcc]
|
|
138
|
+
- Updated dependencies [4568dcc]
|
|
139
|
+
- Updated dependencies [4568dcc]
|
|
140
|
+
- Updated dependencies [4568dcc]
|
|
141
|
+
- Updated dependencies [4568dcc]
|
|
142
|
+
- Updated dependencies [4568dcc]
|
|
143
|
+
- Updated dependencies [a74fa01]
|
|
144
|
+
- Updated dependencies [d00e099]
|
|
145
|
+
- @checkstack/ai-backend@0.11.0
|
|
146
|
+
- @checkstack/healthcheck-common@1.17.0
|
|
147
|
+
- @checkstack/satellite-backend@0.9.0
|
|
148
|
+
- @checkstack/signal-common@0.3.0
|
|
149
|
+
- @checkstack/catalog-common@2.7.3
|
|
150
|
+
- @checkstack/backend-api@0.33.0
|
|
151
|
+
- @checkstack/catalog-backend@1.9.0
|
|
152
|
+
- @checkstack/automation-backend@0.11.4
|
|
153
|
+
- @checkstack/incident-backend@1.13.2
|
|
154
|
+
- @checkstack/sdk@0.130.1
|
|
155
|
+
- @checkstack/ai-common@0.6.6
|
|
156
|
+
- @checkstack/cache-api@0.3.19
|
|
157
|
+
- @checkstack/cache-utils@0.3.0
|
|
158
|
+
- @checkstack/command-backend@0.2.25
|
|
159
|
+
- @checkstack/common@0.22.0
|
|
160
|
+
- @checkstack/gitops-backend@0.5.25
|
|
161
|
+
- @checkstack/gitops-common@0.7.3
|
|
162
|
+
- @checkstack/incident-common@1.10.3
|
|
163
|
+
- @checkstack/maintenance-common@1.10.3
|
|
164
|
+
- @checkstack/notification-common@1.7.1
|
|
165
|
+
- @checkstack/queue-api@0.3.19
|
|
166
|
+
- @checkstack/script-packages-backend@0.4.4
|
|
167
|
+
- @checkstack/secrets-backend@0.3.7
|
|
168
|
+
- @checkstack/secrets-common@0.3.2
|
|
169
|
+
- @checkstack/status-page-backend@0.6.2
|
|
170
|
+
- @checkstack/status-page-common@0.6.3
|
|
171
|
+
|
|
3
172
|
## 1.20.1
|
|
4
173
|
|
|
5
174
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/healthcheck-backend",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.21.1",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -14,32 +14,32 @@
|
|
|
14
14
|
"lint:code": "eslint . --max-warnings 0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@checkstack/ai-backend": "0.
|
|
17
|
+
"@checkstack/ai-backend": "0.11.1",
|
|
18
18
|
"@checkstack/ai-common": "0.6.6",
|
|
19
|
-
"@checkstack/automation-backend": "0.11.
|
|
20
|
-
"@checkstack/backend-api": "0.
|
|
19
|
+
"@checkstack/automation-backend": "0.11.5",
|
|
20
|
+
"@checkstack/backend-api": "0.33.0",
|
|
21
21
|
"@checkstack/cache-api": "0.3.19",
|
|
22
22
|
"@checkstack/cache-utils": "0.3.0",
|
|
23
|
-
"@checkstack/catalog-backend": "1.
|
|
24
|
-
"@checkstack/catalog-common": "2.7.
|
|
25
|
-
"@checkstack/command-backend": "0.2.
|
|
23
|
+
"@checkstack/catalog-backend": "1.9.1",
|
|
24
|
+
"@checkstack/catalog-common": "2.7.3",
|
|
25
|
+
"@checkstack/command-backend": "0.2.25",
|
|
26
26
|
"@checkstack/common": "0.22.0",
|
|
27
|
-
"@checkstack/gitops-backend": "0.5.
|
|
27
|
+
"@checkstack/gitops-backend": "0.5.25",
|
|
28
28
|
"@checkstack/gitops-common": "0.7.3",
|
|
29
|
-
"@checkstack/healthcheck-common": "1.
|
|
30
|
-
"@checkstack/incident-backend": "1.13.
|
|
31
|
-
"@checkstack/incident-common": "1.10.
|
|
32
|
-
"@checkstack/maintenance-common": "1.10.
|
|
33
|
-
"@checkstack/notification-common": "1.7.
|
|
29
|
+
"@checkstack/healthcheck-common": "1.17.0",
|
|
30
|
+
"@checkstack/incident-backend": "1.13.3",
|
|
31
|
+
"@checkstack/incident-common": "1.10.3",
|
|
32
|
+
"@checkstack/maintenance-common": "1.10.3",
|
|
33
|
+
"@checkstack/notification-common": "1.7.1",
|
|
34
34
|
"@checkstack/queue-api": "0.3.19",
|
|
35
|
-
"@checkstack/satellite-backend": "0.
|
|
36
|
-
"@checkstack/script-packages-backend": "0.4.
|
|
37
|
-
"@checkstack/sdk": "0.
|
|
38
|
-
"@checkstack/secrets-backend": "0.3.
|
|
35
|
+
"@checkstack/satellite-backend": "0.9.1",
|
|
36
|
+
"@checkstack/script-packages-backend": "0.4.4",
|
|
37
|
+
"@checkstack/sdk": "0.131.0",
|
|
38
|
+
"@checkstack/secrets-backend": "0.3.7",
|
|
39
39
|
"@checkstack/secrets-common": "0.3.2",
|
|
40
|
-
"@checkstack/signal-common": "0.
|
|
41
|
-
"@checkstack/status-page-backend": "0.6.
|
|
42
|
-
"@checkstack/status-page-common": "0.6.
|
|
40
|
+
"@checkstack/signal-common": "0.3.0",
|
|
41
|
+
"@checkstack/status-page-backend": "0.6.3",
|
|
42
|
+
"@checkstack/status-page-common": "0.6.3",
|
|
43
43
|
"@hono/zod-validator": "^0.7.6",
|
|
44
44
|
"@orpc/contract": "^1.14.4",
|
|
45
45
|
"@orpc/server": "^1.14.4",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@checkstack/drizzle-helper": "0.0.6",
|
|
55
|
-
"@checkstack/scripts": "0.7.
|
|
56
|
-
"@checkstack/test-utils-backend": "0.1.
|
|
55
|
+
"@checkstack/scripts": "0.7.4",
|
|
56
|
+
"@checkstack/test-utils-backend": "0.1.59",
|
|
57
57
|
"@checkstack/tsconfig": "0.0.7",
|
|
58
58
|
"@types/bun": "^1.0.0",
|
|
59
59
|
"@types/pg": "^8.20.0",
|
|
@@ -14,7 +14,8 @@ describe("HEALTHCHECK_SHELL_ENV drift guard", () => {
|
|
|
14
14
|
test("the static table describes every reserved var the producer emits", () => {
|
|
15
15
|
const produced = buildShellRunContextEnv({
|
|
16
16
|
check: { id: "c1", name: "Check One", intervalSeconds: 60 },
|
|
17
|
-
|
|
17
|
+
// A custom field exercises the CHECKSTACK_SYSTEM_<FIELD> derivation.
|
|
18
|
+
system: { id: "s1", name: "System One", metadata: { baseUrl: "https://x" } },
|
|
18
19
|
environment: {
|
|
19
20
|
id: "e1",
|
|
20
21
|
name: "prod",
|
|
@@ -24,8 +25,9 @@ describe("HEALTHCHECK_SHELL_ENV drift guard", () => {
|
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
const tableNames = new Set(HEALTHCHECK_SHELL_ENV.map((v) => v.name));
|
|
27
|
-
// The dynamic per-field
|
|
28
|
+
// The dynamic per-field vars are covered by the wildcard descriptors.
|
|
28
29
|
tableNames.add("CHECKSTACK_ENV_REGION");
|
|
30
|
+
tableNames.add("CHECKSTACK_SYSTEM_BASE_URL");
|
|
29
31
|
|
|
30
32
|
for (const producedName of Object.keys(produced)) {
|
|
31
33
|
expect(tableNames.has(producedName)).toBe(true);
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { AuthService, AuthUser } from "@checkstack/backend-api";
|
|
3
|
+
import {
|
|
4
|
+
canReadConfigurationScope,
|
|
5
|
+
hasConfigurationReadGrant,
|
|
6
|
+
hasGlobalConfigurationRead,
|
|
7
|
+
listReadableSystemIds,
|
|
8
|
+
resolveAssignmentRowScope,
|
|
9
|
+
} from "./assignment-access";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Regression guard for the configuration-centric assignment authorization:
|
|
13
|
+
* global `configuration.read`/`.manage` (or wildcard/service) or a team grant
|
|
14
|
+
* on the CONFIGURATION yields every assignment row, a system-scoped caller is
|
|
15
|
+
* restricted to the systems their teams may read, anyone else is forbidden,
|
|
16
|
+
* and any auth (S2S) failure fails CLOSED.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const QUALIFIED_READ = "healthcheck.healthcheck.read";
|
|
20
|
+
const QUALIFIED_MANAGE = "healthcheck.healthcheck.manage";
|
|
21
|
+
const QUALIFIED_SYSTEM_READ = "catalog.system.read";
|
|
22
|
+
const QUALIFIED_SYSTEM_MANAGE = "catalog.system.manage";
|
|
23
|
+
|
|
24
|
+
const realUser = (accessRules: string[]): AuthUser => ({
|
|
25
|
+
type: "user",
|
|
26
|
+
id: "u1",
|
|
27
|
+
accessRules,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Cast is unavoidable for a partial mock: the helpers only touch
|
|
31
|
+
// `listAccessibleObjectIds` / `check`, and implementing the full AuthService
|
|
32
|
+
// here would just be noise.
|
|
33
|
+
const authWith = (impl: Partial<AuthService>): AuthService =>
|
|
34
|
+
impl as unknown as AuthService;
|
|
35
|
+
|
|
36
|
+
describe("hasGlobalConfigurationRead", () => {
|
|
37
|
+
test("true for global read, global manage, wildcard, and services; false otherwise", () => {
|
|
38
|
+
expect(hasGlobalConfigurationRead(realUser([QUALIFIED_READ]))).toBe(true);
|
|
39
|
+
expect(hasGlobalConfigurationRead(realUser([QUALIFIED_MANAGE]))).toBe(true);
|
|
40
|
+
expect(hasGlobalConfigurationRead(realUser(["*"]))).toBe(true);
|
|
41
|
+
expect(
|
|
42
|
+
hasGlobalConfigurationRead({ type: "service", pluginId: "slo" }),
|
|
43
|
+
).toBe(true);
|
|
44
|
+
expect(
|
|
45
|
+
hasGlobalConfigurationRead(realUser([QUALIFIED_SYSTEM_MANAGE])),
|
|
46
|
+
).toBe(false);
|
|
47
|
+
expect(hasGlobalConfigurationRead(undefined)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("resolveAssignmentRowScope", () => {
|
|
52
|
+
test("global read rule yields every row", () => {
|
|
53
|
+
expect(
|
|
54
|
+
resolveAssignmentRowScope({
|
|
55
|
+
user: realUser([QUALIFIED_READ]),
|
|
56
|
+
hasConfigurationGrant: false,
|
|
57
|
+
readableSystemIds: [],
|
|
58
|
+
}),
|
|
59
|
+
).toEqual({ kind: "all" });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a configuration team grant yields every row", () => {
|
|
63
|
+
expect(
|
|
64
|
+
resolveAssignmentRowScope({
|
|
65
|
+
user: realUser([]),
|
|
66
|
+
hasConfigurationGrant: true,
|
|
67
|
+
readableSystemIds: [],
|
|
68
|
+
}),
|
|
69
|
+
).toEqual({ kind: "all" });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("system grants alone scope the rows to those systems", () => {
|
|
73
|
+
expect(
|
|
74
|
+
resolveAssignmentRowScope({
|
|
75
|
+
user: realUser([]),
|
|
76
|
+
hasConfigurationGrant: false,
|
|
77
|
+
readableSystemIds: ["sys-1"],
|
|
78
|
+
}),
|
|
79
|
+
).toEqual({ kind: "scoped", systemIds: ["sys-1"] });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("no global rule and no grant of either kind is forbidden", () => {
|
|
83
|
+
expect(
|
|
84
|
+
resolveAssignmentRowScope({
|
|
85
|
+
user: realUser([]),
|
|
86
|
+
hasConfigurationGrant: false,
|
|
87
|
+
readableSystemIds: [],
|
|
88
|
+
}),
|
|
89
|
+
).toEqual({ kind: "forbidden" });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("missing user is forbidden", () => {
|
|
93
|
+
expect(
|
|
94
|
+
resolveAssignmentRowScope({
|
|
95
|
+
user: undefined,
|
|
96
|
+
hasConfigurationGrant: false,
|
|
97
|
+
readableSystemIds: ["sys-1"],
|
|
98
|
+
}),
|
|
99
|
+
).toEqual({ kind: "forbidden" });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("a read rule from ANOTHER plugin does not unlock the rows", () => {
|
|
103
|
+
expect(
|
|
104
|
+
resolveAssignmentRowScope({
|
|
105
|
+
user: realUser(["incident.incident.read"]),
|
|
106
|
+
hasConfigurationGrant: false,
|
|
107
|
+
readableSystemIds: [],
|
|
108
|
+
}),
|
|
109
|
+
).toEqual({ kind: "forbidden" });
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("hasConfigurationReadGrant", () => {
|
|
114
|
+
test("resolves a READ grant on the configuration via the auth S2S", async () => {
|
|
115
|
+
const auth = authWith({
|
|
116
|
+
check: async ({ objectType, objectId, action }) => {
|
|
117
|
+
expect(objectType).toBe("healthcheck.healthcheck");
|
|
118
|
+
expect(action).toBe("read");
|
|
119
|
+
return { hasAccess: objectId === "cfg-1" };
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await expect(
|
|
124
|
+
hasConfigurationReadGrant({
|
|
125
|
+
auth,
|
|
126
|
+
user: realUser([]),
|
|
127
|
+
configurationId: "cfg-1",
|
|
128
|
+
}),
|
|
129
|
+
).resolves.toBe(true);
|
|
130
|
+
await expect(
|
|
131
|
+
hasConfigurationReadGrant({
|
|
132
|
+
auth,
|
|
133
|
+
user: realUser([]),
|
|
134
|
+
configurationId: "cfg-2",
|
|
135
|
+
}),
|
|
136
|
+
).resolves.toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("fails CLOSED when the auth S2S errors", async () => {
|
|
140
|
+
const auth = authWith({
|
|
141
|
+
check: async () => {
|
|
142
|
+
throw new Error("auth down");
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
await expect(
|
|
147
|
+
hasConfigurationReadGrant({
|
|
148
|
+
auth,
|
|
149
|
+
user: realUser([]),
|
|
150
|
+
configurationId: "cfg-1",
|
|
151
|
+
}),
|
|
152
|
+
).resolves.toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("listReadableSystemIds", () => {
|
|
157
|
+
test("resolves the team-granted subset keyed on catalog.system read", async () => {
|
|
158
|
+
const auth = authWith({
|
|
159
|
+
listAccessibleObjectIds: async ({ objectType, action, objectIds }) => {
|
|
160
|
+
expect(objectType).toBe("catalog.system");
|
|
161
|
+
expect(action).toBe("read");
|
|
162
|
+
return objectIds.filter((id) => id === "sys-1");
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await expect(
|
|
167
|
+
listReadableSystemIds({
|
|
168
|
+
auth,
|
|
169
|
+
user: realUser([]),
|
|
170
|
+
allSystemIds: ["sys-1", "sys-2"],
|
|
171
|
+
}),
|
|
172
|
+
).resolves.toEqual(["sys-1"]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("the global catalog.system read and manage rules grant every system (parentScope convention)", async () => {
|
|
176
|
+
const auth = authWith({
|
|
177
|
+
listAccessibleObjectIds: async () => {
|
|
178
|
+
throw new Error("must not be called");
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await expect(
|
|
183
|
+
listReadableSystemIds({
|
|
184
|
+
auth,
|
|
185
|
+
user: realUser([QUALIFIED_SYSTEM_READ]),
|
|
186
|
+
allSystemIds: ["sys-1", "sys-2"],
|
|
187
|
+
}),
|
|
188
|
+
).resolves.toEqual(["sys-1", "sys-2"]);
|
|
189
|
+
await expect(
|
|
190
|
+
listReadableSystemIds({
|
|
191
|
+
auth,
|
|
192
|
+
user: realUser([QUALIFIED_SYSTEM_MANAGE]),
|
|
193
|
+
allSystemIds: ["sys-1", "sys-2"],
|
|
194
|
+
}),
|
|
195
|
+
).resolves.toEqual(["sys-1", "sys-2"]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("fails CLOSED when the auth S2S errors", async () => {
|
|
199
|
+
const auth = authWith({
|
|
200
|
+
listAccessibleObjectIds: async () => {
|
|
201
|
+
throw new Error("auth down");
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await expect(
|
|
206
|
+
listReadableSystemIds({
|
|
207
|
+
auth,
|
|
208
|
+
user: realUser([]),
|
|
209
|
+
allSystemIds: ["sys-1"],
|
|
210
|
+
}),
|
|
211
|
+
).resolves.toEqual([]);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe("canReadConfigurationScope", () => {
|
|
216
|
+
const configurationId = "cfg-1";
|
|
217
|
+
|
|
218
|
+
test("global configuration read allows without any S2S call or assignment lookup", async () => {
|
|
219
|
+
const auth = authWith({
|
|
220
|
+
check: async () => {
|
|
221
|
+
throw new Error("must not be called");
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
await expect(
|
|
226
|
+
canReadConfigurationScope({
|
|
227
|
+
auth,
|
|
228
|
+
user: realUser([QUALIFIED_READ]),
|
|
229
|
+
configurationId,
|
|
230
|
+
getAssignedSystemIds: async () => {
|
|
231
|
+
throw new Error("must not be called");
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
).resolves.toBe(true);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("a team grant on the CONFIGURATION allows without the assignment lookup", async () => {
|
|
238
|
+
const auth = authWith({
|
|
239
|
+
check: async () => ({ hasAccess: true }),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await expect(
|
|
243
|
+
canReadConfigurationScope({
|
|
244
|
+
auth,
|
|
245
|
+
user: realUser([]),
|
|
246
|
+
configurationId,
|
|
247
|
+
getAssignedSystemIds: async () => {
|
|
248
|
+
throw new Error("must not be called");
|
|
249
|
+
},
|
|
250
|
+
}),
|
|
251
|
+
).resolves.toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("read access to an ASSIGNED system allows even without a configuration grant", async () => {
|
|
255
|
+
const auth = authWith({
|
|
256
|
+
check: async () => ({ hasAccess: false }),
|
|
257
|
+
listAccessibleObjectIds: async ({ objectIds }) =>
|
|
258
|
+
objectIds.filter((id) => id === "sys-1"),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
await expect(
|
|
262
|
+
canReadConfigurationScope({
|
|
263
|
+
auth,
|
|
264
|
+
user: realUser([]),
|
|
265
|
+
configurationId,
|
|
266
|
+
getAssignedSystemIds: async () => ["sys-1", "sys-2"],
|
|
267
|
+
}),
|
|
268
|
+
).resolves.toBe(true);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("a system grant unrelated to the configuration's assignments denies", async () => {
|
|
272
|
+
const auth = authWith({
|
|
273
|
+
check: async () => ({ hasAccess: false }),
|
|
274
|
+
listAccessibleObjectIds: async () => [],
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
await expect(
|
|
278
|
+
canReadConfigurationScope({
|
|
279
|
+
auth,
|
|
280
|
+
user: realUser([]),
|
|
281
|
+
configurationId,
|
|
282
|
+
getAssignedSystemIds: async () => ["sys-2"],
|
|
283
|
+
}),
|
|
284
|
+
).resolves.toBe(false);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("an unassigned configuration denies a caller with only system grants", async () => {
|
|
288
|
+
const auth = authWith({
|
|
289
|
+
check: async () => ({ hasAccess: false }),
|
|
290
|
+
listAccessibleObjectIds: async () => {
|
|
291
|
+
throw new Error("must not be called for an empty id set");
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
await expect(
|
|
296
|
+
canReadConfigurationScope({
|
|
297
|
+
auth,
|
|
298
|
+
user: realUser([]),
|
|
299
|
+
configurationId,
|
|
300
|
+
getAssignedSystemIds: async () => [],
|
|
301
|
+
}),
|
|
302
|
+
).resolves.toBe(false);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("missing user denies", async () => {
|
|
306
|
+
await expect(
|
|
307
|
+
canReadConfigurationScope({
|
|
308
|
+
auth: authWith({}),
|
|
309
|
+
user: undefined,
|
|
310
|
+
configurationId,
|
|
311
|
+
getAssignedSystemIds: async () => ["sys-1"],
|
|
312
|
+
}),
|
|
313
|
+
).resolves.toBe(false);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("fails CLOSED when the assignment lookup errors", async () => {
|
|
317
|
+
const auth = authWith({
|
|
318
|
+
check: async () => ({ hasAccess: false }),
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await expect(
|
|
322
|
+
canReadConfigurationScope({
|
|
323
|
+
auth,
|
|
324
|
+
user: realUser([]),
|
|
325
|
+
configurationId,
|
|
326
|
+
getAssignedSystemIds: async () => {
|
|
327
|
+
throw new Error("db down");
|
|
328
|
+
},
|
|
329
|
+
}),
|
|
330
|
+
).resolves.toBe(false);
|
|
331
|
+
});
|
|
332
|
+
});
|