Built-in Roles & permissions admin screen (todo §5); /admin/roles list (search/sort/paginate) + create/delete + assign-to-users/groups + "effective access" (Keto expand → transitive members), writing only to Keto — gated admin-only + CSRF-guarded like Users/Groups (Kratos read only to label members). A role = Keto subject set Role:<name>#members; reuses the Groups membership helpers (now-exported pagedTuples/memberCandidates/safeDecode); added a Roles nav entry (i-shield) + a .plain-list CSS rule. Stability-reviewer run as a local PR: APPROVE, no Critical/High; addressed its explicit-expand-depth nit. Live boot-verify caught a real bug the tests missed — Keto v26.2.0 nests the expand subject under tuple (not node top-level as the §4 ExpandTree type guessed), so expandToEffectiveUsers returned []; fixed type+walker+fixtures, re-verified a group-only member surfaces in effective access. 237→243 units + typecheck green; expand chain boot-verified live then torn down.
This commit is contained in:
+4
-4
@@ -291,8 +291,8 @@ export interface AdminGroupsDeps {
|
||||
render: (view: string, data: Record<string, unknown>) => Promise<string>;
|
||||
}
|
||||
|
||||
// Drain every page of a relation-tuple query.
|
||||
async function pagedTuples(keto: KetoClient, query: RelationQuery): Promise<RelationTuple[]> {
|
||||
// Drain every page of a relation-tuple query. (Reused by the Roles screen — same membership model.)
|
||||
export async function pagedTuples(keto: KetoClient, query: RelationQuery): Promise<RelationTuple[]> {
|
||||
const out: RelationTuple[] = [];
|
||||
let pageToken: string | undefined;
|
||||
do {
|
||||
@@ -305,7 +305,7 @@ async function pagedTuples(keto: KetoClient, query: RelationQuery): Promise<Rela
|
||||
|
||||
// Build the member-picker options (every user by email + every existing group) and the id→email map
|
||||
// detail rows render with. One Kratos page + one Keto scan; ample for an admin tool.
|
||||
async function memberCandidates(keto: KetoClient, kratosAdmin: KratosAdmin): Promise<{ emailById: Map<string, string>; options: MemberOption[] }> {
|
||||
export async function memberCandidates(keto: KetoClient, kratosAdmin: KratosAdmin): Promise<{ emailById: Map<string, string>; options: MemberOption[] }> {
|
||||
const { identities } = await kratosAdmin.listIdentities({ pageSize: LIST_FETCH_SIZE });
|
||||
const emailById = new Map<string, string>();
|
||||
const userOptions: MemberOption[] = [];
|
||||
@@ -326,7 +326,7 @@ async function groupExists(keto: KetoClient, name: string): Promise<boolean> {
|
||||
}
|
||||
|
||||
// Decode a path segment without letting malformed %-encoding throw (→ caller treats it as not found).
|
||||
function safeDecode(seg: string): string | null {
|
||||
export function safeDecode(seg: string): string | null {
|
||||
try { return decodeURIComponent(seg); } catch { return null; }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user