@checkstack/automation-frontend 0.6.1 → 0.7.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 CHANGED
@@ -1,5 +1,165 @@
1
1
  # @checkstack/automation-frontend
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d2077bd: Platform-wide team-scoped access control on a unified relation-tuple store.
8
+
9
+ Admins can scope any resource to teams, and the **platform** (not each plugin)
10
+ enforces it. A plugin opts in declaratively by adding `instanceAccess` to a
11
+ procedure's contract; the auth middleware does the rest, so enforcement is
12
+ consistent across catalog, health checks, incidents, maintenances, SLOs,
13
+ automations, and the dependency map, and any third-party plugin gets it for free.
14
+
15
+ Core model:
16
+
17
+ - **Teams are optional.** A resource with no team grants behaves exactly as
18
+ before.
19
+ - **Team grants are additive and restrict who can CHANGE a resource, not who can
20
+ SEE it.** Granting a team `Manage` lets its members view and change the
21
+ resource; `Read-only` lets them view it. Either level grants access to team
22
+ members **even when they lack the global permission**, and granting never
23
+ removes read from anyone who already had it (e.g. a public status page stays
24
+ readable). Privacy is a separate, explicit opt-in via the **Private** toggle,
25
+ which removes the global read path so only the resource's teams can see it.
26
+ - **Ownership at creation.** Create forms expose an **Owning team** picker. A
27
+ non-admin can create a resource for a team they belong to that holds a
28
+ create-capability grant for that type; the new resource is auto-granted to that
29
+ team. Incidents and maintenances are **parent-gated**: anyone who can manage a
30
+ system may open incidents/maintenances for it, no separate grant needed.
31
+ - **Meaningful authorization errors.** A caller with neither the global rule nor
32
+ any team grant for a resource type gets a `403` with a structured body instead
33
+ of a silently-empty `200`. Anonymous callers on public endpoints are never
34
+ `403`'d, so status pages keep rendering.
35
+
36
+ Unified relation-tuple store:
37
+
38
+ - The previously separate access primitives (`resource_team_access.canRead` /
39
+ `.canManage`, ownership, `resource_access_settings.teamOnly`, and
40
+ `resource_create_grant`) are collapsed onto ONE
41
+ `relation_tuple(object, relation, subject)` store: "a team has
42
+ `viewer`/`editor`/`owner` on an object, or `creator` on a type". Privacy is an
43
+ explicit **`private` marker** tuple — its **presence** closes the global read
44
+ path (team grants only), its **absence** is the readable-by-default state, so a
45
+ private resource with zero grants is correctly inaccessible to everyone rather
46
+ than silently globalized. The access decision is a pure, unit-tested function.
47
+ - The auth API is generic: `writeRelation` / `removeRelation` / `setObjectPublic`
48
+ / `listObjectRelations` / `listSubjectRelations` / `setCreateGrant` /
49
+ `listTeamCreateGrants` (user-facing) and `check` / `listAccessibleObjectIds` /
50
+ `hasAnyTypeGrant` / `authorizeCreate` / `setOwner` / `deleteObjectRelations`
51
+ (service-to-service). Migration `0008` backfills tuples from the legacy tables
52
+ and drops them.
53
+
54
+ Explicit per-procedure scoping:
55
+
56
+ - Access rules (`access()` / `accessPair()`) define only the rule (id, level,
57
+ defaults); every procedure declares its own `instanceAccess`. This removes a
58
+ "loaded gun" default that silently applied a shared `idParam` to any procedure
59
+ which forgot its own override.
60
+ - Modes: `idParam` (single-resource pre-check, fails **closed** if the id does
61
+ not resolve), `listKey` / `recordKey` (post-filter a list/record to the
62
+ accessible subset), `create` (authorize creation + write the owning-team
63
+ grant), `parentScope` (scope by read/manage access to a PARENT type,
64
+ cross-plugin single-hop: "you may see incidents/maintenances/SLOs/health for
65
+ system S iff you may see S"), and `global: true` (the honest "intentionally not
66
+ team-scoped" opt-out). A boot-time validator **rejects** any procedure gated on
67
+ a team-scopable resource type that declares no `instanceAccess`, turning the
68
+ previous fail-open into a boot error.
69
+
70
+ Teams administration:
71
+
72
+ - **Team managers** manage their own team's members and managers without the
73
+ global `auth.teams.manage` rule; creating, deleting, and granting a team access
74
+ remain admin-only.
75
+ - A **standalone Teams page** (gated on `auth.teams.read`) lets managers reach
76
+ team administration without the admin Auth Settings page; members are added via
77
+ a debounced directory picker.
78
+ - A **cross-plugin `ResourceResolverRegistry`** lets owning plugins register a
79
+ name/search resolver for their resource types, so the Teams page lists a team's
80
+ grants **by name** (grouped by type) and offers a resource picker — an admin can
81
+ change a grant's level, revoke it, or add one, without auth depending on every
82
+ plugin. Resolvers shipped for catalog systems, health-check configurations,
83
+ incidents, maintenances, SLO objectives, and automations.
84
+
85
+ Frontend:
86
+
87
+ - The resource-side editor is **"Who can change this"** (one Manage checkbox per
88
+ team; unticked = read-only), with an always-visible **Private** toggle
89
+ (disabled until a team that can Manage exists, so a resource can't be stranded).
90
+ - `TeamOwnershipPicker` explains _why_ there's nothing to pick (not a member of
91
+ any team, or none of your teams manage the selected parent) instead of a bare
92
+ "global resource" line.
93
+ - Read-only **"who can change this"** indicators on resource detail pages expand
94
+ to the actual people by name; bulk + per-row **Scope to team** actions in the
95
+ catalog systems list; and the team-access copy spells out that grants are
96
+ additive and that Read-only grants view (not change) even without the global
97
+ permission.
98
+
99
+ Security hardening:
100
+
101
+ - Child deletes in catalog (`removeSystemContact` / `removeSystemLink`) are scoped
102
+ to both the child id and its parent `systemId`, closing a cross-system IDOR for
103
+ team-scoped managers.
104
+ - `searchUsers` is restricted to team administrators, closing a directory/email
105
+ enumeration path opened by the default `auth.teams.read` rule.
106
+ - Grant setters reject unregistered resource types.
107
+
108
+ BREAKING CHANGES (beta; shipped as minor bumps):
109
+
110
+ - `access()` and `accessPair()` no longer accept `idParam` / `listKey` /
111
+ `recordKey`; move instance config to the procedure's `instanceAccess`.
112
+ - Boot fails if a procedure gated on a team-scopable resource type omits
113
+ `instanceAccess`. Declare a scoping mode or `instanceAccess: { global: true }`.
114
+ - The `AuthService` interface is reshaped: `check`, `listAccessibleObjectIds`,
115
+ `hasAnyTypeGrant`, `authorizeCreate` (returns `isPrivate`), `setOwner`
116
+ (`isPrivate`), and `deleteObjectRelations`. Custom `AuthService` implementations
117
+ and mocks must update.
118
+ - The auth RPC contract's per-concept resource-access endpoints are replaced by
119
+ the generic tuple API above; external callers of the old
120
+ `getResourceTeamAccess` / `setResourceTeamAccess` / `setResourceAccessSettings`
121
+ / `grantResourceCreate` / etc. must move to the new procedures.
122
+ - Several contract inputs changed from a bare `string` to an object so the
123
+ middleware can resolve the resource id: catalog `deleteSystem` (`{ id }`),
124
+ `removeSystemContact` / `removeSystemLink` (`{ id, systemId }`); health-check
125
+ `deleteConfiguration` / `pauseConfiguration` / `resumeConfiguration` (`{ id }`).
126
+ All in-tree callers are updated.
127
+ - List/record endpoints that relied on returning an empty `200` to signal "no
128
+ access" now return a `403` for categorically-unauthorized principals.
129
+ - The mis-keyed bulk endpoints `getBulkIncidentsForSystems`,
130
+ `getBulkMaintenancesForSystems`, and `getBulkObjectivesForSystems` no longer
131
+ post-filter their (systemId-keyed) result; access is already gated by
132
+ `catalog.system` upstream.
133
+ - Team membership/manager mutations (`addUserToTeam`, `removeUserFromTeam`,
134
+ `addTeamManager`, `removeTeamManager`) now require `auth.teams.read` instead of
135
+ `auth.teams.manage` at the contract level (broadened to per-team managers).
136
+ - The `resource_team_access`, `resource_access_settings`, and
137
+ `resource_create_grant` tables are dropped (data backfilled into
138
+ `relation_tuple` by migration `0008`). A previously inconsistent "team-only with
139
+ zero grants" resource is now correctly inaccessible to global-access holders.
140
+
141
+ ### Patch Changes
142
+
143
+ - Updated dependencies [551eaa9]
144
+ - Updated dependencies [d2077bd]
145
+ - Updated dependencies [551eaa9]
146
+ - Updated dependencies [9ab73c5]
147
+ - Updated dependencies [5c6393f]
148
+ - @checkstack/ai-common@0.5.0
149
+ - @checkstack/auth-common@0.10.0
150
+ - @checkstack/auth-frontend@0.8.0
151
+ - @checkstack/common@0.16.0
152
+ - @checkstack/automation-common@0.7.0
153
+ - @checkstack/catalog-common@2.4.0
154
+ - @checkstack/ui@1.16.1
155
+ - @checkstack/frontend-api@0.10.0
156
+ - @checkstack/gitops-frontend@0.5.8
157
+ - @checkstack/integration-common@0.9.1
158
+ - @checkstack/script-packages-frontend@0.3.12
159
+ - @checkstack/secrets-frontend@0.2.7
160
+ - @checkstack/template-engine@0.4.5
161
+ - @checkstack/signal-frontend@0.2.5
162
+
3
163
  ## 0.6.1
4
164
 
5
165
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/automation-frontend",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "main": "src/index.tsx",
@@ -13,19 +13,20 @@
13
13
  "lint:code": "eslint . --max-warnings 0"
14
14
  },
