@checkstack/dependency-common 1.2.3 → 1.2.5
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 +82 -0
- package/package.json +7 -7
- package/src/access.ts +26 -3
- package/src/rpc-contract.ts +8 -4
- package/tests/access.test.ts +58 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,87 @@
|
|
|
1
1
|
# @checkstack/dependency-common
|
|
2
2
|
|
|
3
|
+
## 1.2.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f9cfdae: fix(dependency): gate the dependency map behind its own non-public access rule
|
|
8
|
+
|
|
9
|
+
Anonymous users could see the "Dependency Map" nav entry and open the page
|
|
10
|
+
(which then rendered empty) because the map was gated by `dependency.read`,
|
|
11
|
+
which is public so that dependency _warning_ badges stay visible on the
|
|
12
|
+
catalog and dashboard.
|
|
13
|
+
|
|
14
|
+
The full topology map is now gated by a dedicated `dependency.map` access
|
|
15
|
+
rule that is granted to authenticated users by default but is NOT public, so
|
|
16
|
+
anonymous visitors no longer see the nav entry or reach the page. The
|
|
17
|
+
`getAllDependencies`, `getNodePositions`, and `saveNodePositions` endpoints
|
|
18
|
+
move to this rule too, and the dashboard dependency signal now renders as
|
|
19
|
+
plain text (not a map link) for users without map access. Per-system
|
|
20
|
+
dependency warnings stay on the public `dependency.read` rule, so warning
|
|
21
|
+
badges/alerts/signals remain visible to everyone as before.
|
|
22
|
+
|
|
23
|
+
Admins can still grant `dependency.map` to the anonymous role to make the
|
|
24
|
+
map public again.
|
|
25
|
+
|
|
26
|
+
Note: the default-rule sync is add-only, so on existing deployments the
|
|
27
|
+
anonymous role keeps any rules already granted. Since `dependency.map` is a
|
|
28
|
+
brand-new rule the anonymous role never had it, so the map is hidden from
|
|
29
|
+
anonymous users immediately after upgrade with no admin action required.
|
|
30
|
+
|
|
31
|
+
## 1.2.4
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- 56e7c75: Fix frontend access checks to use FULLY-QUALIFIED access-rule ids, and resolve
|
|
36
|
+
the anonymous role on the frontend.
|
|
37
|
+
|
|
38
|
+
Granted access-rule ids are stored fully-qualified as `{pluginId}.{ruleId}` (e.g.
|
|
39
|
+
`incident.incident.read`) so two plugins defining the same short rule id never
|
|
40
|
+
collide. The frontend, however, was checking the UNqualified id (`incident.read`)
|
|
41
|
+
via `isAccessRuleSatisfied`, so every check failed for any user without the `*`
|
|
42
|
+
(admin) grant - masked in development because dev-auth grants `*`. This silently
|
|
43
|
+
broke ALL non-admin frontend gating (route guards, sidebar entries, and
|
|
44
|
+
`useAccess`-based button/link gating).
|
|
45
|
+
|
|
46
|
+
- **`@checkstack/common`**: `AccessRule` now carries a REQUIRED owning `pluginId`;
|
|
47
|
+
`access()` / `accessPair()` require and stamp it; `isAccessRuleSatisfied`
|
|
48
|
+
qualifies the rule (`{pluginId}.{id}`, plus the manage->read escalation) and
|
|
49
|
+
matches ONLY the qualified form. There is intentionally NO unqualified fallback
|
|
50
|
+
- matching a bare id would let one plugin's grant satisfy another plugin's
|
|
51
|
+
identically-named rule (a cross-plugin privilege-escalation flaw). Every plugin
|
|
52
|
+
that defines access rules now passes its own `pluginId`.
|
|
53
|
+
- **`@checkstack/backend`**: `pluginManager.getAllAccessRules()` no longer strips
|
|
54
|
+
the `pluginId` field (the rule `id` is already fully-qualified for the DB sync).
|
|
55
|
+
- **Route guard** (`@checkstack/frontend` / `@checkstack/frontend-api`) now
|
|
56
|
+
checks the FULL rule object (so it qualifies and escalates), not a bare id.
|
|
57
|
+
- **Anonymous role on the frontend**: the `accessRules` procedure is now
|
|
58
|
+
`public`, returning the configurable anonymous role's grants to unauthenticated
|
|
59
|
+
callers; `useAccessRules` fetches them for guests instead of returning an empty
|
|
60
|
+
set. So anonymous UI now reflects exactly what the anonymous role is allowed -
|
|
61
|
+
which an admin can change (`isPublic` is only the seeded default).
|
|
62
|
+
- Incident / maintenance / SLO detail routes are now read-gated (their read rule
|
|
63
|
+
is an `isPublic` default, so the anonymous role holds it unless an admin
|
|
64
|
+
revokes it); their dashboard status signals carry that rule and render as a
|
|
65
|
+
link only when the viewer may open it.
|
|
66
|
+
|
|
67
|
+
**BREAKING (`@checkstack/common`):** `AccessRule.pluginId` is now REQUIRED, and
|
|
68
|
+
`access()` / `accessPair()` require a `pluginId` option. `isAccessRuleSatisfied`
|
|
69
|
+
matches ONLY the fully-qualified `{pluginId}.{ruleId}` form - the previous
|
|
70
|
+
unqualified fallback is removed, because it was a cross-plugin
|
|
71
|
+
privilege-escalation flaw. Any code constructing an `AccessRule` or calling
|
|
72
|
+
`access()`/`accessPair()` must supply the owning `pluginId`.
|
|
73
|
+
|
|
74
|
+
Verified live against an anonymous caller: read pages resolve (qualified match),
|
|
75
|
+
manage actions are denied, manage->read escalation and `*` still work.
|
|
76
|
+
|
|
77
|
+
- Updated dependencies [56e7c75]
|
|
78
|
+
- Updated dependencies [56e7c75]
|
|
79
|
+
- @checkstack/frontend-api@0.9.0
|
|
80
|
+
- @checkstack/catalog-common@2.3.4
|
|
81
|
+
- @checkstack/common@0.15.0
|
|
82
|
+
- @checkstack/notification-common@1.3.3
|
|
83
|
+
- @checkstack/signal-common@0.2.9
|
|
84
|
+
|
|
3
85
|
## 1.2.3
|
|
4
86
|
|
|
5
87
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/dependency-common",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -9,18 +9,18 @@
|
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@checkstack/common": "0.
|
|
13
|
-
"@checkstack/catalog-common": "2.3.
|
|
14
|
-
"@checkstack/frontend-api": "0.
|
|
15
|
-
"@checkstack/notification-common": "1.3.
|
|
16
|
-
"@checkstack/signal-common": "0.2.
|
|
12
|
+
"@checkstack/common": "0.15.0",
|
|
13
|
+
"@checkstack/catalog-common": "2.3.4",
|
|
14
|
+
"@checkstack/frontend-api": "0.9.0",
|
|
15
|
+
"@checkstack/notification-common": "1.3.3",
|
|
16
|
+
"@checkstack/signal-common": "0.2.9",
|
|
17
17
|
"@orpc/contract": "^1.14.4",
|
|
18
18
|
"zod": "^4.2.1"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"typescript": "^5.7.2",
|
|
22
22
|
"@checkstack/tsconfig": "0.0.7",
|
|
23
|
-
"@checkstack/scripts": "0.6.
|
|
23
|
+
"@checkstack/scripts": "0.6.1"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
26
|
"typecheck": "tsgo -b",
|
package/src/access.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { accessPair } from "@checkstack/common";
|
|
1
|
+
import { access, accessPair } from "@checkstack/common";
|
|
2
|
+
import { pluginMetadata } from "./plugin-metadata";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Access rules for the Dependency plugin.
|
|
@@ -6,13 +7,17 @@ import { accessPair } from "@checkstack/common";
|
|
|
6
7
|
export const dependencyAccess = {
|
|
7
8
|
/**
|
|
8
9
|
* Dependency access with both read and manage levels.
|
|
9
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* Read is public by default so unauthenticated visitors can still see the
|
|
12
|
+
* dependency *warning* badges/alerts/signals on the catalog and dashboard.
|
|
13
|
+
* It does NOT grant the full topology map - that is gated separately by
|
|
14
|
+
* `dependencyAccess.map` (below).
|
|
10
15
|
*/
|
|
11
16
|
dependency: accessPair(
|
|
12
17
|
"dependency",
|
|
13
18
|
{
|
|
14
19
|
read: {
|
|
15
|
-
description: "View
|
|
20
|
+
description: "View dependency warnings on systems and the dashboard",
|
|
16
21
|
isDefault: true,
|
|
17
22
|
isPublic: true,
|
|
18
23
|
},
|
|
@@ -23,8 +28,25 @@ export const dependencyAccess = {
|
|
|
23
28
|
},
|
|
24
29
|
{
|
|
25
30
|
idParam: "systemId",
|
|
31
|
+
pluginId: pluginMetadata.pluginId,
|
|
26
32
|
},
|
|
27
33
|
),
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Access to the dependency map (the full system-topology graph) - its nav
|
|
37
|
+
* entry, page, and full-graph/canvas endpoints.
|
|
38
|
+
*
|
|
39
|
+
* Deliberately NOT public: the map exposes the entire system topology, so
|
|
40
|
+
* anonymous visitors must not get it by default. Authenticated users get it
|
|
41
|
+
* by default (`isDefault`). Per-system dependency warnings stay on the public
|
|
42
|
+
* `dependency.read` rule, so withholding the map does not hide warning badges.
|
|
43
|
+
* Admins can still grant this rule to the anonymous role to make the map
|
|
44
|
+
* public again.
|
|
45
|
+
*/
|
|
46
|
+
map: access("map", "read", "View the dependency map (full system topology)", {
|
|
47
|
+
isDefault: true,
|
|
48
|
+
pluginId: pluginMetadata.pluginId,
|
|
49
|
+
}),
|
|
28
50
|
};
|
|
29
51
|
|
|
30
52
|
/**
|
|
@@ -33,4 +55,5 @@ export const dependencyAccess = {
|
|
|
33
55
|
export const dependencyAccessRules = [
|
|
34
56
|
dependencyAccess.dependency.read,
|
|
35
57
|
dependencyAccess.dependency.manage,
|
|
58
|
+
dependencyAccess.map,
|
|
36
59
|
];
|
package/src/rpc-contract.ts
CHANGED
|
@@ -33,11 +33,14 @@ export const dependencyContract = {
|
|
|
33
33
|
)
|
|
34
34
|
.output(z.object({ dependencies: z.array(DependencySchema) })),
|
|
35
35
|
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Get the full dependency graph (all dependencies, for the canvas).
|
|
38
|
+
* Gated by the map rule - the full topology is map-only, not public.
|
|
39
|
+
*/
|
|
37
40
|
getAllDependencies: proc({
|
|
38
41
|
operationType: "query",
|
|
39
42
|
userType: "public",
|
|
40
|
-
access: [dependencyAccess.
|
|
43
|
+
access: [dependencyAccess.map],
|
|
41
44
|
}).output(z.object({ dependencies: z.array(DependencySchema) })),
|
|
42
45
|
|
|
43
46
|
/** Bulk-fetch derived warnings for multiple systems (for dashboard badges) */
|
|
@@ -101,20 +104,21 @@ export const dependencyContract = {
|
|
|
101
104
|
|
|
102
105
|
// ==========================================================================
|
|
103
106
|
// NODE POSITIONS (authenticated - per-user canvas layout persistence)
|
|
107
|
+
// Gated by the map rule - canvas layout only matters to map viewers.
|
|
104
108
|
// ==========================================================================
|
|
105
109
|
|
|
106
110
|
/** Get saved node positions for the dependency map canvas */
|
|
107
111
|
getNodePositions: proc({
|
|
108
112
|
operationType: "query",
|
|
109
113
|
userType: "user",
|
|
110
|
-
access: [dependencyAccess.
|
|
114
|
+
access: [dependencyAccess.map],
|
|
111
115
|
}).output(z.object({ positions: z.array(NodePositionSchema) })),
|
|
112
116
|
|
|
113
117
|
/** Save node positions for the dependency map canvas */
|
|
114
118
|
saveNodePositions: proc({
|
|
115
119
|
operationType: "mutation",
|
|
116
120
|
userType: "user",
|
|
117
|
-
access: [dependencyAccess.
|
|
121
|
+
access: [dependencyAccess.map],
|
|
118
122
|
})
|
|
119
123
|
.input(z.object({ positions: z.array(NodePositionSchema) }))
|
|
120
124
|
.output(z.object({ success: z.boolean() })),
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { isAccessRuleSatisfied } from "@checkstack/common";
|
|
3
|
+
import {
|
|
4
|
+
dependencyAccess,
|
|
5
|
+
dependencyAccessRules,
|
|
6
|
+
} from "@checkstack/dependency-common";
|
|
7
|
+
|
|
8
|
+
const QUALIFIED_MAP = "dependency.map.read";
|
|
9
|
+
const QUALIFIED_READ = "dependency.dependency.read";
|
|
10
|
+
|
|
11
|
+
describe("dependency-common access rules", () => {
|
|
12
|
+
describe("dependency.map", () => {
|
|
13
|
+
test("is its own resource/level with the dependency pluginId", () => {
|
|
14
|
+
expect(dependencyAccess.map.id).toBe("map.read");
|
|
15
|
+
expect(dependencyAccess.map.resource).toBe("map");
|
|
16
|
+
expect(dependencyAccess.map.level).toBe("read");
|
|
17
|
+
expect(dependencyAccess.map.pluginId).toBe("dependency");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("is NOT public - anonymous users must not get the map by default", () => {
|
|
21
|
+
expect(dependencyAccess.map.isPublic).toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("is granted to authenticated users by default", () => {
|
|
25
|
+
expect(dependencyAccess.map.isDefault).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("is registered so it syncs to roles", () => {
|
|
29
|
+
expect(dependencyAccessRules).toContain(dependencyAccess.map);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("the public read grant alone does NOT satisfy the map rule", () => {
|
|
33
|
+
// A visitor who only has the public `dependency.read` (warnings) grant
|
|
34
|
+
// must NOT be able to view the map - the map needs its own rule.
|
|
35
|
+
expect(
|
|
36
|
+
isAccessRuleSatisfied([QUALIFIED_READ], dependencyAccess.map),
|
|
37
|
+
).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("the qualified map grant satisfies the map rule", () => {
|
|
41
|
+
expect(isAccessRuleSatisfied([QUALIFIED_MAP], dependencyAccess.map)).toBe(
|
|
42
|
+
true,
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("dependency.read", () => {
|
|
48
|
+
test("stays public so warning badges remain visible to anonymous users", () => {
|
|
49
|
+
expect(dependencyAccess.dependency.read.isPublic).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("is not satisfied by the map grant (the two are independent)", () => {
|
|
53
|
+
expect(
|
|
54
|
+
isAccessRuleSatisfied([QUALIFIED_MAP], dependencyAccess.dependency.read),
|
|
55
|
+
).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|