yau-plant-assistant/authelia/access-rules.md
Claude 98083cd8d6 Design Phase 9 - operator document management
Operators cannot add a document today: ingestion is CLI-only, needs a host
login and a TTY for confirm_header(), /datadisk/ai-docs is mounted read-only,
ai-api has no identity, and nothing in the stack has a role that can write
doc_chunks. This designs the way in, the way out, and control over what is in
the retrieval pool. Design and schema only - no router, worker or UI code yet.

Documents in (16.1-16.9, db/004):
  upload -> pre-scan -> review -> approve -> published, with the header
  confirmation moved from a terminal prompt to a review screen and recorded
  rather than discarded. A CHECK constraint refuses an approved row without a
  confirmed number, revision and effective date, so an API bug cannot skip it.
  Three roles: agent_ro unchanged, uploads_rw writes the queue only, ingest_rw
  writes doc_chunks and has no HTTP surface.

Documents out (16.10-16.11, db/005):
  --supersede needs a revision to keep, so a cancelled procedure cannot be
  withdrawn at all. Adds withdraw (immediate, reversible, audited), restore
  (refused while another revision is live) and purge (off by default). A
  column grant plus a trigger let the web-facing role make a document less
  citable and never more.

The pool (16.13-16.15, db/006):
  pool_enabled, orthogonal to superseded: one is a claim about the document,
  the other about the corpus. Retrieval requires both, so re-enabling a
  withdrawn document does not make it citable. Named profiles and a
  per-request override let a demo trim the corpus without mutating state on a
  shared live host, and every reduced-pool answer carries a banner with the
  document count, following the used_fixture_data precedent.

Two existing defects found and documented while designing this:
  - ai-ingest takes PGUSER=agent_ro from api.env, a SELECT-only role, so the
    Phase 3 command in the README cannot write doc_chunks (16.1).
  - ingest_file() always inserts superseded = FALSE, so `--all` re-ingests a
    superseded revision as live. --supersede survives only until the next bulk
    run (16.10).

One commit rather than three: the upload, withdrawal and pool designs
interleave in the same spec, README and compose files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:17:38 +10:00

5.4 KiB

Authelia access rules — the additions, as text

This file is documentation, not configuration. Never commit the real ~/authelia/configuration.yml, and never generate a replacement for it. It contains the AD bind account, session secrets and the Duo integration for every service on the host. The four hostnames below are the only part of it this project touches.

What has to change

Four hostnames join the existing HTTPS_UserAccess two_factor rule in ~/authelia/configuration.yml, under access_control.rules:

Hostname Phase Serves
lf.yokogawa.tech 2 Langfuse — traces, prompts, eval runs
cube.yokogawa.tech 5 Cube semantic layer, playground and REST API
api.yokogawa.tech 6 ai-api FastAPI
ai.yokogawa.tech 7 ai-web operator UI

Phase 9 adds no hostname. It adds one path-scoped rule on an existing one — see "Phase 9: the document publisher rule" below.

The shape of the addition — the domain list on the existing trailing rule gains these entries, the policy and subject stay exactly as they already are:

access_control:
  rules:
    # ... existing rules unchanged ...
    - domain:
        # ... existing domains unchanged ...
        - lf.yokogawa.tech        # added <date>, AI PoC Phase 2
        - cube.yokogawa.tech      # added <date>, AI PoC Phase 5
        - api.yokogawa.tech       # added <date>, AI PoC Phase 6
        - ai.yokogawa.tech        # added <date>, AI PoC Phase 7
      policy: two_factor
      subject:
        - group:HTTPS_UserAccess

Add each hostname at the phase that needs it. Every domain added here must also have a Caddyfile block with import authelia (caddy/ai-routes.caddy), and every Caddyfile block must have a rule here. One without the other is a hole.

Phase 9: the document publisher rule

Operator document upload splits the API in two. Anyone in HTTPS_UserAccess may ask a question and upload a document for review. Approving a document — which is what makes it citable, and what decides whether a superseded revision stops being citable — needs a second group, AI_DocPublishers.

access_control:
  rules:
    # ... existing rules unchanged ...

    # MUST come BEFORE the general api.yokogawa.tech rule. Authelia applies the
    # FIRST matching rule and stops. Below it, this rule is dead and every
    # authenticated user can approve a procedure revision.
    - domain: api.yokogawa.tech
      resources:
        - '^/docs/.*'
      policy: two_factor
      subject:
        - group:AI_DocPublishers      # added <date>, AI PoC Phase 9

    # ... the existing trailing rule, unchanged, still carries
    #     api.yokogawa.tech for everyone in HTTPS_UserAccess ...

Three things about this rule specifically:

  • AI_DocPublishers must be created in AD with DIRECT membership. The same trap as HTTPS_UserAccess: a user inside a nested group is silently denied, with no useful log line. svc-authelia is read-only and cannot fix it.

  • It is not the only check. ai-api re-reads Remote-Groups and returns 403 on approve, reject and supersede. This rule protects the whole /docs path; the API protects the three operations that matter, and survives this rule being reordered or dropped in a future edit of a file nobody diffs.

  • Verify it by calling the API directly, as a user who is authenticated but not a publisher. The UI hides the buttons from that user, which proves nothing at all:

    curl -si https://api.yokogawa.tech/docs/uploads/<id>/approve        -X POST -H 'Content-Type: application/json' -d '{}'      # expect 403
    

How to apply it

# 1. Back up first. There are plenty of .bak-* precedents on the host.
sudo cp ~/authelia/configuration.yml ~/authelia/configuration.yml.bak-ai-$(date +%Y%m%d)

# 2. Edit with sudo - the file is root-owned. ~/apply_rule.py rewrites the
#    trailing rule if you prefer it to hand-editing.
sudo nano ~/authelia/configuration.yml

# 3. ANNOUNCE FIRST - this logs out every active user on every service.
docker compose -f ~/authelia-compose.yml restart authelia

# 4. Verify. "Up" is not proof.
curl -sI https://ai.yokogawa.tech        # expect 302 -> auth portal
docker logs --tail 50 authelia

Things that bite

  • Restarting Authelia logs out every active user on the host, including whoever is mid-demo on Grafana. Announce it, and batch the domain additions so you restart once per phase rather than once per hostname.
  • AD group membership must be DIRECT. Authelia resolves direct membership only; a user inside a nested group silently gets denied with no useful log line. Before Phase 7, confirm the demo operator account is a direct member of HTTPS_UserAccess and is Duo-enrolled. svc-authelia is read-only and cannot fix membership for you.
  • Rule order is a security control, not a style choice. First match wins. A path-scoped rule placed after the domain rule it narrows is inert, and nothing warns you — the service keeps working, for everybody.
  • A missing rule fails open at the wrong layer. Caddy will happily serve a hostname that has import authelia before the rule exists — Authelia then applies its default policy. Add the rule in the same change as the Caddyfile block and verify the 302 before telling anyone the URL.
  • Restarting Authelia is also the supported way to refresh someone's group membership after an AD change.