15
15
  "dependencies": {
16
- "@checkstack/auth-common": "0.9.1",
17
- "@checkstack/automation-common": "0.6.0",
18
- "@checkstack/ai-common": "0.4.0",
19
- "@checkstack/catalog-common": "2.3.6",
20
- "@checkstack/common": "0.15.0",
21
- "@checkstack/frontend-api": "0.9.0",
22
- "@checkstack/gitops-frontend": "0.5.7",
23
- "@checkstack/integration-common": "0.9.0",
24
- "@checkstack/script-packages-frontend": "0.3.11",
25
- "@checkstack/secrets-frontend": "0.2.6",
26
- "@checkstack/signal-frontend": "0.2.4",
27
- "@checkstack/template-engine": "0.4.4",
28
- "@checkstack/ui": "1.16.0",
16
+ "@checkstack/auth-common": "0.10.0",
17
+ "@checkstack/auth-frontend": "0.8.0",
18
+ "@checkstack/automation-common": "0.7.0",
19
+ "@checkstack/ai-common": "0.5.0",
20
+ "@checkstack/catalog-common": "2.4.0",
21
+ "@checkstack/common": "0.16.0",
22
+ "@checkstack/frontend-api": "0.10.0",
23
+ "@checkstack/gitops-frontend": "0.5.8",
24
+ "@checkstack/integration-common": "0.9.1",
25
+ "@checkstack/script-packages-frontend": "0.3.12",
26
+ "@checkstack/secrets-frontend": "0.2.7",
27
+ "@checkstack/signal-frontend": "0.2.5",
28
+ "@checkstack/template-engine": "0.4.5",
29
+ "@checkstack/ui": "1.16.1",
29
30
  "@dnd-kit/core": "^6.3.1",
30
31
  "@dnd-kit/sortable": "^8.0.0",
31
32
  "@dnd-kit/utilities": "^3.2.2",
@@ -39,6 +40,6 @@
39
40
  "typescript": "^5.0.0",
40
41
  "@types/react": "^19.0.0",
41
42
  "@checkstack/tsconfig": "0.0.7",
42
- "@checkstack/scripts": "0.6.1"
43
+ "@checkstack/scripts": "0.6.2"
43
44
  }
