@checkstack/automation-frontend 0.11.0 → 0.12.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 +193 -0
- package/package.json +16 -16
- package/src/editor/RunReplayPicker.tsx +1 -1
- package/src/index.tsx +6 -0
- package/src/pages/AutomationEditPage.tsx +35 -26
- package/src/pages/AutomationListPage.tsx +22 -8
- package/src/pages/AutomationTemplatePickerPage.tsx +8 -5
- package/src/pages/RunDetailPage.tsx +14 -6
- package/src/pages/TemplatePlaygroundPage.tsx +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,198 @@
|
|
|
1
1
|
# @checkstack/automation-frontend
|
|
2
2
|
|
|
3
|
+
## 0.12.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [43e4484]
|
|
8
|
+
- Updated dependencies [43e4484]
|
|
9
|
+
- Updated dependencies [43e4484]
|
|
10
|
+
- Updated dependencies [43e4484]
|
|
11
|
+
- Updated dependencies [43e4484]
|
|
12
|
+
- Updated dependencies [43e4484]
|
|
13
|
+
- @checkstack/catalog-common@2.7.0
|
|
14
|
+
- @checkstack/ui@1.26.0
|
|
15
|
+
- @checkstack/frontend-api@0.14.1
|
|
16
|
+
- @checkstack/auth-frontend@0.13.1
|
|
17
|
+
- @checkstack/gitops-frontend@0.7.2
|
|
18
|
+
- @checkstack/script-packages-frontend@0.4.11
|
|
19
|
+
- @checkstack/secrets-frontend@0.3.10
|
|
20
|
+
|
|
21
|
+
## 0.12.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- f93ee7a: Derive frontend authorization gates from the RPC contract instead of hand-picking
|
|
26
|
+
a hook per call site. The backend contract already declares, per procedure, both
|
|
27
|
+
the access rule (`access`) and how it is instance-scoped (`instanceAccess`); the
|
|
28
|
+
frontend gate was a hand re-encoding of that, which is how the "global-only
|
|
29
|
+
team-grant" drift shipped (nothing enforced that the hook a page chose matched
|
|
30
|
+
the mode the contract declared).
|
|
31
|
+
|
|
32
|
+
New `resolveProcedureGate` (`@checkstack/common`) reads a contract procedure's
|
|
33
|
+
metadata and returns the single gate the backend will enforce - classifying
|
|
34
|
+
`global` / `idParam` / `create` / `typeScoped` / post-filtered `open`, deriving
|
|
35
|
+
the object type from the rule and resolving the resource id from the input via
|
|
36
|
+
the contract's declared path. `parentScope` is normalized into an `idParam`/`open`
|
|
37
|
+
gate on a reconstructed parent rule + the parent type (the parent grant string the
|
|
38
|
+
backend checks is exactly `${resourceType}.${action}`, so no contract change was
|
|
39
|
+
needed). New `accessApi.useProcedureAccess(procedure, input)`
|
|
40
|
+
(`@checkstack/frontend-api` / `@checkstack/auth-frontend`) dispatches on the
|
|
41
|
+
derived gate; a call site can no longer gate on the wrong thing.
|
|
42
|
+
|
|
43
|
+
Fix a latent `create.parent` gap: the create gate's global-RBAC path only checked
|
|
44
|
+
the procedure's own manage rule, so a user with GLOBAL manage on the PARENT type
|
|
45
|
+
(e.g. a global system manager creating an incident/maintenance/SLO "for" a system,
|
|
46
|
+
which the backend authorizes via the parent gate) was not offered the create
|
|
47
|
+
affordance. The derived create gate now also ORs global manage on the parent type.
|
|
48
|
+
|
|
49
|
+
Migrate every `useCanCreate` create-button gate (catalog systems, health checks,
|
|
50
|
+
incidents, maintenance, SLOs, automations, status pages) to `useProcedureAccess`
|
|
51
|
+
on the owning create procedure, which also delivers the `create.parent` fix to
|
|
52
|
+
each, then remove `useCanCreate` from the `AccessApi`.
|
|
53
|
+
|
|
54
|
+
BREAKING CHANGES: `accessApi.useCanCreate(...)` is removed from
|
|
55
|
+
`@checkstack/frontend-api`. Replace it with
|
|
56
|
+
`accessApi.useProcedureAccess(SomeApi.contract.createX)` - the create procedure's
|
|
57
|
+
`instanceAccess.create` supplies the object type and parent gate, so no more
|
|
58
|
+
hand-passed `objectType` / `parentType`. The remaining hooks (`useAccess`,
|
|
59
|
+
`useCanAccessType`, `useResourceAccess`, `useRouteAccess`, `useIsAuthenticated`)
|
|
60
|
+
are unchanged: they gate surfaces/rows/routes that are not tied to a single
|
|
61
|
+
procedure. No gate became more restrictive; the create fix makes global
|
|
62
|
+
parent-managers correctly see create controls they were wrongly denied.
|
|
63
|
+
|
|
64
|
+
Patch-level adaptations to the `AccessApi` interface change (no behavior change of
|
|
65
|
+
their own): the host app's fallback `AccessApi` stubs (`@checkstack/frontend`) and
|
|
66
|
+
Storybook's mock (`@checkstack/ui`) drop `useCanCreate` and add the new
|
|
67
|
+
`useProcedureAccess` / `useSurfaceAccess` members so they match the interface, and
|
|
68
|
+
a `@checkstack/catalog-common` doc comment now names `useProcedureAccess` instead
|
|
69
|
+
of the removed hook.
|
|
70
|
+
|
|
71
|
+
- f93ee7a: Fuse authorization into the RPC call so a frontend gate can't drift from - or be
|
|
72
|
+
forgotten alongside - the procedure it guards. This is the structural endpoint of
|
|
73
|
+
the contract-derived gating work: instead of pairing `client.X.useMutation()` with
|
|
74
|
+
a separate `useProcedureAccess(X)`, the gate is welded to the call.
|
|
75
|
+
|
|
76
|
+
- `useGatedMutation` / `useGatedQuery` (`@checkstack/frontend-api`): the plugin
|
|
77
|
+
client's mutation/query hooks now have gate-fused variants that derive the
|
|
78
|
+
authorization verdict from the SAME contract procedure and input the call uses
|
|
79
|
+
and return it as `{ allowed, accessLoading }` on the result. A control cannot
|
|
80
|
+
obtain `mutate` without the verdict, and a gated query stays disabled until the
|
|
81
|
+
caller is authorized (no guaranteed-403 fetch). The id a mutation gates on is
|
|
82
|
+
passed as `gateInput` (e.g. `{ id }`), the same id `mutate` will send.
|
|
83
|
+
- `accessApi.useSurfaceAccess(procedure)` (`@checkstack/auth-frontend`): the
|
|
84
|
+
coarse "can the user reach this management surface" gate, DERIVED from a
|
|
85
|
+
representative procedure of the page (its access rule + object/parent type from
|
|
86
|
+
the contract) instead of hand-passed `objectType`/`parentType` that can drift.
|
|
87
|
+
Generalizes the hand-authored `useCanAccessType` surface gate.
|
|
88
|
+
- Runtime gating-drift detector (`@checkstack/backend-api`): the auth middleware
|
|
89
|
+
logs, in dev/e2e only (no-op in production), when a real user is denied a
|
|
90
|
+
global-only gate - a candidate for the "shown-but-denied" drift class. A
|
|
91
|
+
belt-and-suspenders net for hand-rolled/dynamic call paths the fused hooks
|
|
92
|
+
don't cover.
|
|
93
|
+
|
|
94
|
+
The automation editor is the reference surface: its create/update gates are fused
|
|
95
|
+
directly into the create/update mutations, so there is no separate gate hook to
|
|
96
|
+
keep in sync, and its surface gate uses `useSurfaceAccess`. The run-detail page's
|
|
97
|
+
"Cancel run" control is also fused onto
|
|
98
|
+
`cancelRun` - a real drift fix: it previously gated on a bare
|
|
99
|
+
`useAccess(automation.manage)` (the GLOBAL rule), so a team-scoped manager with a
|
|
100
|
+
grant on the automation but no global rule saw no Cancel button even though the
|
|
101
|
+
`parentScope`d backend would authorize them; the fused gate derives the verdict
|
|
102
|
+
from the page's `automationId`, so they now see it. A
|
|
103
|
+
`checkstack/prefer-gated-mutation` lint rule (dev tooling, scoped, `warn`) nudges
|
|
104
|
+
raw `.useMutation()` toward the fused variant so fusion is the default and raw
|
|
105
|
+
mutations become the deliberate, greppable exception (the remaining raw automation
|
|
106
|
+
mutations - per-row toggle/delete gated via `useResourceAccess`, and the
|
|
107
|
+
stateless `renderTemplate` utility - carry a documented suppression).
|
|
108
|
+
|
|
109
|
+
No behavior change for existing call sites: `useMutation` / `useQuery` /
|
|
110
|
+
`useCanAccessType` are unchanged and remain for per-row arrays, non-procedure
|
|
111
|
+
gates, and compound controls.
|
|
112
|
+
|
|
113
|
+
- f93ee7a: Fix a class of 403s where team-scoped managers were blocked from endpoints they
|
|
114
|
+
needed. A repo-wide audit of every `instanceAccess: { global: true }` procedure
|
|
115
|
+
found more instances of the same bug behind the health-check editor fix: an
|
|
116
|
+
endpoint on a team-scopable resource type, gated so only the GLOBAL access rule
|
|
117
|
+
(never a team grant) authorizes it.
|
|
118
|
+
|
|
119
|
+
Automation: the editor utilities and catalogs (`validateDefinition`,
|
|
120
|
+
`listTriggers`, `listActions`, `listArtifactTypes`, `listAutomationGroups`,
|
|
121
|
+
`listAutomationTemplates`, `renderTemplate`, `testScript`) now use `typeScoped`
|
|
122
|
+
so a team-scoped automation manager can author without the global rule. The run
|
|
123
|
+
endpoints (`listRuns`, `getRun`, `cancelRun`, `getRunScopeForReplay`) are scoped
|
|
124
|
+
to their parent automation via `parentScope` on `automationId`; `getRun`,
|
|
125
|
+
`cancelRun`, and `getRunScopeForReplay` now take the owning `automationId`
|
|
126
|
+
(always available in the run URL/editor) and the handler filters the run fetch by
|
|
127
|
+
it, so a run id cannot be paired with a foreign automation the caller happens to
|
|
128
|
+
hold a grant on. The two migration-admin endpoints stay `global: true` (genuine
|
|
129
|
+
platform-admin actions).
|
|
130
|
+
|
|
131
|
+
Health check: `validateConfiguration` (editor deep-validate) and
|
|
132
|
+
`getPlatformNotificationDefaults` (fetched on every assignment-editor mount) move
|
|
133
|
+
to `typeScoped`. The paired WRITE `setPlatformNotificationDefaults` stays
|
|
134
|
+
`global: true` on purpose - it rewrites instance-wide defaults for every team, so
|
|
135
|
+
a single team grant must not authorize it. Because that write stays global-only,
|
|
136
|
+
the assignment editor's "Notification defaults" button is now gated on the global
|
|
137
|
+
`configuration.manage` rule (`healthcheck-frontend`), so a team-scoped manager no
|
|
138
|
+
longer sees an editor whose Save always 403'd.
|
|
139
|
+
|
|
140
|
+
Anomaly: the anomaly settings panels embedded in the health-check editor
|
|
141
|
+
(`updateAnomalyConfig` / `getAnomalyConfig` and `updateAnomalyAssignmentConfig` /
|
|
142
|
+
`getAnomalyAssignmentConfig`) were authorized against the non-team-scopable
|
|
143
|
+
`anomaly_feed` type (via `global: true` or an `idParam` that could never match a
|
|
144
|
+
team grant), so a team-scoped manager who owns the check/system saw "Save
|
|
145
|
+
Defaults" / "Save Exceptions" buttons whose Save always 403'd. They now
|
|
146
|
+
`parentScope` on the owning health-check configuration (`healthcheck.healthcheck`)
|
|
147
|
+
and catalog system (`catalog.system`) respectively, so managing the check/system
|
|
148
|
+
authorizes reading and editing its anomaly settings. The frontend needed no
|
|
149
|
+
change: those buttons were already disabled for non-managers, and the panels are
|
|
150
|
+
only reachable inside the manager-gated editor. Also, the automation "New
|
|
151
|
+
automation" template picker (`automation-frontend`) gated its page on the bare
|
|
152
|
+
global manage rule; it now uses the create capability, so a team-scoped creator
|
|
153
|
+
(whom the route already reveals the page to) is no longer shown a blocked page.
|
|
154
|
+
|
|
155
|
+
Incident & maintenance: `removeLink` was `global: true` because its input carried
|
|
156
|
+
only the link id. It now takes the owning `incidentId` / `maintenanceId`
|
|
157
|
+
(mirroring `addLink`), authorizes per-instance via `idParam`, and the service
|
|
158
|
+
scopes the delete by that parent id so a link cannot be removed by pairing its id
|
|
159
|
+
with a different incident/maintenance the caller manages. The AI `removeLink`
|
|
160
|
+
tools carry the parent id too.
|
|
161
|
+
|
|
162
|
+
BREAKING CHANGES: `automation.getRun`, `automation.cancelRun`,
|
|
163
|
+
`automation.getRunScopeForReplay`, `incident.removeLink`, and
|
|
164
|
+
`maintenance.removeLink` now require a parent id (`automationId` /
|
|
165
|
+
`incidentId` / `maintenanceId`) in their input. Endpoints previously gated by a
|
|
166
|
+
global rule alone now also accept the owning team's grant; no endpoint became
|
|
167
|
+
more permissive for a user who lacks both the global rule and a relevant team
|
|
168
|
+
grant.
|
|
169
|
+
|
|
170
|
+
Not team-scopable, so intentionally left `global: true` (verified by the audit):
|
|
171
|
+
catalog environments, anomaly config, SLO list/streak/milestone reads and
|
|
172
|
+
health-check history/stats (their read rules are public/default), and every
|
|
173
|
+
hand-rolled HTTP route (global admin/infra or already team-aware).
|
|
174
|
+
|
|
175
|
+
### Patch Changes
|
|
176
|
+
|
|
177
|
+
- Updated dependencies [f93ee7a]
|
|
178
|
+
- Updated dependencies [f93ee7a]
|
|
179
|
+
- Updated dependencies [f93ee7a]
|
|
180
|
+
- Updated dependencies [f93ee7a]
|
|
181
|
+
- @checkstack/common@0.22.0
|
|
182
|
+
- @checkstack/frontend-api@0.14.0
|
|
183
|
+
- @checkstack/auth-frontend@0.13.0
|
|
184
|
+
- @checkstack/ui@1.25.1
|
|
185
|
+
- @checkstack/catalog-common@2.6.3
|
|
186
|
+
- @checkstack/automation-common@0.10.0
|
|
187
|
+
- @checkstack/auth-common@0.13.0
|
|
188
|
+
- @checkstack/ai-common@0.6.6
|
|
189
|
+
- @checkstack/gitops-frontend@0.7.1
|
|
190
|
+
- @checkstack/integration-common@0.9.8
|
|
191
|
+
- @checkstack/script-packages-frontend@0.4.10
|
|
192
|
+
- @checkstack/secrets-frontend@0.3.9
|
|
193
|
+
- @checkstack/template-engine@0.4.11
|
|
194
|
+
- @checkstack/signal-frontend@0.3.5
|
|
195
|
+
|
|
3
196
|
## 0.11.0
|
|
4
197
|
|
|
5
198
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/automation-frontend",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -16,20 +16,20 @@
|
|
|
16
16
|
"lint:code": "eslint . --max-warnings 0"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@checkstack/auth-common": "0.
|
|
20
|
-
"@checkstack/auth-frontend": "0.
|
|
21
|
-
"@checkstack/automation-common": "0.
|
|
22
|
-
"@checkstack/ai-common": "0.6.
|
|
23
|
-
"@checkstack/catalog-common": "2.
|
|
24
|
-
"@checkstack/common": "0.
|
|
25
|
-
"@checkstack/frontend-api": "0.
|
|
26
|
-
"@checkstack/gitops-frontend": "0.7.
|
|
27
|
-
"@checkstack/integration-common": "0.9.
|
|
28
|
-
"@checkstack/script-packages-frontend": "0.4.
|
|
29
|
-
"@checkstack/secrets-frontend": "0.3.
|
|
30
|
-
"@checkstack/signal-frontend": "0.3.
|
|
31
|
-
"@checkstack/template-engine": "0.4.
|
|
32
|
-
"@checkstack/ui": "1.
|
|
19
|
+
"@checkstack/auth-common": "0.13.0",
|
|
20
|
+
"@checkstack/auth-frontend": "0.13.1",
|
|
21
|
+
"@checkstack/automation-common": "0.10.0",
|
|
22
|
+
"@checkstack/ai-common": "0.6.6",
|
|
23
|
+
"@checkstack/catalog-common": "2.7.0",
|
|
24
|
+
"@checkstack/common": "0.22.0",
|
|
25
|
+
"@checkstack/frontend-api": "0.14.1",
|
|
26
|
+
"@checkstack/gitops-frontend": "0.7.2",
|
|
27
|
+
"@checkstack/integration-common": "0.9.8",
|
|
28
|
+
"@checkstack/script-packages-frontend": "0.4.11",
|
|
29
|
+
"@checkstack/secrets-frontend": "0.3.10",
|
|
30
|
+
"@checkstack/signal-frontend": "0.3.5",
|
|
31
|
+
"@checkstack/template-engine": "0.4.11",
|
|
32
|
+
"@checkstack/ui": "1.26.0",
|
|
33
33
|
"@dnd-kit/core": "^6.3.1",
|
|
34
34
|
"@dnd-kit/sortable": "^8.0.0",
|
|
35
35
|
"@dnd-kit/utilities": "^3.2.2",
|
|
@@ -43,6 +43,6 @@
|
|
|
43
43
|
"typescript": "^5.0.0",
|
|
44
44
|
"@types/react": "^19.0.0",
|
|
45
45
|
"@checkstack/tsconfig": "0.0.7",
|
|
46
|
-
"@checkstack/scripts": "0.7.
|
|
46
|
+
"@checkstack/scripts": "0.7.3"
|
|
47
47
|
}
|
|
48
48
|
}
|
|
@@ -45,7 +45,7 @@ export const RunReplayPicker: React.FC<RunReplayPickerProps> = ({
|
|
|
45
45
|
);
|
|
46
46
|
|
|
47
47
|
const replayQuery = client.getRunScopeForReplay.useQuery(
|
|
48
|
-
{ runId: selectedRunId ?? "" },
|
|
48
|
+
{ runId: selectedRunId ?? "", automationId },
|
|
49
49
|
{ enabled: Boolean(selectedRunId), gcTime: 0 },
|
|
50
50
|
);
|
|
51
51
|
|
package/src/index.tsx
CHANGED
|
@@ -51,6 +51,9 @@ export default createFrontendPlugin({
|
|
|
51
51
|
})),
|
|
52
52
|
title: "Automations",
|
|
53
53
|
accessRule: automationAccess.read,
|
|
54
|
+
// Team-scoped: a team with any automation grant may reach the list; the
|
|
55
|
+
// backend `listAutomations` post-filters it to their own automations.
|
|
56
|
+
manageCapability: { objectType: automationResourceTypes.automation },
|
|
54
57
|
nav: { group: "Automation", icon: Workflow },
|
|
55
58
|
},
|
|
56
59
|
{
|
|
@@ -83,6 +86,9 @@ export default createFrontendPlugin({
|
|
|
83
86
|
})),
|
|
84
87
|
title: "Edit automation",
|
|
85
88
|
accessRule: automationAccess.read,
|
|
89
|
+
// Team-scoped: a team manager of an automation may reach its edit page
|
|
90
|
+
// (the page + backend enforce the per-instance manage grant).
|
|
91
|
+
manageCapability: { objectType: automationResourceTypes.automation },
|
|
86
92
|
},
|
|
87
93
|
{
|
|
88
94
|
route: automationRoutes.routes.runs,
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
AutomationApi,
|
|
18
18
|
automationAccess,
|
|
19
19
|
automationRoutes,
|
|
20
|
-
automationResourceTypes,
|
|
21
20
|
type AutomationDefinition,
|
|
22
21
|
} from "@checkstack/automation-common";
|
|
23
22
|
import {
|
|
@@ -116,9 +115,14 @@ const AutomationEditContent: React.FC = () => {
|
|
|
116
115
|
const toast = useToast();
|
|
117
116
|
const navigate = useNavigate();
|
|
118
117
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
118
|
+
// Surface gate: the caller may reach this create/edit page when they hold the
|
|
119
|
+
// GLOBAL read rule OR any team-derived grant on the automation type
|
|
120
|
+
// (create/manage). A team-scoped creator/manager holds no global rule but must
|
|
121
|
+
// still open the full flow; the mutation controls below stay gated on the
|
|
122
|
+
// per-instance manage grant (`canManageThisAutomation`) and the create
|
|
123
|
+
// capability (`canCreate`), and the backend enforces `create`/`idParam`.
|
|
124
|
+
const { allowed: canAccessSurface, loading: accessLoading } =
|
|
125
|
+
accessApi.useSurfaceAccess(AutomationApi.contract.listAutomations);
|
|
122
126
|
// allowGlobal: the GLOBAL manage rule means the caller may create a resource
|
|
123
127
|
// not scoped to any team. This feeds the TeamOwnershipPicker only.
|
|
124
128
|
const { allowed: hasGlobalManageAccess } = accessApi.useAccess(
|
|
@@ -126,21 +130,10 @@ const AutomationEditContent: React.FC = () => {
|
|
|
126
130
|
);
|
|
127
131
|
const allowGlobal = hasGlobalManageAccess;
|
|
128
132
|
|
|
129
|
-
// Create
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
objectType: automationResourceTypes.automation,
|
|
134
|
-
});
|
|
135
|
-
// Per-resource manage gate (existing automations): grants to global-manage
|
|
136
|
-
// holders AND to team-scoped users with a per-object grant on THIS id.
|
|
137
|
-
const { canAccess } = accessApi.useResourceAccess({
|
|
138
|
-
accessRule: automationAccess.manage,
|
|
139
|
-
objectType: automationResourceTypes.automation,
|
|
140
|
-
resourceIds: !isNew && automationId ? [automationId] : [],
|
|
141
|
-
});
|
|
142
|
-
const canManageThisAutomation =
|
|
143
|
-
!isNew && automationId ? canAccess(automationId) : false;
|
|
133
|
+
// Create + per-instance manage gates are FUSED into the create/update
|
|
134
|
+
// mutations below (`useGatedMutation`), so the controls that call `mutate`
|
|
135
|
+
// read their own authorization from the same object - the gate can't drift
|
|
136
|
+
// from the call. See `createMutation` / `updateMutation`.
|
|
144
137
|
|
|
145
138
|
// GitOps provenance lock: when this automation is declaratively managed,
|
|
146
139
|
// disable manual edits + show a banner. `entityId` is the automation id
|
|
@@ -151,10 +144,8 @@ const AutomationEditContent: React.FC = () => {
|
|
|
151
144
|
entityId: isNew ? undefined : automationId,
|
|
152
145
|
});
|
|
153
146
|
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
const canManage =
|
|
157
|
-
(isNew ? canCreate : canManageThisAutomation) && !isLocked;
|
|
147
|
+
// `canManage` is derived from the fused mutation gates below (create for new,
|
|
148
|
+
// update-idParam for existing) AND the GitOps lock - see after the mutations.
|
|
158
149
|
|
|
159
150
|
const loadQuery = client.getAutomation.useQuery(
|
|
160
151
|
{ id: automationId ?? "" },
|
|
@@ -285,6 +276,9 @@ const AutomationEditContent: React.FC = () => {
|
|
|
285
276
|
setTab(next);
|
|
286
277
|
};
|
|
287
278
|
|
|
279
|
+
// Not gated: a sandboxed validation dry-run (typeScoped), not a resource
|
|
280
|
+
// write - nothing to fuse a per-instance verdict onto.
|
|
281
|
+
// eslint-disable-next-line checkstack/prefer-gated-mutation -- validation dry-run, not a gated write
|
|
288
282
|
const validateMutation = client.validateDefinition.useMutation({
|
|
289
283
|
onSuccess: (result) => {
|
|
290
284
|
setValidationErrors(result.valid ? [] : result.errors);
|
|
@@ -296,6 +290,7 @@ const AutomationEditContent: React.FC = () => {
|
|
|
296
290
|
// so its constant background runs don't flicker the Save button's
|
|
297
291
|
// pending state. `mutateAsync` is stable across renders.
|
|
298
292
|
const { mutateAsync: runLiveValidation } =
|
|
293
|
+
// eslint-disable-next-line checkstack/prefer-gated-mutation -- validation dry-run, not a gated write
|
|
299
294
|
client.validateDefinition.useMutation();
|
|
300
295
|
|
|
301
296
|
// Re-validate (debounced) on every edit in either tab, so invalid
|
|
@@ -335,7 +330,9 @@ const AutomationEditContent: React.FC = () => {
|
|
|
335
330
|
return () => clearTimeout(handle);
|
|
336
331
|
}, [tab, yamlText, definition, runLiveValidation]);
|
|
337
332
|
|
|
338
|
-
|
|
333
|
+
// Gate-fused create: `createMutation.allowed` is the create capability derived
|
|
334
|
+
// from `createAutomation`'s own contract - you cannot get `mutate` without it.
|
|
335
|
+
const createMutation = client.createAutomation.useGatedMutation({
|
|
339
336
|
onSuccess: (data) => {
|
|
340
337
|
// Saved: clear the dirty flag, then redirect on the next tick so the
|
|
341
338
|
// unsaved-changes guard has committed `isDirty = false` and doesn't
|
|
@@ -358,7 +355,11 @@ const AutomationEditContent: React.FC = () => {
|
|
|
358
355
|
},
|
|
359
356
|
});
|
|
360
357
|
|
|
361
|
-
|
|
358
|
+
// Gate-fused update: `updateMutation.allowed` is the per-instance manage grant
|
|
359
|
+
// on THIS automation, derived from `updateAutomation`'s `idParam` contract and
|
|
360
|
+
// the id passed as `gateInput` - the same id `mutate` will send.
|
|
361
|
+
const updateMutation = client.updateAutomation.useGatedMutation({
|
|
362
|
+
gateInput: !isNew && automationId ? { id: automationId } : undefined,
|
|
362
363
|
onSuccess: (data) => {
|
|
363
364
|
setIsDirty(false);
|
|
364
365
|
toast.success(`Saved ${data.name}`);
|
|
@@ -366,6 +367,14 @@ const AutomationEditContent: React.FC = () => {
|
|
|
366
367
|
onError: (error) => toastError(toast, "Failed to save automation", error),
|
|
367
368
|
});
|
|
368
369
|
|
|
370
|
+
// Effective edit permission, derived from the FUSED mutation gates (create for
|
|
371
|
+
// new, per-instance update for existing) AND not GitOps-locked.
|
|
372
|
+
const canManage =
|
|
373
|
+
(isNew ? createMutation.allowed : updateMutation.allowed) && !isLocked;
|
|
374
|
+
|
|
375
|
+
// Compound-gated: the "Run now" control is rendered only under `canManage`
|
|
376
|
+
// (itself the fused update gate), so its enablement is already contract-derived.
|
|
377
|
+
// eslint-disable-next-line checkstack/prefer-gated-mutation -- compound-gated by canManage (fused from updateAutomation)
|
|
369
378
|
const manualRunMutation = client.manualRun.useMutation({
|
|
370
379
|
onSuccess: (data) => {
|
|
371
380
|
toast.success(`Manual run queued`);
|
|
@@ -539,7 +548,7 @@ const AutomationEditContent: React.FC = () => {
|
|
|
539
548
|
subtitle={isNew ? "Wire a trigger to one or more actions" : undefined}
|
|
540
549
|
icon={Workflow}
|
|
541
550
|
loading={accessLoading || (!isNew && loadQuery.isLoading)}
|
|
542
|
-
allowed={
|
|
551
|
+
allowed={canAccessSurface && (isNew ? canManage : true)}
|
|
543
552
|
actions={
|
|
544
553
|
<div className="flex items-center gap-2">
|
|
545
554
|
<Link to={resolveRoute(automationRoutes.routes.list)}>
|
|
@@ -78,15 +78,20 @@ const AutomationListContent: React.FC = () => {
|
|
|
78
78
|
const toast = useToast();
|
|
79
79
|
const navigate = useNavigate();
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
// Surface gate: the GLOBAL read rule OR any team-derived grant on the
|
|
82
|
+
// automation type. A team-scoped creator/manager holds no global rule but must
|
|
83
|
+
// still reach the list (the backend `listAutomations` post-filters to their
|
|
84
|
+
// own automations via `listKey`), so they can find and open their automation.
|
|
85
|
+
const { allowed: canAccessSurface, loading: accessLoading } =
|
|
86
|
+
accessApi.useCanAccessType({
|
|
87
|
+
accessRule: automationAccess.read,
|
|
88
|
+
objectType: automationResourceTypes.automation,
|
|
89
|
+
});
|
|
84
90
|
// Create/page gate: whether the user may create a new automation at all
|
|
85
91
|
// (global manage rule OR team-derived create capability).
|
|
86
|
-
const { allowed: canCreate } = accessApi.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
92
|
+
const { allowed: canCreate } = accessApi.useProcedureAccess(
|
|
93
|
+
AutomationApi.contract.createAutomation,
|
|
94
|
+
);
|
|
90
95
|
|
|
91
96
|
const [statusFilter, setStatusFilter] = React.useState<
|
|
92
97
|
"all" | "enabled" | "disabled"
|
|
@@ -123,6 +128,11 @@ const AutomationListContent: React.FC = () => {
|
|
|
123
128
|
// 2. onError rolls back from the snapshot, then surfaces a toast.
|
|
124
129
|
// 3. onSettled invalidates so server truth settles in either branch.
|
|
125
130
|
// 4. No success toast — the visible switch flip IS the feedback.
|
|
131
|
+
// Per-ROW mutation: one instance dispatched with each row's id, and every
|
|
132
|
+
// control is gated by `canAccess(automation.id)` below. There is no single id
|
|
133
|
+
// to fuse a gate onto, so this stays raw (the array-gated exception the rule
|
|
134
|
+
// documents).
|
|
135
|
+
// eslint-disable-next-line checkstack/prefer-gated-mutation -- per-row, gated via useResourceAccess().canAccess(id)
|
|
126
136
|
const toggleMutation = client.toggleAutomation.useMutation<{
|
|
127
137
|
previous: AutomationsQueryData | undefined;
|
|
128
138
|
}>({
|
|
@@ -153,6 +163,10 @@ const AutomationListContent: React.FC = () => {
|
|
|
153
163
|
},
|
|
154
164
|
});
|
|
155
165
|
|
|
166
|
+
// Per-ROW mutation: dispatched with the row's id from the delete dialog, and
|
|
167
|
+
// every delete control is gated by `canAccess(automation.id)` below. No single
|
|
168
|
+
// id to fuse onto (the array-gated exception the rule documents).
|
|
169
|
+
// eslint-disable-next-line checkstack/prefer-gated-mutation -- per-row, gated via useResourceAccess().canAccess(id)
|
|
156
170
|
const deleteMutation = client.deleteAutomation.useMutation({
|
|
157
171
|
onSuccess: () => {
|
|
158
172
|
toast.success("Automation deleted");
|
|
@@ -440,7 +454,7 @@ const AutomationListContent: React.FC = () => {
|
|
|
440
454
|
subtitle="Trigger-driven workflows that react to platform events"
|
|
441
455
|
icon={Workflow}
|
|
442
456
|
loading={accessLoading}
|
|
443
|
-
allowed={
|
|
457
|
+
allowed={canAccessSurface}
|
|
444
458
|
actions={
|
|
445
459
|
<div className="flex items-center gap-2">
|
|
446
460
|
<Link to={resolveRoute(automationRoutes.routes.playground)}>
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
} from "@checkstack/frontend-api";
|
|
10
10
|
import {
|
|
11
11
|
AutomationApi,
|
|
12
|
-
automationAccess,
|
|
13
12
|
automationRoutes,
|
|
14
13
|
type AutomationTemplate,
|
|
15
14
|
} from "@checkstack/automation-common";
|
|
@@ -35,9 +34,13 @@ const AutomationTemplatePickerContent: React.FC = () => {
|
|
|
35
34
|
const accessApi = useApi(accessApiRef);
|
|
36
35
|
const navigate = useNavigate();
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
37
|
+
// The "New automation" picker is a create surface. Gate on the create
|
|
38
|
+
// capability DERIVED from the create procedure's contract (its `create`
|
|
39
|
+
// instanceAccess): global manage rule OR a team-derived create/manage grant,
|
|
40
|
+
// so a team-scoped automation creator - whom the route's `manageCapability`
|
|
41
|
+
// already reveals this page to - is not blocked by a bare global-rule check.
|
|
42
|
+
const { allowed: canCreate, loading: accessLoading } =
|
|
43
|
+
accessApi.useProcedureAccess(AutomationApi.contract.createAutomation);
|
|
41
44
|
|
|
42
45
|
const query = client.listAutomationTemplates.useQuery();
|
|
43
46
|
|
|
@@ -68,7 +71,7 @@ const AutomationTemplatePickerContent: React.FC = () => {
|
|
|
68
71
|
subtitle="Start from a curated example or build your own from scratch"
|
|
69
72
|
icon={Workflow}
|
|
70
73
|
loading={accessLoading}
|
|
71
|
-
allowed={
|
|
74
|
+
allowed={canCreate}
|
|
72
75
|
>
|
|
73
76
|
<div className="space-y-6">
|
|
74
77
|
<div className="relative flex flex-col gap-3 overflow-hidden rounded-[var(--d-card-r)] border border-border/70 bg-gradient-to-b from-surface-2 to-surface p-[var(--d-pad)] shadow-[0_1px_2px_hsl(var(--foreground)/0.04),0_10px_30px_-14px_hsl(var(--foreground)/0.12)] sm:flex-row sm:items-center sm:justify-between">
|
|
@@ -115,12 +115,11 @@ const RunDetailContent: React.FC = () => {
|
|
|
115
115
|
const { allowed, loading: accessLoading } = accessApi.useAccess(
|
|
116
116
|
automationAccess.read,
|
|
117
117
|
);
|
|
118
|
-
const { allowed: canManage } = accessApi.useAccess(automationAccess.manage);
|
|
119
118
|
|
|
120
119
|
const query = client.getRun.useQuery(
|
|
121
|
-
{ id: runId ?? "" },
|
|
120
|
+
{ id: runId ?? "", automationId: automationId ?? "" },
|
|
122
121
|
{
|
|
123
|
-
enabled: Boolean(runId),
|
|
122
|
+
enabled: Boolean(runId) && Boolean(automationId),
|
|
124
123
|
// Live runs only: poll every 2s while the run is `running`/`waiting` so a
|
|
125
124
|
// user watching an execution sees steps progress without a manual reload.
|
|
126
125
|
// Returning `false` on a terminal status stops the polling entirely.
|
|
@@ -137,7 +136,14 @@ const RunDetailContent: React.FC = () => {
|
|
|
137
136
|
query.data?.run.status === "running" ||
|
|
138
137
|
query.data?.run.status === "waiting";
|
|
139
138
|
|
|
140
|
-
|
|
139
|
+
// Fuse the cancel gate onto the mutation: `cancelRun` is `parentScope`d on the
|
|
140
|
+
// owning automation (MANAGE), so the verdict derives from THIS page's
|
|
141
|
+
// `automationId` - a team-scoped manager (grant on this automation, no global
|
|
142
|
+
// rule) can cancel their own run, which a bare `useAccess(manage)` on the
|
|
143
|
+
// global rule would wrongly hide.
|
|
144
|
+
const cancelMutation = client.cancelRun.useGatedMutation({
|
|
145
|
+
gateInput: automationId ? { automationId } : undefined,
|
|
146
|
+
});
|
|
141
147
|
|
|
142
148
|
if (!automationId || !runId) {
|
|
143
149
|
return (
|
|
@@ -178,14 +184,16 @@ const RunDetailContent: React.FC = () => {
|
|
|
178
184
|
All runs
|
|
179
185
|
</Button>
|
|
180
186
|
</Link>
|
|
181
|
-
{
|
|
187
|
+
{cancelMutation.allowed &&
|
|
182
188
|
query.data &&
|
|
183
189
|
(query.data.run.status === "running" ||
|
|
184
190
|
query.data.run.status === "waiting") && (
|
|
185
191
|
<Button
|
|
186
192
|
variant="destructive"
|
|
187
193
|
size="sm"
|
|
188
|
-
onClick={() =>
|
|
194
|
+
onClick={() =>
|
|
195
|
+
cancelMutation.mutate({ id: runId, automationId })
|
|
196
|
+
}
|
|
189
197
|
disabled={cancelMutation.isPending}
|
|
190
198
|
>
|
|
191
199
|
Cancel run
|
|
@@ -74,6 +74,11 @@ const TemplatePlaygroundContent: React.FC = () => {
|
|
|
74
74
|
| null
|
|
75
75
|
>(null);
|
|
76
76
|
|
|
77
|
+
// Stateless `typeScoped` render utility (no automation id in input): the gate
|
|
78
|
+
// is "any automation grant", already enforced at the page surface, and there
|
|
79
|
+
// is no resource instance to fuse a per-call gate onto. Stays raw (the
|
|
80
|
+
// utility/no-instance exception the rule documents).
|
|
81
|
+
// eslint-disable-next-line checkstack/prefer-gated-mutation -- typeScoped utility, no resource id to gate on; surface-gated
|
|
77
82
|
const renderMutation = client.renderTemplate.useMutation({
|
|
78
83
|
onSuccess: (data) => {
|
|
79
84
|
if (data.success) {
|