Answer prose round 1: 0 🔴, 1 🟡 (0 misread); the secrets checklist reads in one pass, and four comments shrink or say granted
This commit is contained in:
+3
-3
@@ -8,9 +8,9 @@ at boot. Entries start at 0.3.0.
|
|||||||
|
|
||||||
### Discovery refuses a plugin with the id `error`
|
### Discovery refuses a plugin with the id `error`
|
||||||
|
|
||||||
`/error` is the page Kratos sends a failed sign-in flow to, and a plugin named `error` would shadow
|
`/error` is the page Kratos sends a failed self-service flow to, and a plugin named `error` would
|
||||||
it. Discovery now refuses that id at boot, like the other reserved ids. Rename such a plugin's folder
|
shadow it. Discovery now refuses that id at boot, like the other reserved ids. Rename such a plugin's
|
||||||
before upgrading.
|
folder before upgrading.
|
||||||
|
|
||||||
### The admin's "group already exists" refusal is translated
|
### The admin's "group already exists" refusal is translated
|
||||||
|
|
||||||
|
|||||||
@@ -417,7 +417,7 @@ URL/path-safe (`isValidPluginId`: lowercase `a–z`, digits, dashes) and also na
|
|||||||
`views/`, its `/public/<id>/` assets, and by convention its nav/permission names.
|
`views/`, its `/public/<id>/` assets, and by convention its nav/permission names.
|
||||||
|
|
||||||
`RESERVED_PLUGIN_IDS` are refused at discovery — the gated `dashboard`, the Kratos auth flows
|
`RESERVED_PLUGIN_IDS` are refused at discovery — the gated `dashboard`, the Kratos auth flows
|
||||||
(`auth`, `login`, `logout`, `recovery`, `registration`, `settings`, `verification`), the `oauth2`
|
(`auth`, `login`, `logout`, `recovery`, `registration`, `settings`, `verification`) and their `error` page, the `oauth2`
|
||||||
provider routes, and `public` — since plugin routes resolve first and a folder claiming one would
|
provider routes, and `public` — since plugin routes resolve first and a folder claiming one would
|
||||||
silently shadow a built-in. `admin` is **not** reserved: the admin screens are themselves a plugin.
|
silently shadow a built-in. `admin` is **not** reserved: the admin screens are themselves a plugin.
|
||||||
|
|
||||||
@@ -1271,9 +1271,9 @@ override sets `APP_URL=http://localhost:3000`, and `127.0.0.1` is canonicalised
|
|||||||
A clean clone needs **none** of the above. What can't be auto-generated is **production-only**:
|
A clean clone needs **none** of the above. What can't be auto-generated is **production-only**:
|
||||||
|
|
||||||
1. **Production secrets** — every value below ships as a committed dev throwaway and **must** be
|
1. **Production secrets** — every value below ships as a committed dev throwaway and **must** be
|
||||||
replaced before a deploy faces the internet. Of these, `REQUIRE_SECURE_SECRETS=true` checks only
|
replaced before a deploy faces the internet. Of them, `REQUIRE_SECURE_SECRETS=true` checks only
|
||||||
`CSRF_SECRET` (its [Configuration](#configuration) row lists the other secrets it checks); the
|
`CSRF_SECRET`, and the flag's [Configuration](#configuration) row lists what else it checks; the
|
||||||
rest fail silently, so treat this as a checklist.
|
rest fail silently, so treat the table as a checklist.
|
||||||
|
|
||||||
| Secret | Where | Protects |
|
| Secret | Where | Protects |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ export function identityPayload(email: string, password: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
export function permissionTuple(userId: string, permission: string) {
|
||||||
return { namespace: "Permission", object: permission, relation: "granted", subject_id: `user:${userId}` };
|
return { namespace: "Permission", object: permission, relation: "granted", subject_id: `user:${userId}` };
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -39,7 +39,7 @@ export interface CompletedLogin {
|
|||||||
|
|
||||||
// The coarse permissions a user holds — directly (`Permission:<name>#granted@user:<id>`) or transitively via a
|
// 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
|
// 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 —
|
// namespace) and asks Keto to check each grant, 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
|
// 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.
|
// request; permission count is small, so the per-permission checks are cheap and run in parallel.
|
||||||
export async function readPermissions(keto: KetoClient, userId: string): Promise<string[]> {
|
export async function readPermissions(keto: KetoClient, userId: string): Promise<string[]> {
|
||||||
|
|||||||
+2
-2
@@ -117,8 +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
|
// 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
|
// 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
|
// where Kratos redirects. Kratos' csrf_token field guards the POST, so the host's own CSRF token
|
||||||
// isn't checked here. `submitFlow` throws only on a 5xx or an unreachable Kratos, so of
|
// isn't checked here. `submitFlow` throws only on a 5xx or an unreachable Kratos, so only
|
||||||
// `flowFailure`'s branches only the 503 fires here.
|
// `flowFailure`'s 503 branch can fire here.
|
||||||
function codeFlowSubmit(kratos: KratosPublic, flowType: FlowType, secureCookies: boolean): BuiltinRoute["handler"] {
|
function codeFlowSubmit(kratos: KratosPublic, flowType: FlowType, secureCookies: boolean): BuiltinRoute["handler"] {
|
||||||
return async (ctx: RequestContext): Promise<RouteResult> => {
|
return async (ctx: RequestContext): Promise<RouteResult> => {
|
||||||
const pathname = ctx.url.pathname;
|
const pathname = ctx.url.pathname;
|
||||||
|
|||||||
@@ -139,9 +139,8 @@ test("findConflicts: each single slot (`home`/`dashboard`) may have one owner
|
|||||||
assert.deepEqual(findConflicts([p({ id: "a", dashboard: handler, home: handler }), p({ id: "b" })]).filter((c) => c.kind === "home" || c.kind === "dashboard"), []);
|
assert.deepEqual(findConflicts([p({ id: "a", dashboard: handler, home: handler }), p({ id: "b" })]).filter((c) => c.kind === "home" || c.kind === "dashboard"), []);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Drift guard: RESERVED_PLUGIN_IDS is a hand-maintained mirror of the host's own top-level mounts —
|
// Drift guard: every mount but the auth flows is listed by hand, so a new top-level built-in route
|
||||||
// a folder claiming one would silently shadow a built-in route. Only the auth flows derive from
|
// needs a line here too.
|
||||||
// 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", () => {
|
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 seg = (path: string): string => path.split("/")[1] ?? ""; // first segment of "/x/y"
|
||||||
const builtins = new Set<string>([
|
const builtins = new Set<string>([
|
||||||
|
|||||||
Reference in New Issue
Block a user