# Permissions example: admin access

Internal repo artifact · Inspired by Frustration · 2026-10-21 snapshot

Source commit: `c9cd4790e42c99b35eed3731bd2c6a5a44c941d8`

Not a client outcome or a model-quality benchmark.

These are real source extracts from our own application. No customer data, credentials or generated screenshots are included. Checksums identify the complete source files at the snapshot; excerpts below are unmodified.

## What to inspect

The marketing app checks a verified user, an email allowlist and the admin role. Missing email, a failed allowlist check, a missing role, query errors and exceptions deny access. Signing in with a different method does not grant a broader role. The accompanying guard scans the three admin gates for authentication-method and session-holder-writable metadata reads.

## Limits

This is one real application authorization boundary, not a full agent tool access policy, penetration test or security certification. The source guard detects references to prohibited fields; it does not exercise all runtime authorization paths. API routes enforce their own checks. No actual allowlist, user record or token is included. A client needs its own written read/write scopes and approval rules.

## Reproduce in the source repository

Run `node --test scripts/check-admin-gate-method-blind.test.mjs` from the repository root. This reproduces the static authorization guard, not an authenticated production request.

Source: `apps/marketing-next/src/lib/admin/admin-access.ts`
SHA-256: `be48500db0e11e9d8223a1a5de01f704666341b6b45ca63a688c2d91c198f6c8`

```ts
import { createSupabaseServerClient } from "@/lib/supabase/server";
import { isAdminEmailAllowlisted } from "./admin-email-allowlist";

export type AdminAccess =
  | { ok: true; email: string; userId: string }
  | { ok: false };

/**
 * Page-level admin gate: session + email allowlist + user_profiles.role.
 * API routes still enforce via requireAdminAuth (defense in depth).
 */
export async function resolveAdminAccess(): Promise<AdminAccess> {
  try {
    const supabase = await createSupabaseServerClient();
    const {
      data: { user },
    } = await supabase.auth.getUser();
    if (!user?.email) return { ok: false };
    if (!isAdminEmailAllowlisted(user.email)) return { ok: false };

    const { data: profile, error } = await supabase
      .from("user_profiles")
      .select("role")
      .eq("id", user.id)
      .maybeSingle();

    if (error || profile?.role !== "admin") return { ok: false };

    return { ok: true, email: user.email, userId: user.id };
  } catch {
    return { ok: false };
  }
}
```

Source: `scripts/check-admin-gate-method-blind.test.mjs`
SHA-256: `4af8833832de8219d8b2a16cc7a0fe59ca51b557c2a0230026068c3d17983307`

