Plan 0.5.0 from the ten highest-scoring items; reserve the plugin id error, translate the duplicate-group refusal, and correct four comments and README passages
This commit is contained in:
@@ -28,7 +28,7 @@ export function identityPayload(email: string, password: string) {
|
||||
};
|
||||
}
|
||||
|
||||
// Coarse-permission grant: `Permission:<permission>#members@user:<id>`. Subject ids are `user:<kratos-id>`
|
||||
// Coarse-permission grant: `Permission:<permission>#granted@user:<id>`. Subject ids are `user:<kratos-id>`
|
||||
// (namespaces.keto.ts) — the source of truth the login flow projects into the JWT permissions.
|
||||
export function permissionTuple(userId: string, permission: string) {
|
||||
return { namespace: "Permission", object: permission, relation: "granted", subject_id: `user:${userId}` };
|
||||
|
||||
+2
-2
@@ -37,8 +37,8 @@ export interface CompletedLogin {
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
// The coarse permissions a user holds — directly (`Permission:<name>#members@user:<id>`) or transitively via a
|
||||
// group that is a member of the permission. Enumerates the defined permissions (the distinct objects in the Permission
|
||||
// The coarse permissions a user holds — directly (`Permission:<name>#granted@user:<id>`) or transitively via a
|
||||
// group granted the permission. Enumerates the defined permissions (the distinct objects in the Permission
|
||||
// namespace) and asks Keto to resolve each membership, so a permission granted to a group reaches the JWT —
|
||||
// matching the OPL model and the admin "Effective access" view. At login/refresh only, never per
|
||||
// request; permission count is small, so the per-permission checks are cheap and run in parallel.
|
||||
|
||||
+2
-1
@@ -117,7 +117,8 @@ function flowPage(kratos: KratosPublic, flowType: FlowType, secureCookies: boole
|
||||
// A code flow's POST (flow-view.ts → CODE_FLOWS). Trims the pasted one-time code, since Kratos
|
||||
// refuses one with whitespace around it, then passes the form on to Kratos and sends the browser
|
||||
// where Kratos redirects. Kratos' csrf_token field guards the POST, so the host's own CSRF token
|
||||
// isn't checked here.
|
||||
// isn't checked here. `submitFlow` throws only on a 5xx or an unreachable Kratos, so of
|
||||
// `flowFailure`'s branches only the 503 fires here.
|
||||
function codeFlowSubmit(kratos: KratosPublic, flowType: FlowType, secureCookies: boolean): BuiltinRoute["handler"] {
|
||||
return async (ctx: RequestContext): Promise<RouteResult> => {
|
||||
const pathname = ctx.url.pathname;
|
||||
|
||||
@@ -191,7 +191,9 @@ test("admin Groups screen: gate, list, create, detail/membership, delete (CSRF-g
|
||||
// An invalid name, a duplicate name, or a missing CSRF token are all refused, nothing written.
|
||||
const before = tuples.length;
|
||||
assert.equal((await post("/admin/groups", `_csrf=${token}&name=Bad Name&member=user:${grace}`)).status, 400);
|
||||
assert.equal((await post("/admin/groups", `_csrf=${token}&name=eng&member=user:${grace}`)).status, 400); // already exists
|
||||
const duplicate = await post("/admin/groups?locale=sv-SE", `_csrf=${token}&name=eng&member=user:${grace}`);
|
||||
assert.equal(duplicate.status, 400);
|
||||
assert.match(await duplicate.text(), /Det finns redan en grupp med det namnet\./);
|
||||
assert.equal((await post("/admin/groups", `name=x&member=user:${grace}`)).status, 403);
|
||||
assert.equal(tuples.length, before);
|
||||
|
||||
|
||||
@@ -140,8 +140,8 @@ test("findConflicts: each single slot (`home`/`dashboard`) may have one owner
|
||||
});
|
||||
|
||||
// Drift guard: RESERVED_PLUGIN_IDS is a hand-maintained mirror of the host's own top-level mounts —
|
||||
// a folder claiming one would silently shadow a built-in route. Derive the segments from the real
|
||||
// route constants so adding a new auth flow or provider route without reserving its id fails here.
|
||||
// a folder claiming one would silently shadow a built-in route. Only the auth flows derive from
|
||||
// AUTH_FLOWS; every other mount is listed by hand, so a new top-level route needs a line here too.
|
||||
test("RESERVED_PLUGIN_IDS covers every built-in top-level mount; `home` (the / field) and `admin` (a plugin) are NOT reserved", () => {
|
||||
const seg = (path: string): string => path.split("/")[1] ?? ""; // first segment of "/x/y"
|
||||
const builtins = new Set<string>([
|
||||
@@ -151,6 +151,7 @@ test("RESERVED_PLUGIN_IDS covers every built-in top-level mount; `home` (the / f
|
||||
"oauth2", // /oauth2/login · /consent · /logout (Hydra provider)
|
||||
"dashboard", // the gated app home
|
||||
"public", // static assets
|
||||
"error", // /error (Kratos' error sink)
|
||||
]);
|
||||
for (const id of builtins) assert.ok(RESERVED_PLUGIN_IDS.has(id), `built-in mount "${id}" must be a reserved plugin id`);
|
||||
// "/" is owned by the `home` manifest field (not a /<id> route), so it cannot be shadowed and is
|
||||
|
||||
@@ -124,7 +124,7 @@ export function isValidPluginId(id: string): boolean {
|
||||
// one — discovery refuses it. "/" is owned by the `home` field, not a route, so it needs no
|
||||
// reservation; `admin` is deliberately absent, the admin screens being a drop-in plugin.
|
||||
export const RESERVED_PLUGIN_IDS: ReadonlySet<string> = new Set([
|
||||
"auth", "dashboard", "login", "logout", "oauth2", "public", "recovery", "registration", "settings", "verification",
|
||||
"auth", "dashboard", "error", "login", "logout", "oauth2", "public", "recovery", "registration", "settings", "verification",
|
||||
]);
|
||||
|
||||
export interface Semver {
|
||||
|
||||
Reference in New Issue
Block a user