44
45
  }
@@ -50,6 +50,7 @@ import {
50
50
  GitOpsLockBanner,
51
51
  useProvenanceLock,
52
52
  } from "@checkstack/gitops-frontend";
53
+ import { TeamOwnershipPicker, teamCreateErrorMessage } from "@checkstack/auth-frontend";
53
54
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
54
55
  import { AutomationDefinitionEditor } from "../editor/AutomationDefinitionEditor";
55
56
  import { assignDefaultIds } from "../editor/action-helpers";
@@ -112,6 +113,9 @@ const AutomationEditContent: React.FC = () => {
112
113
  const { allowed: hasManageAccess } = accessApi.useAccess(
113
114
  automationAccess.manage,
114
115
  );
116
+ // allowGlobal: reuse the manage access check — global manage permission
117
+ // means the caller may create a resource not scoped to any team.
118
+ const allowGlobal = hasManageAccess;
115
119
 
116
120
  // GitOps provenance lock: when this automation is declaratively managed,
117
121
  // disable manual edits + show a banner. `entityId` is the automation id
@@ -139,6 +143,10 @@ const AutomationEditContent: React.FC = () => {
139
143
  // "not chosen yet" — save is blocked until one is selected.
140
144
  const [runAsApplicationId, setRunAsApplicationId] = React.useState("");
141
145
  const [statusEnabled, setStatusEnabled] = React.useState(true);
146
+ // Owning team — create mode only. null means global (no team owner).
147
+ const [ownerTeamId, setOwnerTeamId] = React.useState<string | null>(null);
148
+ // Inline error for team-create failures (e.g. OWNER_TEAM_REQUIRED).
149
+ const [ownerTeamError, setOwnerTeamError] = React.useState<string | null>(null);
142
150
 
143
151
  // Existing group values for the picker's "pick existing" suggestions.
144
152
  const groupsQuery = client.listAutomationGroups.useQuery();
@@ -299,7 +307,14 @@ const AutomationEditContent: React.FC = () => {
299
307
  resolveRoute(automationRoutes.routes.edit, { automationId: data.id }),
300
308
  );
301
309
  },
302
- onError: (error) => toast.error(extractErrorMessage(error)),
310
+ onError: (error) => {
311
+ const inline = teamCreateErrorMessage(error);
312
+ if (inline) {
313
+ setOwnerTeamError(inline);
314
+ return;
315
+ }
316
+ toast.error(extractErrorMessage(error));
317
+ },
303
318
  });
