Skip to main content

Policy

Every authorization decision the id service makes goes through lib/server/policy.ts, which uses @fairgarden/policy⁠ (external site)(github.com/fairgarden/policy): each decision is asked the way Open Policy Agent expects, so the built-in rules can be replaced by Rego without touching anything that asks, and every one is recorded.

The decisions

authz — may this person do this to this resource? The input is a Kubernetes SubjectAccessReview's attributes:

{
  "user": { "name": "d4618cea-…", "authenticated": true },
  "verb": "delete",
  "resource": { "group": "id.fairgarden.org", "version": "v1alpha1", "resource": "passkeys", "name": "…", "owner": "d4618cea-…" }
}

Answer { "allow": true }, or { "allow": false, "reason": "…" }.

release — which of the scopes a service asked for may it be offered, and why not the others?

{ "user": { "name": "…" }, "client": { "id": "events", "name": "Events" }, "scopes": ["openid", "residential_address"], "purpose": "Consent" }

purpose is Preview while the consent screen is shown, and Consent when the person's answer is recorded.

Answer { "scopes": ["openid"], "reasons": { "residential_address": "Only Members may ask where you live." } }.

Reasons are shown

A person is never refused something unexplained. Every reason release gives is shown on the consent screen beside the scope it withholds, and so is every reason another service's own policy gives in its claims review.

The organization's rules

id's decisions are the built-in rules until an organization says otherwise. policies/id.rego is those rules in Rego, with two places for an organization's own:

deny contains <reason>          # refuse a request id's rules would allow
withheld[<scope>] := <reason>   # keep a scope from a service, and say why

The organization writes them in its distribution's policies/, in package fairgarden.id — examples/privacy has one, keeping residential addresses from every service but Members:

package fairgarden.id

withheld["residential_address"] := "Only Members may ask where you live." if {
	"residential_address" in input.scopes
	input.client.id != "members"
}

The distribution builds id's rules and the organization's into one policy when it deploys, and id's build takes a copy (fg-dist policy use), which it runs in process — no OPA server, nothing to publish. Built on its own, outside a distribution, id has no policy to copy, and its built-in rules decide. See @fairgarden/policy⁠ (external site)(github.com/fairgarden/policy) for how the layers fit, and FG_POLICY_* in configuration for policy from elsewhere.

pnpm policy:test        # id's own rules and tests
fg-policy test --base policies --dir examples/privacy

Anyone may read it

/policy shows the policy in force to anyone, signed in or not: what each decision is for, in the organization's words; every rule, and whether it is id's or the organization's; the settings the rules read; the revision, and the commit it was built from. The API serves the same as policies/current. Reading it asks nothing of the policy, so it can be read even when the policy is refused.

Every decision is recorded

In id_policy_decisions, in OPA's own decision log format: the input, the result or the error, when, and the revision of the policy that answered — builtin, or the organization's, such as 2026.10.01+3f9a2c1e4b5d. Every revision id runs is kept whole in id_policy_revisions, so each decision can be read beside the rules that made it: the account page links each decision the organization's policy made to /policy?revision=…. Decisions are kept FG_ID_POLICY_LOG_RETENTION_DAYS (400), and also written to standard output, for a log drain, with FG_ID_POLICY_LOG_STDOUT=true.

pnpm policy:log --subject d4618cea-… --since 30d

prints them as JSON lines, for an audit. People can read the decisions made about them too, as policydecisions.

Failing closed

If the policy cannot be evaluated — it cannot be read, it was refused, the OPA server is down — the answer is no: the API says 503, and nothing is released.