Skip to content

Commit b93f247

Browse files
committed
RBAC: extend Permission + RbacResource for CASL conditional rules (TRI-8893)
Pairs with the cloud-side CASL refactor that switches role storage to packed CASL rules + introduces conditional rules (e.g. Member's prod env-var restrictions). Two interface changes here: - Permission gains optional `inverted` and `conditions` fields. The Roles page renders `inverted: true` rules as ✗ and `conditions` (e.g. `{ envType: "PRODUCTION" }`) as a tier badge. - RbacResource gains an open-ended `[key: string]: unknown` index so routes can pass condition-relevant fields alongside `type` / `id` (e.g. `{ type: "envvars", envType: env.type }`). The plugin's CASL-backed matcher reads these off the resource object. Roles page UI: TableHeader gains an "Allowed" column rendering ✓/✗ per rule, and conditional rules show a `(production only)` / `(non-production only)` Badge next to the permission name. Group order gains a leading "All" for Owner/Admin's wildcard rules and an "Environment" group for the new envvars/apiKeys catalogue pairs.
1 parent c4abb4b commit b93f247

2 files changed

Lines changed: 63 additions & 5 deletions

File tree

  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.roles
  • packages/plugins/src

apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.roles/route.tsx

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,30 +179,49 @@ function RoleCard({
179179
<Table containerClassName="border-t-0">
180180
<TableHeader>
181181
<TableRow>
182+
<TableHeaderCell hiddenLabel>Allowed</TableHeaderCell>
182183
<TableHeaderCell>Permission</TableHeaderCell>
183184
<TableHeaderCell>Description</TableHeaderCell>
184185
</TableRow>
185186
</TableHeader>
186187
<TableBody>
187188
{role.permissions.length === 0 ? (
188-
<TableBlankRow colSpan={2}>
189+
<TableBlankRow colSpan={3}>
189190
<Paragraph variant="small" className="text-text-dimmed">
190191
This role has no permissions assigned.
191192
</Paragraph>
192193
</TableBlankRow>
193194
) : (
194195
grouped.flatMap(({ group, permissions }) => [
195196
<TableRow key={`${group}-header`}>
196-
<TableCell colSpan={2} className="bg-charcoal-800">
197+
<TableCell colSpan={3} className="bg-charcoal-800">
197198
<Header3 className="text-xs uppercase tracking-wide text-text-dimmed">
198199
{group}
199200
</Header3>
200201
</TableCell>
201202
</TableRow>,
202-
...permissions.map((permission) => (
203-
<TableRow key={`${role.id}-${permission.name}`}>
203+
...permissions.map((permission, idx) => (
204+
<TableRow key={`${role.id}-${permission.name}-${idx}`}>
205+
<TableCell className="w-8 text-center">
206+
{permission.inverted ? (
207+
<span className="text-error" aria-label="Denied">
208+
209+
</span>
210+
) : (
211+
<span className="text-success" aria-label="Allowed">
212+
213+
</span>
214+
)}
215+
</TableCell>
204216
<TableCell>
205-
<code className="text-xs">{permission.name}</code>
217+
<div className="flex items-center gap-2">
218+
<code className="text-xs">{permission.name}</code>
219+
{permission.conditions ? (
220+
<Badge variant="extra-small">
221+
{formatConditions(permission.conditions)}
222+
</Badge>
223+
) : null}
224+
</div>
206225
</TableCell>
207226
<TableCell>
208227
<Paragraph variant="small">
@@ -234,6 +253,7 @@ const PERMISSION_GROUP_BY_NAME: Record<string, string> = {
234253
"write:tasks": "Tasks",
235254
"trigger:tasks": "Tasks",
236255
"batchTrigger:tasks": "Tasks",
256+
"deploy:tasks": "Tasks",
237257
"read:waitpoints": "Waitpoints",
238258
"write:waitpoints": "Waitpoints",
239259
"read:inputStreams": "Realtime",
@@ -245,12 +265,26 @@ const PERMISSION_GROUP_BY_NAME: Record<string, string> = {
245265
"read:query": "Query",
246266
"read:tokens": "Tokens",
247267
"write:tokens": "Tokens",
268+
"read:envvars": "Environment",
269+
"write:envvars": "Environment",
270+
"read:apiKeys": "Environment",
271+
"write:apiKeys": "Environment",
248272
"read:members": "Organisation",
249273
"manage:members": "Organisation",
250274
"manage:billing": "Organisation",
275+
// System-role meta pairs ("manage:all", "read:all", …) — collapse to
276+
// a single "All" group at the top.
277+
"manage:all": "All",
278+
"read:all": "All",
279+
"write:all": "All",
280+
"trigger:all": "All",
281+
"batchTrigger:all": "All",
282+
"update:all": "All",
283+
"deploy:all": "All",
251284
};
252285

253286
const GROUP_ORDER = [
287+
"All",
254288
"Runs",
255289
"Tasks",
256290
"Waitpoints",
@@ -259,10 +293,22 @@ const GROUP_ORDER = [
259293
"Prompts",
260294
"Query",
261295
"Tokens",
296+
"Environment",
262297
"Organisation",
263298
"Other",
264299
];
265300

301+
// Render a CASL conditions object into a tier badge label. Only one
302+
// condition key is recognised today (envType); extending this requires
303+
// adding a new branch when ALLOWED_CONDITIONS grows.
304+
function formatConditions(conditions: Record<string, unknown>): string {
305+
if (typeof conditions.envType === "string") {
306+
const t = conditions.envType.toLowerCase();
307+
return `${t} only`;
308+
}
309+
return JSON.stringify(conditions);
310+
}
311+
266312
function groupPermissions(
267313
permissions: LoaderPermission[]
268314
): { group: string; permissions: LoaderPermission[] }[] {

packages/plugins/src/rbac.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
export type Permission = {
2+
// `<action>:<subject>` — display name, derived from the ability rule.
23
name: string;
34
description: string;
5+
// Inverted rules (CASL `cannot`) surface as ✗ in the Roles page.
6+
inverted?: boolean;
7+
// CASL conditions (e.g. `{ envType: "PRODUCTION" }`) — when present,
8+
// the Roles page renders a tier badge alongside the permission row.
9+
conditions?: Record<string, unknown>;
410
};
511

612
export type Role = {
@@ -19,6 +25,12 @@ export type RbacSubject =
1925
export type RbacResource = {
2026
type: string;
2127
id?: string;
28+
// Extra fields a route may pass for condition-based ability checks —
29+
// e.g. `envType` for env-tier-scoped rules ("Member can read envvars
30+
// unless envType === 'PRODUCTION'"). The plugin's ability matcher
31+
// (CASL) reads these off the resource object; routes that don't use
32+
// conditional rules can keep passing `{ type, id? }`.
33+
[key: string]: unknown;
2234
};
2335

2436
export type RbacEnvironment = {

0 commit comments

Comments
 (0)