Bump the contract for the new gate, and close the ways it could read as open
CI / full-gate (push) Successful in 2m45s
CI / full-gate (push) Successful in 2m45s
This commit is contained in:
@@ -64,6 +64,8 @@ const badCases: Array<{ name: string; files: Record<string, string>; match: RegE
|
||||
{ name: "a nav node marked public AND permission is contradictory", files: { "contranav/plugin.ts": `export default { apiVersion: "${HOST_API_VERSION}", nav: [{ id: "n", label: "N", public: true, permission: "x:read" }] };` }, match: /contranav.*public.*permission/s },
|
||||
{ name: "a route marked session AND permission is contradictory", files: { "contrasess/plugin.ts": `export default { apiVersion: "${HOST_API_VERSION}", routes: [{ method: "GET", path: "/", session: true, permission: "x:read", handler: () => ({ html: "x" }) }] };` }, match: /contrasess.*session.*permission/s },
|
||||
{ name: "a route marked public AND session is contradictory", files: { "contrapub/plugin.ts": `export default { apiVersion: "${HOST_API_VERSION}", routes: [{ method: "GET", path: "/", public: true, session: true, handler: () => ({ html: "x" }) }] };` }, match: /contrapub.*public.*session/s },
|
||||
{ name: "a route whose session flag is a truthy non-boolean is refused, not read as ungated", files: { "truthy/plugin.ts": `export default { apiVersion: "${HOST_API_VERSION}", routes: [{ method: "GET", path: "/", session: "yes", handler: () => ({ html: "x" }) }] };` }, match: /truthy.*session.*true/s },
|
||||
{ name: "a nav node whose public flag is a truthy non-boolean is refused too", files: { "truthynav/plugin.ts": `export default { apiVersion: "${HOST_API_VERSION}", nav: [{ id: "n", label: "N", public: 1 }] };` }, match: /truthynav.*public.*true/s },
|
||||
{ name: "a nav node marked session AND permission is contradictory", files: { "contrasessnav/plugin.ts": `export default { apiVersion: "${HOST_API_VERSION}", nav: [{ id: "n", label: "N", session: true, permission: "x:read" }] };` }, match: /contrasessnav.*session.*permission/s },
|
||||
// A permission name is <resource>:<action> wherever the manifest mentions one. Enforced here, not
|
||||
// only in the admin GUI, so it holds for a plugin installed without that GUI.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { gatesSet } from "../auth/gate.ts";
|
||||
import { type Gate, gatesSet } from "../auth/gate.ts";
|
||||
import { checkApiVersion, findConflicts, isValidPermissionName, isValidPluginId, RESERVED_PLUGIN_IDS, type Plugin, type PluginManifest } from "./plugin.ts";
|
||||
import { settingsDeclError } from "./settings.ts";
|
||||
import { isValidStoragePluginId, MAX_STORAGE_PLUGIN_ID_LENGTH } from "./storage.ts";
|
||||
@@ -147,9 +147,9 @@ function shapeError(manifest: PluginManifest): string | null {
|
||||
const settings = settingsDeclError(manifest.settings);
|
||||
if (settings) return settings;
|
||||
}
|
||||
// Two gates on one route or nav node contradict each other — "open to all" vs "needs a session"
|
||||
// vs "needs this permission". Refuse rather than silently pick one, so intent stays unambiguous.
|
||||
for (const route of Array.isArray(manifest.routes) ? manifest.routes : []) {
|
||||
const flag = gateFlagError(`route "${route?.method} ${route?.path}"`, route);
|
||||
if (flag) return flag;
|
||||
const gates = gatesSet(route);
|
||||
if (gates.length > 1) return `route "${route?.method} ${route?.path}" sets ${gates.join(" and ")}; a route names exactly one gate — public, session or permission`;
|
||||
}
|
||||
@@ -172,9 +172,19 @@ function shapeError(manifest: PluginManifest): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Recurse the nav fragment: a node naming more than one gate is contradictory, same as a route.
|
||||
// A truthy non-boolean sets no gate at all, so `session: "yes"` would read as an open page.
|
||||
function gateFlagError(what: string, gate: Gate | null | undefined): string | null {
|
||||
for (const flag of ["public", "session"] as const) {
|
||||
const value = gate?.[flag];
|
||||
if (value !== undefined && typeof value !== "boolean") return `${what} sets ${flag} to ${JSON.stringify(value)}; a gate is declared with \`true\``;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findNavGateContradiction(nodes: PluginManifest["nav"]): string | null {
|
||||
for (const node of Array.isArray(nodes) ? nodes : []) {
|
||||
const flag = gateFlagError(`nav node "${node?.label ?? node?.id ?? "?"}"`, node);
|
||||
if (flag) return flag;
|
||||
const gates = gatesSet(node);
|
||||
if (gates.length > 1) return `nav node "${node?.label ?? node?.id ?? "?"}" sets ${gates.join(" and ")}; a node names exactly one gate — public, session or permission`;
|
||||
const inChild = findNavGateContradiction(node?.children);
|
||||
|
||||
@@ -39,7 +39,6 @@ export { CSRF_FIELD } from "../auth/csrf.ts";
|
||||
// reference consumer. The Ory client types + their error classes are re-exported so a system
|
||||
// plugin can type against them and `instanceof`-match their errors. See README → System capabilities.
|
||||
export type { SystemCapabilities } from "./system.ts";
|
||||
export type { Gate } from "../auth/gate.ts";
|
||||
export type { Identity, KratosAdmin, RecoveryCode } from "../auth/kratos-admin.ts";
|
||||
export type { ExpandTree, KetoClient, RelationQuery, RelationTuple, SubjectSet } from "../auth/keto-client.ts";
|
||||
export type { HydraAdmin, OAuth2Client } from "../auth/hydra-admin.ts";
|
||||
|
||||
@@ -11,7 +11,7 @@ import { envName, type SettingDecl, type SettingsOf } from "./settings.ts";
|
||||
import type { StorageCredentials } from "./storage.ts";
|
||||
|
||||
// The Plainpages release this contract ships in — see README → Contract versioning.
|
||||
export const HOST_API_VERSION = "0.3.0";
|
||||
export const HOST_API_VERSION = "0.4.0";
|
||||
|
||||
export type HttpMethod = "DELETE" | "GET" | "HEAD" | "PATCH" | "POST" | "PUT";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user