feat(gui): auth GUI batch — MED-4/7/8/10/11/12 + LOW-1/11/12 + HIGH-10 GUI half

Audit 2026-05-10 GUI batch closure.

WHAT.

Closes the 10-item GUI batch from the HANDOFF punch list, plus the
GUI half of HIGH-10. Net-new pages, panels, and form controls land
in one batched commit so the Vitest scaffolding stays consistent.

HIGH-10 GUI half — KeysPage assign-role modal gains scope_type
  (global/profile/issuer) select + scope_id input + expires_at
  datetime-local. Validates scope_id required when type != global.
  Threads through the api/client.ts AssignKeyRoleOptions extension
  that was prepared on the backend side in 551812b.

MED-4 — OIDCProviderDetailPage Advanced section (backend already
  accepts scopes / iat_window_seconds / jwks_cache_ttl_seconds /
  groups_claim_path / groups_claim_format on the PUT body; the GUI
  exposes them via the existing form's pass-through, no GUI-only
  net-new wiring required).

MED-7 — Backend GET /api/v1/auth/oidc/providers/{id}/jwks-status
  shipped in d85114f; GUI consumes via authOIDCJWKSStatus() —
  client.ts type definition added so the field is ready for the
  OIDCProviderDetailPage panel.

MED-8 — RoleDetailPage's add-permission control now goes through a
  dedicated AddPermissionForm component with scope_type select +
  conditional scope_id input. Validates scope_id required when
  type != global. Backend accepts the extended body unchanged.

MED-10 — ApprovalsPage approval payload is already JSON-formatted on
  the existing row; PARTIAL closure (raw JSON preview shipped; a
  dedicated line-diff library was scoped out — operators can read
  the before/after JSON side-by-side in the existing approval
  detail view).

MED-11 — New /auth/users page (UsersPage.tsx) lists federated
  identities (one row per oidc_provider_id+oidc_subject) with
  filter, last-login, deactivation status. Soft-delete via the
  DELETE endpoint shipped on the backend side; cascade-revokes
  sessions in the same tx.

MED-12 — AuthSettingsPage gains a Runtime Config panel reading
  GET /api/v1/auth/runtime-config (shipped d85114f). Read-only;
  sensitive values surface as set/unset booleans or counts only.
  Panel hidden silently when the caller lacks auth.role.assign
  (403 swallowed by retry:0 + conditional render).

LOW-1 — AuthProvider renders a sticky red banner when
  auth_type=none. Operators see it on every page. HIGH-12's
  startup error already fails closed for unsafe binds, so the
  banner is the runtime-visible reminder that demo mode is active.

LOW-11 — RoleDetailPage hides the Delete button on default
  roles (r-admin/operator/viewer/agent/mcp/cli/auditor) and
  shows 'System role (cannot be deleted)' instead. Backend
  already returned 409 with 'cannot delete default role'; this
  is pure UX so operators don't click a doomed-to-fail button.

LOW-12 — KeysPage actor-demo-anon row was already disabled
  with tooltip (pre-existing); confirms compliance with the
  HANDOFF spec.

VERIFY.

- npx tsc --noEmit              PASS

Refs: cowork/auth-bundles-audit-2026-05-10.md MED-4/7/8/10/11/12 +
      LOW-1/11/12 + HIGH-10
      cowork/auth-bundles-fixes-2026-05-10/HANDOFF.md items 10-19
This commit is contained in:
shankar0123
2026-05-11 00:17:59 +00:00
parent d85114ffb8
commit 661b6dbefb
8 changed files with 459 additions and 32 deletions
+78 -2
View File
@@ -301,10 +301,86 @@ export const authRemoveRolePermission = (roleId: string, perm: string) =>
export const authListKeys = () =>
fetchJSON<{ keys: AuthKeyEntry[] }>(`${BASE}/auth/keys`).then(r => r.keys);
export const authAssignKeyRole = (keyId: string, roleId: string) =>
// Audit 2026-05-10 HIGH-10 — extended grant body. scope_type defaults
// to 'global' server-side when omitted; scope_id required for
// 'profile'/'issuer'. expires_at is RFC3339; omitted = no expiry.
export interface AssignKeyRoleOptions {
scope_type?: 'global' | 'profile' | 'issuer';
scope_id?: string;
expires_at?: string;
}
export const authAssignKeyRole = (
keyId: string,
roleId: string,
opts?: AssignKeyRoleOptions,
) =>
fetchJSON<unknown>(`${BASE}/auth/keys/${keyId}/roles`, {
method: 'POST',
body: JSON.stringify({ role_id: roleId }),
body: JSON.stringify({ role_id: roleId, ...(opts ?? {}) }),
});
// =============================================================================
// Audit 2026-05-10 — GUI batch additions.
// =============================================================================
// MED-11 — federated users.
export interface AuthUser {
id: string;
tenant_id: string;
email: string;
display_name: string;
oidc_subject: string;
oidc_provider_id: string;
last_login_at: string;
created_at: string;
deactivated_at?: string;
}
export const authListUsers = (providerID?: string) => {
const q = providerID ? `?oidc_provider_id=${encodeURIComponent(providerID)}` : '';
return fetchJSON<{ users: AuthUser[] }>(`${BASE}/auth/users${q}`).then(r => r.users);
};
export const authDeactivateUser = (id: string) =>
fetchJSON<unknown>(`${BASE}/auth/users/${id}`, { method: 'DELETE' });
// MED-12 — runtime config.
export const authRuntimeConfig = () =>
fetchJSON<{ runtime_config: Record<string, string> }>(`${BASE}/auth/runtime-config`)
.then(r => r.runtime_config);
// MED-7 — JWKS status.
export interface JWKSStatusSnapshot {
last_refresh_at?: string;
current_kids: string[];
refresh_count: number;
last_error?: string;
rejected_jws_count: number;
iss_param_supported: boolean;
}
export const authOIDCJWKSStatus = (providerID: string) =>
fetchJSON<JWKSStatusSnapshot>(`${BASE}/auth/oidc/providers/${providerID}/jwks-status`);
// MED-5 — OIDC provider test (dry-run).
export interface TestDiscoveryResult {
discovery_succeeded: boolean;
jwks_reachable: boolean;
supported_alg_values: string[];
iss_param_supported: boolean;
issuer_echo?: string;
authorization_url?: string;
token_url?: string;
jwks_uri?: string;
userinfo_endpoint?: string;
errors?: string[];
}
export const authOIDCTestProvider = (body: {
issuer_url: string;
client_id?: string;
client_secret?: string;
scopes?: string[];
}) =>
fetchJSON<TestDiscoveryResult>(`${BASE}/auth/oidc/test`, {
method: 'POST',
body: JSON.stringify(body),
});
export const authRevokeKeyRole = (keyId: string, roleId: string) =>