Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/roles-and-permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"seamless-auth-admin-dashboard": minor
---

Add a Roles page to manage the permission catalogue, the organization role catalogue, and what each instance and organization role grants. Every write asks for step-up, read-only admins see the model without the controls, and a refused delete or mapping names what stood in the way. The page states that a change reaches a user with their next access token, and that an instance `admin` and an organization `admin` never share a grant. Against an API older than v0.23 it says the API needs upgrading instead of retrying an error.

Organization member roles are now picked from the organization role catalogue. A legacy role a member already holds stays visible and is kept, and a refused grant names the roles. The audit events for permissions, roles, members and session exchange have categories and labels in the Events view. `@seamless-auth/types` moves to 0.38.
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ Current route model:
- `/users`
- `/users/:id`
- `/organizations`
- `/roles`
- `/applications`
- `/resources`
- `/enrollment`
Expand Down Expand Up @@ -199,7 +200,8 @@ Every admin write the API guards with step-up calls `ensureStepUp()` from `useSt
the mutation. `apiFetch` also handles `403 { error: "step_up_required" }` centrally: it asks
`StepUpRequiredHandler` for a step-up and retries the request once, so a window that lapses
mid-task prompts instead of failing. `ApiError.code` carries the API's `error` code for branching
(for example `policy_version_conflict`).
(for example `policy_version_conflict`). `ApiError.details` carries the API's `details` unparsed;
`describeRefusal` in `src/lib/roleRefusals.ts` turns the role and permission refusals into text.

For a route that answers with a file rather than JSON (the coverage CSV, the audit
export), use `apiDownload(path, fallbackName)` and `saveBlob` from `src/lib/csvExport.ts`.
Expand All @@ -217,6 +219,7 @@ Current page intent:
- `Users.tsx`: searchable user directory with create, edit, and delete actions
- `UserDetail.tsx`: single-user overview, sessions, credentials, events, and anomalies
- `Organizations.tsx`: organization directory, details, and membership management
- `Roles.tsx`: the permission catalogue, organization role catalogue, and what each role grants. Instance roles are listed for their mappings but stay defined in `SystemConfig.tsx`. An API older than v0.23 answers 404 on these routes, which is a state of its own rather than an error
- `Applications.tsx`: OpenID Provider applications (clients): create and edit, public and confidential clients, redirect URI checks (https, loopback, private-use), secret rotation shown once, key registration, and the resources and scopes each application may use. Shared-secret clients are marked as weaker
- `Resources.tsx`: OpenID Provider resources (APIs), their audience identifier, scopes, and token TTL
- `Enrollment.tsx`: passkey enrollment status by user, and enrollment invites
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,17 @@ When the server adapter is mounted at `/auth`, the dashboard sends this as `DELE
- list and inspect organizations
- create and update organizations
- add, update, and remove organization members
- assign members roles from the organization role catalogue, keeping a legacy role a member already holds

### Roles and Permissions

- manage the permission catalogue and the organization role catalogue
- edit which permissions each instance and organization role grants
- instance roles stay defined in System configuration, and the page links there
- a change reaches a user when their next access token is issued, within the access token TTL
- an instance `admin` and an organization `admin` are different roles and never share a grant
- needs Seamless Auth API v0.23, and a server adapter that forwards these routes. An older API answers 404, and the page says so
- every write asks for step-up first

### Applications and Resources

Expand Down
30 changes: 30 additions & 0 deletions e2e/factories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import type {
Organization,
OrganizationMembership,
PartialAuthEvent,
Permission,
PolicyDocument,
PolicyDryRunResponse,
PolicyVersion,
RoleDefinition,
Session,
StepUpStatus,
SystemConfig,
Expand Down Expand Up @@ -205,6 +207,7 @@ export function makeOidcApplication(
postLogoutRedirectUris: [],
grantTypes: ["authorization_code", "refresh_token"],
deviceFlowEnabled: false,
sessionExchangeEnabled: false,
noticesEnabled: false,
pkceRequired: true,
nonceRequired: true,
Expand All @@ -222,6 +225,32 @@ export function makeOidcApplication(
};
}

export function makePermission(
overrides: Partial<Permission> = {},
): Permission {
return {
id: nextId("permission"),
name: "invoices:read",
description: null,
createdAt: isoOffset(-30 * DAY),
updatedAt: isoOffset(-1 * DAY),
...overrides,
};
}

export function makeRoleDefinition(
overrides: Partial<RoleDefinition> = {},
): RoleDefinition {
return {
name: "member",
kind: "organization",
description: null,
builtIn: false,
permissions: [],
...overrides,
};
}

export function makeOidcResource(
overrides: Partial<OidcResource> = {},
): OidcResource {
Expand Down Expand Up @@ -363,6 +392,7 @@ export function makeSystemConfig(
origins: ["http://localhost:4287"],
oidc_provider_enabled: false,
oidc_issuer: null,
oidc_cli_client_id: null,
allow_client_secrets: false,
third_party_clients_enabled: false,
hosted_ui: null,
Expand Down
18 changes: 18 additions & 0 deletions e2e/personas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ import {
makeMeUser,
makeOrganization,
makePolicyDryRun,
makePermission,
makePolicyVersion,
makeRoleDefinition,
makeSession,
makeStepUpStatus,
makeSystemConfig,
Expand Down Expand Up @@ -173,6 +175,22 @@ export function seedDeployment(api: MockApi) {
api.get("/system-config/roles", {
json: { roles: ["user", "admin:read", "admin:write"] },
});
api.get("/admin/permissions", () => {
const permissions = [makePermission({ name: "invoices:read" })];
return { json: { permissions, total: permissions.length } };
});
api.get("/admin/roles", {
json: {
roles: [
...["user", "admin:read", "admin:write"].map((name) =>
makeRoleDefinition({ name, kind: "instance" }),
),
...["admin", "member", "owner"].map((name) =>
makeRoleDefinition({ name, builtIn: true }),
),
],
},
});
api.post("/system-config/oauth-providers", ({ payload }) => ({
provider: payload,
}));
Expand Down
4 changes: 2 additions & 2 deletions e2e/tests/organizations.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,13 @@ test.describe("Organizations", () => {
.click();

await page.getByLabel("Member email").fill("hopper@example.com");
await page.getByLabel("Member roles").selectOption("user");
await page.getByLabel("Member roles").selectOption("admin");
await page.getByRole("button", { name: "Add", exact: true }).click();

await expectToast(page, "Member added");
expect(
api.lastCall("POST", "/admin/organizations/org_1/members")?.payload,
).toMatchObject({ email: "hopper@example.com", roles: ["user"] });
).toMatchObject({ email: "hopper@example.com", roles: ["admin"] });
});

test("removes a member behind a confirmation", async ({
Expand Down
Loading
Loading