```js
// Guard: HOW you signed in must never widen WHAT you may do.
//
// The three admin gates below authorize on identity (`user_profiles.role` plus
// the ADMIN_EMAIL_ALLOWLIST env var) and read nothing about the authentication
// method. Adding passkeys does not change that by design: Supabase mints the
// session, so a passkey session is byte-identical to a magic-link session and
// every gate keeps working untouched (HO-7121, plan
// docs/plans/2026-08-24-login-magic-link-and-passkeys.md § Build 6).
//
// WHY THIS IS A STATIC GUARD AND NOT A UNIT TEST. The obvious test — mock a
// session carrying `amr: ["webauthn"]`, assert `resolveAdminAccess()` still
// refuses a non-allowlisted email — CANNOT FAIL. The gates never read `amr`, so
// the mock's method field is inert and the assertion passes for the same reason
// it would pass with the field deleted. It would be a test of the mock. The
// real risk is a FUTURE edit — "admins with a passkey skip the profile lookup",
// "step up to /admin/internal when aal2" — and the thing that can actually go
// red on that edit is a scan of what these files reference. So: this.
//
// The banned tokens, and why each one is a hole rather than a style violation:
//
//   amr / aal      — assurance and method claims. Reading either turns "signed
//                    in with a passkey" into a permission, which is the exact
//                    inversion this guard exists to prevent. Method may gate a
//                    UI affordance; it must never gate authorization.
//   app_metadata   — provider/identity data. Banned WHOLE, not just
//                    `.provider`: matching only the dotted form is evaded by
//                    `const { app_metadata } = user` on the previous line, and
//                    a gate has no legitimate read of it at all.
//   user_metadata  — PRIVILEGE ESCALATION. It is writable by the session holder
//                    via `auth.updateUser({ data })`. Any gate that reads it is
//                    letting the attacker supply the input to their own
//                    authorization check. This is a MUST-NOT in the plan, and
//                    it is not hypothetical: the reference implementation
//                    stores its passkey-prompt dismissal stamp there.
//   factors        — MFA enrolment state; same shape of mistake as aal.
//   identities     — which providers are linked; same shape as app_metadata.
//
// Comment lines are stripped before scanning, deliberately: a comment saying
// "must not read user_metadata" is exactly the thing we want a future editor to
// write, and a guard that reds on its own rationale gets deleted. Code —
// including a trailing comment on a line of code — is scanned as-is.
import assert from "node:assert/strict";
import { existsSync, readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, it } from "node:test";

const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");

// If one of these is renamed or moved, this test must go RED, not quietly pass
// over a file that no longer exists. Fixing a rename means editing this list,
// which is the moment to re-read the rule above.
const GATES = [
  "apps/marketing-next/src/lib/admin/admin-access.ts",
  "apps/inspired-api/lib/admin-auth.ts",
  "apps/inspired-api/proxy.ts",
];

// `_` is a word character in JS regex, so a plain \b would miss `mfa_amr_claims`
// and `raw_user_meta_data`. These lookarounds treat anything non-alphanumeric
// (underscore included) as a boundary.
const BOUNDARY_BEFORE = "(?<![A-Za-z0-9])";
const BOUNDARY_AFTER = "(?![A-Za-z0-9])";
const token = (body) => new RegExp(`${BOUNDARY_BEFORE}(?:${body})${BOUNDARY_AFTER}`);

const BANNED = [
  { name: "amr", pattern: token("amr"), why: "authentication-method claim" },
  { name: "aal", pattern: token("aal[0-9]?"), why: "assurance-level claim" },
  { name: "app_metadata", pattern: token("app_metadata"), why: "provider/identity data" },
  {
    name: "user_metadata",
    pattern: token("user_metadata"),
    why: "SESSION-HOLDER WRITABLE via auth.updateUser({ data }) — reading it in a gate is privilege escalation",
  },
  { name: "factors", pattern: token("factors"), why: "MFA enrolment state" },
  { name: "identities", pattern: token("identities"), why: "linked-provider list" },
];

/**
 * Drop whole-line comments — the `//` form and every line of a block comment,
 * open, middle and close — and keep everything else byte-for-byte, line
 * numbers included. Line-based on purpose: a `//`
 * inside a string literal (`"https://…"`) never starts a line, so this cannot
 * truncate code and hide a token the way a naive comment stripper would.
 */
function codeLines(source) {
  return source.split("\n").map((line, i) => {
    const trimmed = line.trim();
    const isComment =
      trimmed.startsWith("//") ||
      trimmed.startsWith("/*") ||
      trimmed.startsWith("*/") ||
      trimmed.startsWith("*");
    return { number: i + 1, text: isComment ? "" : line };
  });
}

describe("the admin gates are method-blind", () => {
  it("every gate file exists at its recorded path", () => {
    const missing = GATES.filter((rel) => !existsSync(join(ROOT, rel)));
    assert.deepEqual(
      missing,
      [],
      "these admin gate files no longer exist — a rename must fail this guard, not slip past it:\n" +
        missing.map((f) => `  ${f}`).join("\n"),
    );
  });

  it("no gate reads the authentication method, assurance level, or holder-writable metadata", () => {
    const offenders = [];
    for (const rel of GATES) {
      const path = join(ROOT, rel);
      if (!existsSync(path)) continue; // already reported above
      for (const { number, text } of codeLines(readFileSync(path, "utf8"))) {
        for (const { name, pattern, why } of BANNED) {
          if (pattern.test(text)) {
            offenders.push(`${rel}:${number}  [${name} — ${why}]  ${text.trim()}`);
          }
        }
      }
    }

    assert.deepEqual(
      offenders,
      [],
      "An admin gate references the authentication METHOD. Authorization must stay a " +
        "function of identity (user_profiles.role + ADMIN_EMAIL_ALLOWLIST) only — a passkey " +
        "session is byte-identical to a magic-link one and must grant byte-identical access:\n  " +
        offenders.join("\n  "),
    );
  });
});
```
