@checkstack/healthcheck-backend 1.11.1 → 1.13.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/CHANGELOG.md +327 -0
- package/package.json +29 -29
- package/src/automations.test.ts +43 -1
- package/src/automations.ts +22 -3
- package/src/history-access.test.ts +283 -0
- package/src/history-access.ts +203 -0
- package/src/hooks.ts +7 -0
- package/src/index.ts +21 -4
- package/src/queue-executor.test.ts +108 -16
- package/src/queue-executor.ts +116 -9
- package/src/router-pause-recompute.test.ts +142 -0
- package/src/router.ts +138 -6
- package/src/service-env-filter.test.ts +299 -0
- package/src/service-paused-filter.test.ts +391 -0
- package/src/service-rollup-worst-wins.test.ts +205 -0
- package/src/service.ts +320 -28
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { AuthService, AuthUser } from "@checkstack/backend-api";
|
|
3
|
+
import {
|
|
4
|
+
canReadRunScope,
|
|
5
|
+
hasGlobalHistoryAccess,
|
|
6
|
+
listManageableSystemIds,
|
|
7
|
+
listTeamManageableConfigurationIds,
|
|
8
|
+
resolveHistoryScope,
|
|
9
|
+
} from "./history-access";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Regression guard for the run-history authorization: global
|
|
13
|
+
* `configuration.manage` (or wildcard/service) yields unrestricted access, a
|
|
14
|
+
* team-scoped caller is restricted to runs of their granted CONFIGURATIONS
|
|
15
|
+
* plus all runs of the SYSTEMS they manage (a system's owning team sees every
|
|
16
|
+
* run of that system, whoever owns the configuration), anyone else is
|
|
17
|
+
* forbidden, and any auth (S2S) failure fails CLOSED.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const QUALIFIED_MANAGE = "healthcheck.healthcheck.manage";
|
|
21
|
+
const QUALIFIED_SYSTEM_MANAGE = "catalog.system.manage";
|
|
22
|
+
|
|
23
|
+
const realUser = (accessRules: string[]): AuthUser => ({
|
|
24
|
+
type: "user",
|
|
25
|
+
id: "u1",
|
|
26
|
+
accessRules,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Cast is unavoidable for a partial mock: the helpers only touch
|
|
30
|
+
// `listAccessibleObjectIds` / `check`, and implementing the full AuthService
|
|
31
|
+
// here would just be noise.
|
|
32
|
+
const authWith = (impl: Partial<AuthService>): AuthService =>
|
|
33
|
+
impl as unknown as AuthService;
|
|
34
|
+
|
|
35
|
+
describe("hasGlobalHistoryAccess", () => {
|
|
36
|
+
test("true for global manage, wildcard, and services; false otherwise", () => {
|
|
37
|
+
expect(hasGlobalHistoryAccess(realUser([QUALIFIED_MANAGE]))).toBe(true);
|
|
38
|
+
expect(hasGlobalHistoryAccess(realUser(["*"]))).toBe(true);
|
|
39
|
+
expect(hasGlobalHistoryAccess({ type: "service", pluginId: "slo" })).toBe(
|
|
40
|
+
true,
|
|
41
|
+
);
|
|
42
|
+
expect(hasGlobalHistoryAccess(realUser(["healthcheck.healthcheck.read"]))).toBe(
|
|
43
|
+
false,
|
|
44
|
+
);
|
|
45
|
+
expect(hasGlobalHistoryAccess(undefined)).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("resolveHistoryScope", () => {
|
|
50
|
+
test("global manage rule yields the unfiltered feed", () => {
|
|
51
|
+
expect(
|
|
52
|
+
resolveHistoryScope({
|
|
53
|
+
user: realUser([QUALIFIED_MANAGE]),
|
|
54
|
+
accessibleConfigurationIds: [],
|
|
55
|
+
accessibleSystemIds: [],
|
|
56
|
+
}),
|
|
57
|
+
).toEqual({ kind: "all" });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("configuration grants scope the feed", () => {
|
|
61
|
+
expect(
|
|
62
|
+
resolveHistoryScope({
|
|
63
|
+
user: realUser([]),
|
|
64
|
+
accessibleConfigurationIds: ["cfg-1"],
|
|
65
|
+
accessibleSystemIds: [],
|
|
66
|
+
}),
|
|
67
|
+
).toEqual({ kind: "scoped", configurationIds: ["cfg-1"], systemIds: [] });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("system manage alone scopes the feed (owning team sees its runs)", () => {
|
|
71
|
+
expect(
|
|
72
|
+
resolveHistoryScope({
|
|
73
|
+
user: realUser([]),
|
|
74
|
+
accessibleConfigurationIds: [],
|
|
75
|
+
accessibleSystemIds: ["sys-1"],
|
|
76
|
+
}),
|
|
77
|
+
).toEqual({ kind: "scoped", configurationIds: [], systemIds: ["sys-1"] });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("no global rule and no grant of either kind is forbidden", () => {
|
|
81
|
+
expect(
|
|
82
|
+
resolveHistoryScope({
|
|
83
|
+
user: realUser(["healthcheck.healthcheck.read"]),
|
|
84
|
+
accessibleConfigurationIds: [],
|
|
85
|
+
accessibleSystemIds: [],
|
|
86
|
+
}),
|
|
87
|
+
).toEqual({ kind: "forbidden" });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("missing user is forbidden", () => {
|
|
91
|
+
expect(
|
|
92
|
+
resolveHistoryScope({
|
|
93
|
+
user: undefined,
|
|
94
|
+
accessibleConfigurationIds: [],
|
|
95
|
+
accessibleSystemIds: [],
|
|
96
|
+
}),
|
|
97
|
+
).toEqual({ kind: "forbidden" });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("a manage rule from ANOTHER plugin does not unlock the feed", () => {
|
|
101
|
+
expect(
|
|
102
|
+
resolveHistoryScope({
|
|
103
|
+
user: realUser(["incident.incident.manage"]),
|
|
104
|
+
accessibleConfigurationIds: [],
|
|
105
|
+
accessibleSystemIds: [],
|
|
106
|
+
}),
|
|
107
|
+
).toEqual({ kind: "forbidden" });
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("listTeamManageableConfigurationIds", () => {
|
|
112
|
+
test("resolves the manage-granted subset via the auth S2S", async () => {
|
|
113
|
+
const auth = authWith({
|
|
114
|
+
listAccessibleObjectIds: async ({ objectType, action, objectIds }) => {
|
|
115
|
+
expect(objectType).toBe("healthcheck.healthcheck");
|
|
116
|
+
expect(action).toBe("manage");
|
|
117
|
+
return objectIds.filter((id) => id === "cfg-2");
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await expect(
|
|
122
|
+
listTeamManageableConfigurationIds({
|
|
123
|
+
auth,
|
|
124
|
+
user: realUser([]),
|
|
125
|
+
allConfigurationIds: ["cfg-1", "cfg-2"],
|
|
126
|
+
}),
|
|
127
|
+
).resolves.toEqual(["cfg-2"]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("fails CLOSED when the auth S2S errors", async () => {
|
|
131
|
+
const auth = authWith({
|
|
132
|
+
listAccessibleObjectIds: async () => {
|
|
133
|
+
throw new Error("auth down");
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await expect(
|
|
138
|
+
listTeamManageableConfigurationIds({
|
|
139
|
+
auth,
|
|
140
|
+
user: realUser([]),
|
|
141
|
+
allConfigurationIds: ["cfg-1"],
|
|
142
|
+
}),
|
|
143
|
+
).resolves.toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("listManageableSystemIds", () => {
|
|
148
|
+
test("resolves the team-granted subset keyed on catalog.system manage", async () => {
|
|
149
|
+
const auth = authWith({
|
|
150
|
+
listAccessibleObjectIds: async ({ objectType, action, objectIds }) => {
|
|
151
|
+
expect(objectType).toBe("catalog.system");
|
|
152
|
+
expect(action).toBe("manage");
|
|
153
|
+
return objectIds.filter((id) => id === "sys-1");
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await expect(
|
|
158
|
+
listManageableSystemIds({
|
|
159
|
+
auth,
|
|
160
|
+
user: realUser([]),
|
|
161
|
+
allSystemIds: ["sys-1", "sys-2"],
|
|
162
|
+
}),
|
|
163
|
+
).resolves.toEqual(["sys-1"]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("the global catalog.system.manage rule grants every system (parentScope convention)", async () => {
|
|
167
|
+
const auth = authWith({
|
|
168
|
+
listAccessibleObjectIds: async () => {
|
|
169
|
+
throw new Error("must not be called");
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await expect(
|
|
174
|
+
listManageableSystemIds({
|
|
175
|
+
auth,
|
|
176
|
+
user: realUser([QUALIFIED_SYSTEM_MANAGE]),
|
|
177
|
+
allSystemIds: ["sys-1", "sys-2"],
|
|
178
|
+
}),
|
|
179
|
+
).resolves.toEqual(["sys-1", "sys-2"]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("fails CLOSED when the auth S2S errors", async () => {
|
|
183
|
+
const auth = authWith({
|
|
184
|
+
listAccessibleObjectIds: async () => {
|
|
185
|
+
throw new Error("auth down");
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await expect(
|
|
190
|
+
listManageableSystemIds({
|
|
191
|
+
auth,
|
|
192
|
+
user: realUser([]),
|
|
193
|
+
allSystemIds: ["sys-1"],
|
|
194
|
+
}),
|
|
195
|
+
).resolves.toEqual([]);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("canReadRunScope", () => {
|
|
200
|
+
const scope = { configurationId: "cfg-1", systemId: "sys-1" };
|
|
201
|
+
|
|
202
|
+
test("global healthcheck manage allows without any S2S call", async () => {
|
|
203
|
+
const auth = authWith({
|
|
204
|
+
check: async () => {
|
|
205
|
+
throw new Error("must not be called");
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
await expect(
|
|
210
|
+
canReadRunScope({ auth, user: realUser([QUALIFIED_MANAGE]), ...scope }),
|
|
211
|
+
).resolves.toBe(true);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("a team grant on the CONFIGURATION allows", async () => {
|
|
215
|
+
const auth = authWith({
|
|
216
|
+
check: async ({ objectType, objectId }) => ({
|
|
217
|
+
hasAccess:
|
|
218
|
+
objectType === "healthcheck.healthcheck" && objectId === "cfg-1",
|
|
219
|
+
}),
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await expect(
|
|
223
|
+
canReadRunScope({ auth, user: realUser([]), ...scope }),
|
|
224
|
+
).resolves.toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("a team grant on the SYSTEM allows even without a configuration grant", async () => {
|
|
228
|
+
const auth = authWith({
|
|
229
|
+
check: async ({ objectType, objectId }) => ({
|
|
230
|
+
hasAccess: objectType === "catalog.system" && objectId === "sys-1",
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
await expect(
|
|
235
|
+
canReadRunScope({ auth, user: realUser([]), ...scope }),
|
|
236
|
+
).resolves.toBe(true);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("the global catalog.system.manage rule allows (parentScope convention)", async () => {
|
|
240
|
+
const auth = authWith({
|
|
241
|
+
check: async () => {
|
|
242
|
+
throw new Error("must not be called");
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
await expect(
|
|
247
|
+
canReadRunScope({
|
|
248
|
+
auth,
|
|
249
|
+
user: realUser([QUALIFIED_SYSTEM_MANAGE]),
|
|
250
|
+
...scope,
|
|
251
|
+
}),
|
|
252
|
+
).resolves.toBe(true);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("no grant of either kind denies", async () => {
|
|
256
|
+
const auth = authWith({
|
|
257
|
+
check: async () => ({ hasAccess: false }),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await expect(
|
|
261
|
+
canReadRunScope({ auth, user: realUser([]), ...scope }),
|
|
262
|
+
).resolves.toBe(false);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("missing user denies", async () => {
|
|
266
|
+
const auth = authWith({});
|
|
267
|
+
await expect(
|
|
268
|
+
canReadRunScope({ auth, user: undefined, ...scope }),
|
|
269
|
+
).resolves.toBe(false);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("fails CLOSED when the auth S2S errors", async () => {
|
|
273
|
+
const auth = authWith({
|
|
274
|
+
check: async () => {
|
|
275
|
+
throw new Error("auth down");
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
await expect(
|
|
280
|
+
canReadRunScope({ auth, user: realUser([]), ...scope }),
|
|
281
|
+
).resolves.toBe(false);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { qualifyAccessRuleId } from "@checkstack/common";
|
|
2
|
+
import type { AuthUser, AuthService } from "@checkstack/backend-api";
|
|
3
|
+
import {
|
|
4
|
+
healthCheckAccess,
|
|
5
|
+
healthCheckResourceTypes,
|
|
6
|
+
pluginMetadata,
|
|
7
|
+
} from "@checkstack/healthcheck-common";
|
|
8
|
+
import { catalogResourceTypes } from "@checkstack/catalog-common";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The qualified global rule that unlocks the UNFILTERED run-history feed.
|
|
12
|
+
* Kept in lock-step with the history procs' contract docs: their `access` is
|
|
13
|
+
* deliberately empty (the OR below is not expressible declaratively), so THIS
|
|
14
|
+
* module is the authorization for them.
|
|
15
|
+
*/
|
|
16
|
+
const QUALIFIED_MANAGE_RULE = qualifyAccessRuleId(
|
|
17
|
+
pluginMetadata,
|
|
18
|
+
healthCheckAccess.configuration.manage,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The parent (catalog.system) global manage rule. Follows the middleware's
|
|
23
|
+
* parentScope convention: authorizing via a parent consults the parent's own
|
|
24
|
+
* grants AND its global `{resourceType}.{action}` rule.
|
|
25
|
+
*/
|
|
26
|
+
const QUALIFIED_SYSTEM_MANAGE_RULE = `${catalogResourceTypes.system}.manage`;
|
|
27
|
+
|
|
28
|
+
/** A user/application principal (the only kinds that can hold team grants). */
|
|
29
|
+
type GrantHolder = AuthUser & { type: "user" | "application" };
|
|
30
|
+
|
|
31
|
+
const isGrantHolder = (user: AuthUser): user is GrantHolder =>
|
|
32
|
+
user.type === "user" || user.type === "application";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Does the caller hold UNRESTRICTED run-history access? True for wildcard
|
|
36
|
+
* admins, global `healthcheck.healthcheck.manage` holders, and trusted
|
|
37
|
+
* services (same stance as the middleware).
|
|
38
|
+
*/
|
|
39
|
+
export function hasGlobalHistoryAccess(user: AuthUser | undefined): boolean {
|
|
40
|
+
if (!user) return false;
|
|
41
|
+
if (user.type === "service") return true;
|
|
42
|
+
const rules = user.accessRules ?? [];
|
|
43
|
+
return rules.includes("*") || rules.includes(QUALIFIED_MANAGE_RULE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The outcome of authorizing the run-history FEED for the current caller. */
|
|
47
|
+
export type HistoryScope =
|
|
48
|
+
/** Global manage (or wildcard/service): the caller sees every run. */
|
|
49
|
+
| { kind: "all" }
|
|
50
|
+
/**
|
|
51
|
+
* Team-scoped caller: restrict rows to runs of these configurations OR runs
|
|
52
|
+
* belonging to these systems (a system's owning team sees ALL of its runs,
|
|
53
|
+
* regardless of who owns the configuration).
|
|
54
|
+
*/
|
|
55
|
+
| { kind: "scoped"; configurationIds: string[]; systemIds: string[] }
|
|
56
|
+
/** No global rule and no team grant of either kind: the read is forbidden. */
|
|
57
|
+
| { kind: "forbidden" };
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Pure decision half of the feed authorization: given the caller and their
|
|
61
|
+
* team-accessible configuration/system ids, decide the feed scope. Split from
|
|
62
|
+
* the S2S lookups so the branching is unit-testable without an auth service.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveHistoryScope({
|
|
65
|
+
user,
|
|
66
|
+
accessibleConfigurationIds,
|
|
67
|
+
accessibleSystemIds,
|
|
68
|
+
}: {
|
|
69
|
+
user: AuthUser | undefined;
|
|
70
|
+
accessibleConfigurationIds: string[];
|
|
71
|
+
accessibleSystemIds: string[];
|
|
72
|
+
}): HistoryScope {
|
|
73
|
+
if (!user) return { kind: "forbidden" };
|
|
74
|
+
if (hasGlobalHistoryAccess(user)) return { kind: "all" };
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
accessibleConfigurationIds.length === 0 &&
|
|
78
|
+
accessibleSystemIds.length === 0
|
|
79
|
+
) {
|
|
80
|
+
return { kind: "forbidden" };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
kind: "scoped",
|
|
84
|
+
configurationIds: accessibleConfigurationIds,
|
|
85
|
+
systemIds: accessibleSystemIds,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolve which of `allConfigurationIds` the caller may MANAGE via team
|
|
91
|
+
* grants on the CONFIGURATION itself. FAILS CLOSED: an S2S error yields an
|
|
92
|
+
* empty set, i.e. narrower access, never wider.
|
|
93
|
+
*/
|
|
94
|
+
export async function listTeamManageableConfigurationIds({
|
|
95
|
+
auth,
|
|
96
|
+
user,
|
|
97
|
+
allConfigurationIds,
|
|
98
|
+
}: {
|
|
99
|
+
auth: AuthService;
|
|
100
|
+
user: AuthUser;
|
|
101
|
+
allConfigurationIds: string[];
|
|
102
|
+
}): Promise<string[]> {
|
|
103
|
+
if (!isGrantHolder(user)) return [];
|
|
104
|
+
if (allConfigurationIds.length === 0) return [];
|
|
105
|
+
try {
|
|
106
|
+
return await auth.listAccessibleObjectIds({
|
|
107
|
+
userId: user.id,
|
|
108
|
+
userType: user.type,
|
|
109
|
+
objectType: healthCheckResourceTypes.configuration,
|
|
110
|
+
objectIds: allConfigurationIds,
|
|
111
|
+
action: "manage",
|
|
112
|
+
hasGlobalAccess: false,
|
|
113
|
+
});
|
|
114
|
+
} catch {
|
|
115
|
+
// SECURITY: fail closed - an auth outage must not widen the feed.
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve which of `allSystemIds` the caller may MANAGE - via a team grant on
|
|
122
|
+
* the system, or via the global `catalog.system.manage` rule (the parentScope
|
|
123
|
+
* convention). A system's owning team sees every run of that system, even for
|
|
124
|
+
* configurations owned elsewhere. FAILS CLOSED on S2S errors.
|
|
125
|
+
*/
|
|
126
|
+
export async function listManageableSystemIds({
|
|
127
|
+
auth,
|
|
128
|
+
user,
|
|
129
|
+
allSystemIds,
|
|
130
|
+
}: {
|
|
131
|
+
auth: AuthService;
|
|
132
|
+
user: AuthUser;
|
|
133
|
+
allSystemIds: string[];
|
|
134
|
+
}): Promise<string[]> {
|
|
135
|
+
if (!isGrantHolder(user)) return [];
|
|
136
|
+
if (allSystemIds.length === 0) return [];
|
|
137
|
+
const rules = user.accessRules ?? [];
|
|
138
|
+
if (rules.includes(QUALIFIED_SYSTEM_MANAGE_RULE)) return allSystemIds;
|
|
139
|
+
try {
|
|
140
|
+
return await auth.listAccessibleObjectIds({
|
|
141
|
+
userId: user.id,
|
|
142
|
+
userType: user.type,
|
|
143
|
+
objectType: catalogResourceTypes.system,
|
|
144
|
+
objectIds: allSystemIds,
|
|
145
|
+
action: "manage",
|
|
146
|
+
hasGlobalAccess: false,
|
|
147
|
+
});
|
|
148
|
+
} catch {
|
|
149
|
+
// SECURITY: fail closed - an auth outage must not widen the feed.
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Per-object check for the single-configuration history surfaces
|
|
156
|
+
* (`getRunById`, `getDetailedAggregatedHistory`): the caller may read runs of
|
|
157
|
+
* (`configurationId`, `systemId`) iff they hold global history access, a team
|
|
158
|
+
* manage grant on the CONFIGURATION, or manage access to the SYSTEM (team
|
|
159
|
+
* grant or global catalog rule). FAILS CLOSED on S2S errors.
|
|
160
|
+
*/
|
|
161
|
+
export async function canReadRunScope({
|
|
162
|
+
auth,
|
|
163
|
+
user,
|
|
164
|
+
configurationId,
|
|
165
|
+
systemId,
|
|
166
|
+
}: {
|
|
167
|
+
auth: AuthService;
|
|
168
|
+
user: AuthUser | undefined;
|
|
169
|
+
configurationId: string;
|
|
170
|
+
systemId: string;
|
|
171
|
+
}): Promise<boolean> {
|
|
172
|
+
if (!user) return false;
|
|
173
|
+
if (hasGlobalHistoryAccess(user)) return true;
|
|
174
|
+
if (!isGrantHolder(user)) return false;
|
|
175
|
+
|
|
176
|
+
const rules = user.accessRules ?? [];
|
|
177
|
+
if (rules.includes(QUALIFIED_SYSTEM_MANAGE_RULE)) return true;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const configCheck = await auth.check({
|
|
181
|
+
userId: user.id,
|
|
182
|
+
userType: user.type,
|
|
183
|
+
objectType: healthCheckResourceTypes.configuration,
|
|
184
|
+
objectId: configurationId,
|
|
185
|
+
action: "manage",
|
|
186
|
+
hasGlobalAccess: false,
|
|
187
|
+
});
|
|
188
|
+
if (configCheck.hasAccess) return true;
|
|
189
|
+
|
|
190
|
+
const systemCheck = await auth.check({
|
|
191
|
+
userId: user.id,
|
|
192
|
+
userType: user.type,
|
|
193
|
+
objectType: catalogResourceTypes.system,
|
|
194
|
+
objectId: systemId,
|
|
195
|
+
action: "manage",
|
|
196
|
+
hasGlobalAccess: false,
|
|
197
|
+
});
|
|
198
|
+
return systemCheck.hasAccess;
|
|
199
|
+
} catch {
|
|
200
|
+
// SECURITY: fail closed - an auth outage must not widen access.
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
package/src/hooks.ts
CHANGED
|
@@ -46,6 +46,12 @@ export const healthCheckHooks = {
|
|
|
46
46
|
latencyMs: number | undefined;
|
|
47
47
|
result: Record<string, unknown> | undefined;
|
|
48
48
|
timestamp: string;
|
|
49
|
+
/**
|
|
50
|
+
* Environment the run was executed for. null = the env-less slice (no
|
|
51
|
+
* environment membership). Forwarded by the anomaly plugin so its inline
|
|
52
|
+
* detector resolves the per-env baseline rather than a cross-env one.
|
|
53
|
+
*/
|
|
54
|
+
environmentId: string | null;
|
|
49
55
|
}>("healthcheck.check.completed"),
|
|
50
56
|
|
|
51
57
|
/**
|
|
@@ -64,5 +70,6 @@ export const healthCheckHooks = {
|
|
|
64
70
|
latencyMs: number | undefined;
|
|
65
71
|
result: Record<string, unknown> | undefined;
|
|
66
72
|
timestamp: string;
|
|
73
|
+
environmentId: string | null;
|
|
67
74
|
}>("healthcheck.check.failed"),
|
|
68
75
|
} as const;
|
package/src/index.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
setupHealthCheckWorker,
|
|
3
3
|
bootstrapHealthChecks,
|
|
4
|
+
recomputeSystemRollupHealth,
|
|
4
5
|
} from "./queue-executor";
|
|
5
6
|
import { setupRetentionJob } from "./retention-job";
|
|
6
7
|
import * as schema from "./schema";
|
|
7
8
|
import {
|
|
8
9
|
healthCheckAccessRules,
|
|
9
10
|
healthCheckAccess,
|
|
11
|
+
healthCheckResourceTypes,
|
|
10
12
|
pluginMetadata,
|
|
11
13
|
healthCheckContract,
|
|
12
14
|
healthcheckRoutes,
|
|
@@ -242,10 +244,14 @@ export default createBackendPlugin({
|
|
|
242
244
|
const typedDb = database as SafeDatabase<typeof schema>;
|
|
243
245
|
|
|
244
246
|
// Resolve/search health-check configurations by name for the Teams admin
|
|
245
|
-
// UI (team grants are stored as opaque
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
|
|
247
|
+
// UI (team grants are stored as opaque `<type>:<configId>` rows, where
|
|
248
|
+
// <type> is `healthCheckResourceTypes.configuration` — i.e.
|
|
249
|
+
// `healthcheck.healthcheck`, the key the RPC middleware derives from the
|
|
250
|
+
// configuration access rule's resource). This MUST match that grant key,
|
|
251
|
+
// or grant names never resolve. Lets the auth backend render grants by
|
|
252
|
+
// name and power the grant picker without depending on healthcheck
|
|
253
|
+
// internals.
|
|
254
|
+
resourceResolverRegistry.register(healthCheckResourceTypes.configuration, {
|
|
249
255
|
resolveNames: async (ids) => {
|
|
250
256
|
if (ids.length === 0) return new Map();
|
|
251
257
|
const rows = await typedDb
|
|
@@ -480,6 +486,17 @@ export default createBackendPlugin({
|
|
|
480
486
|
maintenanceClient,
|
|
481
487
|
logger,
|
|
482
488
|
signalService,
|
|
489
|
+
recomputeSystemRollupHealth: (systemId) =>
|
|
490
|
+
recomputeSystemRollupHealth({
|
|
491
|
+
systemId,
|
|
492
|
+
// Reuse the COMPUTE-ON-READ service instance bound to the
|
|
493
|
+
// `health` entity read accessor — it's the same db/registry
|
|
494
|
+
// the rollup write inside `executeHealthCheckJob` uses.
|
|
495
|
+
service,
|
|
496
|
+
getHealthEntity: () => healthEntity,
|
|
497
|
+
advisoryLock,
|
|
498
|
+
logger,
|
|
499
|
+
}),
|
|
483
500
|
});
|
|
484
501
|
rpc.registerRouter(healthCheckRouter, healthCheckContract);
|
|
485
502
|
|