Skip to content

Scope-based RBAC for restricted docs and inline sections.

Updated Jun 9, 2026

Access Control ​

Olly docs uses scope-based RBAC so individual pages and inline sections can be restricted to specific Keycloak roles, while the bulk of the corpus stays public.

Model ​

Every viewer has zero or more scopes (Keycloak realm roles). Pages and blocks declare required scopes. A viewer matches if the intersection with required scopes is non-empty (mode: any, default) or covers all (mode: all). The docs-admin scope is a super-scope - it satisfies any check.

ScopeGrants access to
docs-adminEverything. Super-scope; satisfies any check.
docs-internalOlly-internal content (this page, internal design docs, raw eval reports).
partner-healthPages shared with NHS / health-partner reviewers.
partner-insurancePages shared with reinsurance and underwriting partners.
partner-clinical-opsPages shared with clinical operations partners (triage hint review, MSK pathway evidence).

How to gate a whole page ​

Add access: to the page frontmatter:

yaml
---
title: My Internal Doc
access:
  scopes: [docs-internal]
  mode: any
---

The page is still emitted into dist/ but nginx refuses to serve it without a matching session. Use the page-gate when the entire page is sensitive - the URL itself is enough of a signal that you don't want the body cached anywhere public.

How to gate a block within an otherwise-public page ​

Wrap the sensitive content in a <Internal> element. The block content is extracted at build time and never appears in the public static HTML - only a <GatedBlock id="..." /> placeholder remains in the page.

markdown
Most of this page is public.

<Internal>

## Internal-only section

This content is sha256'd, written to `/_gated/<id>.html`, and only fetched
after oauth2-proxy confirms the viewer has the required scopes.

</Internal>

Public content continues below.

The audit script scripts/audit-gated-leak.sh greps the built dist/ after every build to confirm no inner block text leaked into the public HTML.

How to grant scopes to a user ​

  1. Sign in at https://auth.dev.hiolly.com/admin.
  2. Realm olly -> Users -> pick user -> Role mapping.
  3. Assign realm role(s) from the table above.

Roles propagate at next sign-in or after the oauth2-proxy session refreshes (max 1h).

Architecture ​

LayerWhere it runsWhat it does
Auth gatewaysystemd unit oauth2-proxy-docs on dev-1Validates the session cookie against Keycloak, exposes /oauth2/auth for nginx subrequests, serves userinfo at /_user.json.
Cookie domain.dev.hiolly.comSingle sign-on across all *.dev.hiolly.com hosts.
nginx/etc/nginx/sites-enabled/ollyauth_request /oauth2/auth?allowed_groups=... per path. Per-page scope map at /var/www/docs.dev.hiolly.com/_access/page-scopes.conf, regenerated on every build.
Gated blocksmarkdown-it plugin gated-block.tsWrites /var/www/docs.dev.hiolly.com/_gated/<sha256>.html at build time. nginx serves with the same auth_request pattern.
Sidebar filterSidebarFilter componentFetches /_user.json (oauth2-proxy userinfo) and hides items the viewer can't access.
Login UILoginButton in the navAnonymous "Sign in" pill, signed-in email pill with sign-out dropdown.
403 page/403.html (static, public)Shown when a signed-in user lacks the required scope.

Defense-in-depth: every layer enforces scopes independently. The sidebar filter only hides links - it never gates content. nginx + oauth2-proxy is the source of truth.

Operational notes ​

  • Rotate the docs-auth client secret in Keycloak quarterly. Update /etc/oauth2-proxy/docs-client-secret and systemctl restart oauth2-proxy-docs.
  • Cookie secret in /etc/oauth2-proxy/cookie-secret rotates annually. Rotating invalidates all live sessions.
  • nginx access maps regenerate on every npm run build and live under /var/www/docs.dev.hiolly.com/_access/. If a scope check looks wrong, check the generated page-scopes.conf first - it pins the build-time view of frontmatter.

SSO across all olly hosts

cookie_domain=.dev.hiolly.com means signing in at any olly host (member portal, employer portal, admin console, docs) gives a session at docs too. This is intentional SSO. Tighten in /etc/oauth2-proxy/docs.cfg if you don't want it.

Olly Health Insurance Platform