Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions packages/settings-ui/src/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@
// only, never as visible text.
//
// `PRINCIPAL_KIND_LABEL` and `PRINCIPAL_KIND_ORDER` live here too, shared by
// every picker that lists principals (Grants' target select and filter,
// Roles' assignment select): Grants/Roles assign to people, agents, *and*
// workflows (see `people-section.tsx`'s own header comment), and a picker
// that shows only names with no kind is kind-blind — a workflow's machine
// principal can look identical to a person's account. Every such picker
// must show which kind an option is, not just its name.
// every picker that lists principals (Grants' target select and filter).
// Grants assign to people, agents, *and* workflows. Roles' assignment picker
// is scoped to user-kind principals only (CL-6664): agents/workflows are
// machine identities that belong on separate surfaces.

import { SETTINGS_STRINGS } from "./strings";

Expand Down
34 changes: 13 additions & 21 deletions packages/settings-ui/src/roles-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,7 @@ import {
UnauthenticatedError,
describeQueryError,
} from "@corbits/api-query";
import {
PRINCIPAL_KIND_LABEL,
PRINCIPAL_KIND_ORDER,
principalLabel,
} from "./identity";
import { principalLabel } from "./identity";
import { SETTINGS_STRINGS } from "./strings";
import {
assignRole,
Expand Down Expand Up @@ -317,7 +313,13 @@ export function RoleAssignments({
const [principalId, setPrincipalId] = useState("");
const [roleId, setRoleId] = useState("");

const assignments = principals.flatMap((principal) =>
// CL-6664: Scope both picker and assignments to user-kind principals only.
// Agents and workflows are machine identities — the "Person" picker and
// its assignment table should match the People section's member roster,
// not the full tenant-wide principal list.
const people = principals.filter((p) => p.kind === "user");

const assignments = people.flatMap((principal) =>
principal.roles.map((role) => ({ principal, role })),
);

Expand All @@ -333,21 +335,11 @@ export function RoleAssignments({
onChange={(event) => setPrincipalId(event.target.value)}
>
<option value="">—</option>
{PRINCIPAL_KIND_ORDER.map((kind) => {
const kindPrincipals = principals.filter(
(principal) => principal.kind === kind,
);
if (kindPrincipals.length === 0) return null;
return (
<optgroup key={kind} label={PRINCIPAL_KIND_LABEL[kind]}>
{kindPrincipals.map((principal) => (
<option key={principal.id} value={principal.id}>
{principalLabel(principal.displayName).label}
</option>
))}
</optgroup>
);
})}
{people.map((principal) => (
<option key={principal.id} value={principal.id}>
{principalLabel(principal.displayName).label}
</option>
))}
</select>
</label>
<label className="settings-form-field">
Expand Down
65 changes: 46 additions & 19 deletions packages/settings-ui/test/roles-section.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
// CL-6077: the "Assign a role" person picker previously listed every
// principalpeople, agents, and workflows — as flat, indistinguishable
// options. Grouping by kind (optgroup) means a workflow's machine
// principal can never be mistaken for a person's account in the picker.
// CL-6664: the "Assign a role" person picker must show only user-kind
// principalsthe same roster as the People section. Agent/workflow
// machine identities are excluded to prevent placeholder-named garbage
// accounts from polluting the picker.

import { describe, expect, test } from "bun:test";
import { renderToStaticMarkup } from "react-dom/server";

import { PRINCIPAL_KIND_LABEL, PRINCIPAL_KIND_ORDER } from "../src/identity";
import { RoleAssignments } from "../src/roles-section";

const timestamps = {
Expand All @@ -32,7 +31,7 @@ function principal(
}

describe("RoleAssignments picker", () => {
test("groups the person select by principal kind, in user/agent/workflow order", () => {
test("shows only user-kind principals, not agents or workflows", () => {
const markup = renderToStaticMarkup(
<RoleAssignments
roles={[
Expand All @@ -45,37 +44,65 @@ describe("RoleAssignments picker", () => {
},
]}
principals={[
principal("workflow", "prn_wf", "Nightly digest"),
principal("user", "prn_user", "Alice Anderson"),
principal("agent", "prn_agent", "Research Assistant"),
principal("workflow", "prn_wf", "Nightly digest"),
]}
onAssign={() => undefined}
onUnassign={() => undefined}
/>,
);

const groupOrder = [...markup.matchAll(/<optgroup label="([^"]+)"/g)].map(
(match) => match[1],
);
expect(groupOrder).toEqual(
PRINCIPAL_KIND_ORDER.map((kind) => PRINCIPAL_KIND_LABEL[kind]),
expect(markup).toContain("Alice Anderson");
expect(markup).not.toContain("Research Assistant");
expect(markup).not.toContain("Nightly digest");
expect(markup).not.toContain("<optgroup");
});

test("shows no optgroups (flat list)", () => {
const markup = renderToStaticMarkup(
<RoleAssignments
roles={[]}
principals={[
principal("user", "prn_1", "Alice Anderson"),
principal("user", "prn_2", "Bob Baker"),
]}
onAssign={() => undefined}
onUnassign={() => undefined}
/>,
);
expect(groupOrder).not.toEqual([...PRINCIPAL_KIND_ORDER]);
expect(markup).not.toContain("<optgroup");
expect(markup).toContain("Alice Anderson");
expect(markup).toContain("Bob Baker");
});

test("omits an empty kind group entirely rather than an empty optgroup", () => {
test("excludes agents from the assignments table too", () => {
const markup = renderToStaticMarkup(
<RoleAssignments
roles={[]}
principals={[principal("user", "prn_user", "Alice Anderson")]}
principals={[
principal("user", "prn_user", "Alice Anderson"),
principal("agent", "prn_agent", "Research Assistant"),
]}
onAssign={() => undefined}
onUnassign={() => undefined}
/>,
);
const groupOrder = [...markup.matchAll(/<optgroup label="([^"]+)"/g)].map(
(match) => match[1],
// The assignments table header should exist but have no rows
expect(markup).toContain("No one has been assigned a role yet.");
expect(markup).not.toContain("Research Assistant");
});

test("does not show user kind label for single-kind list", () => {
const markup = renderToStaticMarkup(
<RoleAssignments
roles={[]}
principals={[principal("user", "prn_1", "Alice Anderson")]}
onAssign={() => undefined}
onUnassign={() => undefined}
/>,
);
expect(groupOrder).toEqual([PRINCIPAL_KIND_LABEL.user]);
expect(groupOrder).not.toEqual(["user"]);
expect(markup).toContain("Alice Anderson");
expect(markup).not.toContain("<optgroup");
});
});
Loading