304
319
 
305
320
  const updateMutation = client.updateAutomation.useMutation({
@@ -343,6 +358,7 @@ const AutomationEditContent: React.FC = () => {
343
358
  };
344
359
 
345
360
  const handleSave = async () => {
361
+ setOwnerTeamError(null);
346
362
  const committed = commitActiveTab();
347
363
  if (!committed) return;
348
364
 
@@ -375,6 +391,7 @@ const AutomationEditContent: React.FC = () => {
375
391
  runAs: runAsApplicationId,
376
392
  status: statusEnabled ? "enabled" : "disabled",
377
393
  definition: merged,
394
+ teamId: ownerTeamId ?? undefined,
378
395
  });
379
396
  } else if (automationId) {
380
397
  updateMutation.mutate({
@@ -555,6 +572,19 @@ const AutomationEditContent: React.FC = () => {
555
572
  disabled={!canManage}
556
573
  showError={!!runAsError}
557
574
  />
575
+ {/* Owning team — shown only when creating a new automation */}
576
+ {isNew && (
577
+ <TeamOwnershipPicker
578
+ value={ownerTeamId}
579
+ onChange={(id) => {
580
+ setOwnerTeamId(id);
581
+ setOwnerTeamError(null);
582
+ }}
583
+ allowGlobal={allowGlobal}
584
+ disabled={!canManage}
585
+ error={ownerTeamError}
586
+ />
587
+ )}
558
588
  <div className="flex items-center justify-between">
559
589
  <Label htmlFor="enabled">Enabled</Label>
560
590
  <Toggle
package/tsconfig.json CHANGED
@@ -10,6 +10,9 @@
10
10
  {
11
11
  "path": "../auth-common"
12
12
  },
13
+ {
14
+ "path": "../auth-frontend"
15
+ },
13
16
  {
14
17
  "path": "../automation-common"
15
18
  },