Skip to main content

AUTHORIZATION

Authorization in swamp serve is grant-based. Each grant binds a subject to an effect (allow or deny) on a set of actions against a resource selector. The server evaluates all matching grants at access-check time.

Default deny

If no grant matches a request, the request is denied. There is no implicit allow.

Subjects

A grant's --subject identifies who the grant applies to.

Format Description
user:<id> A specific authenticated user
group:<name> Members of a local group managed via swamp access group
idp-group:<collective-slug> Users whose IdP token includes the group

Local groups are resolved at policy reload time. IdP groups come from the OAuth provider's userinfo response, using the field specified by --groups-field (default: collectives).

IdP group refresh

The server periodically refreshes IdP group memberships by re-fetching each authenticated user's groups from the OAuth provider. The refresh interval is controlled by --group-refresh-interval (default: 4h). On each refresh cycle, the server calls the provider's userinfo endpoint for every active session and updates the policy snapshot with the current group memberships.

Transient refresh errors (network timeouts, provider unavailability) are fail-open: the server retains the last known group memberships and retries on the next cycle. A 401 Unauthorized response from the provider is treated as a hard revocation — the user's session is invalidated and all idp-group: grants for that user stop matching immediately.

See swamp serve — IdP group refresh and deprovisioning for the design rationale behind fail-open vs hard revocation.

Refresh does not apply to SAML federation

The refresh cycle above describes what the server does with the groups Swamp returns. Whether those groups are themselves current depends on how the operative's identity provider is federated:

Federation When group memberships are re-read from the IdP
OIDC On each refresh cycle, live from the IdP
SAML Only when the operative next completes an SSO sign-in

A SAML assertion is delivered once, at sign-in, and carries no token Swamp could use to ask the IdP anything afterwards. So for a SAML-federated collective the effective bound on a group change taking effect — including a removal — is the operative's next SSO sign-in, not --group-refresh-interval. Plan deprovisioning around session invalidation rather than around the refresh cycle.

Effects

Each grant has exactly one effect:

Effect Meaning
allow Permits the specified actions
deny Blocks the specified actions

Deny wins

When both allow and deny grants match a request, deny wins. A deny grant always overrides an allow grant, regardless of specificity.

Actions

Action Description
run Execute a model method or workflow
read Read data outputs, model definitions, vaults
write Write data, create models
approve Approve or reject a suspended workflow approval gate
admin Manage access: grants, tokens, groups; any request on an access-control record; the audit log

Actions are specified as comma-separated values in --allow or --deny.

Run implies approve

By default, a grant of run on a workflow implicitly permits approve on the same resource. A user who can run a workflow can also approve its gates — there is no separation of duties by default. swamp access check and swamp access can-i mark an approve decision that comes only from a run grant as [implied by run].

swamp serve --approve-requires-explicit-grant turns the implication off. With it set, only a grant that names approve counts toward deciding a manual approval gate; an allow grant of run alone no longer permits approve. A deny grant of run still denies approve.

Setting Default
Flag: --approve-requires-explicit-grant off
Config file: auth.approve-requires-explicit-grant false
Environment: SWAMP_APPROVE_REQUIRES_EXPLICIT_GRANT unset

The setting requires --auth-mode token or oauth; with --auth-mode none the server logs a warning and the setting has no effect. Every replica serving the same repository must use the same value — each replica decides gates with its own policy. See Serve Flags.

Without the setting, separation of duties is enforced by granting approve without run to the approver role and denying approve to runners who should not self-approve (see Approve-only grants). See the Separation of duties scenario for worked examples.

Approve-only grants

A grant of approve alone — without run — permits the holder to approve or reject workflow gates but not to start runs or resume them. This is the building block for approval-only roles:

# Approvers can approve gates on deploy workflows but cannot start them
swamp access grant create --subject group:release-managers \
  --allow approve --on 'workflow:@acme/deploy-*' \
  --server wss://swamp.example.com

To prevent a runner from approving their own gates, add a deny grant:

# Runners can start the workflow but cannot approve its gates
swamp access grant create --subject group:developers \
  --allow run --on 'workflow:@acme/deploy-*' \
  --server wss://swamp.example.com

swamp access grant create --subject group:developers \
  --deny approve --on 'workflow:@acme/deploy-*' \
  --server wss://swamp.example.com

The deny-wins rule ensures the deny on approve overrides the implicit approve from run. On a server started with --approve-requires-explicit-grant, the deny grant is not needed: the run grant does not imply approve.

Methods

A grant can optionally scope its actions to specific model method names using the methods field. When present, the grant matches only when the requested method is in the list.

Behavior Description
methods absent Grant applies to all methods on the matched resource
methods present Grant applies only to the listed methods
swamp access grant create --subject user:alice --allow run \
  --on 'model:hello' --methods read,list

This grant allows alice to run the read and list methods on model hello, but not create, delete, or any other method.

Method scoping also works in grant files:

# grants/reader.yaml
grants:
  - subject: "group:readers"
    effect: allow
    actions: [run]
    resource: "model:*"
    methods: [read, list]

The methodName field is available in CEL conditions for further refinement:

--when 'methodName == "deploy"'

See the CEL Expressions reference for the full list of fields available in grant conditions.

Admin superuser

A grant of admin on access:* implies all actions on all resources. This is the superuser grant — the holder can perform any operation. The --admins flag materializes this grant for the listed principals on every server start.

Resource selectors

The --on flag specifies which resources the grant applies to.

Selector Matches
workflow:@acme/* All workflows in the @acme namespace
workflow:@acme/deploy A specific workflow
model:hello A specific model
model:* All models except access-control models
data:* All data outputs except access-control records
data:@acme/secrets-* Data outputs matching the prefix
vault:roomcontrol One vault; see Vaults
vault:prod-* Vaults whose names match the prefix
access:* The access control system itself, including access-control records and the audit log
access:swamp/* All access-control records
access:swamp/grant Access-control records of one type
access:audit The serve audit log

The * wildcard matches any suffix.

Access-control records

Swamp stores its own control-plane state as model data in the repository, beside user models. These model types hold it:

Purpose Model types
Access control swamp/grant, swamp/group, swamp/server-token
Worker fleet swamp/enrollment-token, swamp/worker, swamp/step-lease, swamp/pending-dispatch, swamp/fleet-probe

Serve authorizes a model, its data and its outputs of one of these types as the access resource named by the type, such as access:swamp/server-token, never as model:<name> or data:<name>. Every request on one is decided as admin, whatever action the request asks for:

  • swamp data get, list, versions, search, query, delete and rename
  • swamp model get and edit
  • swamp model output get, data and logs
  • swamp model method history get and logs
  • cancel and attach of a run on one of these models

A read, write or run grant on data:* or model:* does not reach these records. A request for one record is refused:

Access denied: user:bob does not have 'admin' on access:swamp/server-token

swamp data search and swamp data query leave the records out of their results.

Grant Reaches access-control records
admin on access:* Every request
admin on access:swamp/* or access:swamp/<type> Single-record reads and writes; search and query results when combined with read on data
read, write or run on data:* or model:* None

Creating a model of one of these types, or running a method on one, needs admin on access:* itself. A narrower access:swamp/* grant is refused:

Access denied: user:carol does not have 'admin' on access:*

The selector matches the record's type. --when conditions see the record's own name, modelType and tags, so a condition naming one grant or token still applies to that record.

A refused request reveals that the name exists: a name that matches nothing reports not found instead. Name and tag denies behave the same way.

swamp access grant list and swamp access group list still check read on access:grant and access:group.

Expressions never return these records. data.latest, data.query and the other data functions skip them, and the model map leaves out models of these types. This holds wherever expressions are evaluated, locally as well as on a server. A local swamp data query or swamp data search, run without --server, still lists them.

Requests by id match the resource's name

Selectors match names, never ids. A request may name a model or workflow by name or by UUID. Either way, serve resolves it first and checks the grant against the resolved resource's canonical name and fields (modelType and tags for a model, ns and tags for its data, tags for a workflow). A request by the UUID of prod-db is denied by deny model:prod-* just as a request by name is, and CEL conditions on tags apply the same way. The operation then acts on exactly the resource that was checked.

Lookup order

A name or UUID is looked up by name first, then by exact id. A resource named with another resource's UUID wins over the resource that has that id. The CLI's swamp workflow get, delete, edit and validate resolve a UUID argument the same way.

A string that matches nothing is checked as sent, and the operation reports its usual not-found. A lookup that fails with an error fails the request.

Run cancel and attach

Run cancel and attach are authorized on the model or workflow the run was started on. Serve records its name and id when the run starts, and finds it by that id when the cancel or attach arrives, so renaming the resource during the run cannot redirect the check.

Workflow files that fail to parse

A workflow file that fails to parse is authorized on the name it declares. Its tags cannot be read, so tag-conditioned rules do not apply to it; name selectors do.

Output and run reads

swamp model output get, data and logs, swamp model method history get and logs, and swamp workflow history get and logs take an output or run id prefix, or a model or workflow name or id.

  • A prefix of 3 or more hex characters is resolved to the output or run it matches, and the grant is checked against the model or workflow that owns it, never the prefix.
  • When several models or workflows share that id with a copy, every owner is checked, and one deny refuses the read.
  • A read by name is checked against that model or workflow and against the owner of the latest output or run it returns.
  • Outputs of a deleted model are checked on the model id. Runs of a deleted workflow are checked on the workflow name recorded on the run, without tags.
  • A prefix that matches several outputs or runs, or none, is checked as sent.
  • swamp model output data and swamp model output logs return data artifact content, so they need a read on data: for the owning model, as swamp data get does, as well as the model: read. A principal granted only model reads cannot read artifact content this way.
  • Outputs and runs of an access-control model need admin on access:swamp/<type>. Once the model is deleted they are checked on the type recorded with the output or run, so they stay admin-only.

Direct type runs

A direct type run names a model type and a definition name instead of an existing model. Serve authorizes the requested model, then the type, then the definition the definition name refers to, by its canonical name and fields. Before that definition exists, it is checked by the name it will be created with, and the requested type. The run is locked, recorded for cancel and attach, and audited on that definition.

The run fails if the name resolves to a different definition by the time it starts, because one was renamed in or the definition was deleted. If no definition had the name at the check and a concurrent run has created one since, the run uses it when the caller may run that definition.

A direct run can rewrite an existing definition's global arguments, so it takes that definition's model lock whatever kind of method it runs.

Not yet covered

These forms still authorize differently:

Vaults

A vault is a resource of its own kind, vault:<name>. The pattern matches the vault's name exactly, or by prefix with a trailing *: vault:prod-* covers prod-db and prod-api. Conditions see the vault's name and the secret key the operation names:

grants:
  - subject: "user:ci"
    effect: deny
    actions: [read, write]
    resource: "vault:prod-*"
    condition: 'key == "root-password"'

Vault access is decided at two points, by two rules:

Request rule (vault.* requests) Run-time rule (vault operations in a serve run)
Refused by A deny for the action on data:vault, data:<name> or vault:<name> A reserved vault, unless admin on access:*; a deny for the action on vault:<name>
Admitted by The data:vault or data:<name> check, or an allow on vault:<name> Anything, until the principal holds a vault allow; then only an allow on vault:<name>
No matching grant Refused Allowed, unless the principal is vault-scoped
data grants Count Play no part

Request rule

A vault.* request, such as swamp vault get or swamp vault put with --server, is authorized on vault:<name> with its own action:

Action Requests
read get, describe, inspect, list-keys, read-secret, audit-trail
write put, delete, annotate, create, edit (rename and repair included)
admin put with refresh options, migrate

For a vault named N:

  1. A deny for the action on data:vault, data:N or vault:N refuses.
  2. Otherwise read/write on data:vault or data:N (admin on model for migrate) admits the request.
  3. Otherwise an allow for the action on vault:N admits it, except put with refresh options, which needs admin on data:vault: a refresh hook is a shell command serve runs on its host.
  4. Otherwise the request is refused.

swamp vault search, and swamp vault audit-trail without a vault, list each vault the rule allows. A vault grant never reaches another vault, and a read grant never allows a write.

A deny on data:vault or data:<vault name> refuses every request for that vault. Write vault denies as vault:<name> grants: a data deny has no effect on runs.

Run-time rule

During a serve run, every vault operation is judged against the principal that triggered the run: vault.get(...) expressions in definitions, workflows and method inputs, method code using context.vaultService, sensitive outputs and their read-back, and data queries that resolve vault references. Reads are read; puts, deletes and annotation changes are write. For vault N and action A:

  1. Reserved vaults. A vault whose name starts with _, such as _token-secrets, is refused in every serve run unless the principal holds admin on access:*.
  2. Inert without vault grants. While the policy holds no vault: grant, the operation is allowed.
  3. Deny. A vault deny for A that matches N refuses.
  4. Scoped. If any vault allow, for any action, applies to the principal — directly, through a local group or through an IdP group — the principal is vault-scoped: the operation is allowed only when an allow for A matches N.
  5. Otherwise the operation is allowed.

A deny-only grant blocks just the denied vault. A principal granted read on vault:roomcontrol can read only roomcontrol in its runs, and write nowhere. A principal with admin on access:* is never vault-scoped, and is still refused by an explicit vault deny. data grants play no part at run time: deny read data:prod-db does not stop a run reading the vault prod-db.

The first vault grant written applies the rule to the whole server. A principal given any vault allow loses every vault the allow does not name, so grant every vault such a principal needs in the same change.

A refusal fails the step before the vault provider is touched, and names the vault and the principal, never a value:

Failed to resolve vault expression vault.get("erp", "password"): Reading vault 'erp' is refused for user:room-bot: the principal holds vault grants and none allows read on vault:erp; add a vault:erp allow grant for read to this principal

Each refusal is audited with category secrets and outcome denied.

Run principals and resumes

The policy is read on every operation, so grant reloads and revocations apply mid-run. The principal's memberships are captured when the run starts:

Run started by Memberships
A server token Re-read from the token record. Once the token is revoked, re-minted or deleted, every vault operation is refused. Expiry alone does not cut a run off; token GC deletes it after its grace period
An OAuth login The session's IdP groups and collectives at run start. The login token is not re-checked
A schedule or webhook Local groups of service:scheduler or service:webhook

A resume, whether an approve-then-resume, a parent's auto-resume, detached or attached, is scoped by the run's recorded triggering principal and memberships, never by the approver or the resumer. A suspended run that recorded no principal (started on an older release, or saved by an older replica) refuses every vault operation once the policy holds a vault grant. Resume or cancel such runs before writing the first vault grant.

Scheduled runs act as service:scheduler and webhook runs as service:webhook. A vault grant on either scopes every scheduled or webhook run on the server. To bound one workflow's runs, use its vaults: list.

Workflow vaults list

A workflow's vaults: list is checked alongside the run-time rule, so either can refuse. It applies to local runs too. See Workflows.

Sensitive outputs

Storing a sensitive output field, and every read-back of its reference, is a vault operation on the vault the field lands in. A vault-scoped principal needs read and write on that vault. A mutating method whose sensitive outputs would land in a vault the principal may not use is refused before it runs.

Explaining vault decisions

swamp access check and swamp access can-i on a concrete vault:<name> add an In serve runs: line with the run-time decision, and a runVaultAccess field in JSON. See Access Commands.

Upgrading

Write vault grants, or workflows that set vaults:, only once every serve replica on the datastore runs a release that supports them. An older replica:

  • refuses to start on a grant file holding a vault grant
  • ignores stored vault grants, denies included
  • rejects a workflow file that sets vaults:
  • saves runs without their triggering principal, so their resumes refuse every vault operation once vault grants exist

See Limit which vaults a run can use.

Expressions and grants

An expression can read beyond the model it belongs to: other models' data, other models' definitions, and the environment. Over serve, expressions are evaluated in the server process, so serve checks expression text against the principal who supplies it, at the time they supply it.

Supplied by Checked
model.create Every expression in its global arguments
model.edit, workflow.edit Each expression the edit puts somewhere new
model.method.run inputs Every expression in the inputs
Runs, evaluate and validate Nothing: stored content is its author's

Stored content is never checked again when it runs, so an operative running a workflow an admin wrote needs no grants beyond run. Admins and --auth-mode none are not checked.

What a reference needs

Reference Needs
Another model's data by name: data.latest, data.version, data.listVersions, data.findBySpec, model.<name>.resource, file.contents read on that data
A model's definition: model.<name>.input, model.<name>.definition read on the model
A computed model name, data.query, data.findByTag, the whole model map, a ns: or *: prefix, more than 32 named models read on all data
env write on the model; see env
vault.get(...) Nothing when written; checked at run time. See Vault secrets

Reading a model's data by name needs read on every record stored under that name and on every definition with that name or id; deny wins. A name nothing owns yet needs read on all data, since it reads whatever is stored under it later.

A reference that needs read on all data is refused by any data deny. A target computed from self, as in data.latest(self.name, ...), counts as computed. Text serve cannot parse is judged as reading all data.

A refusal quotes the expression as sent, with the same wording whether the target exists or is denied, and is audited like any other denial.

What an edit is checked for

model.edit and workflow.edit check:

  • every expression whose text is new
  • every stored expression the edit moves or copies to a place it was not
  • every expression in a workflow step whose target the edit changes

An expression left in place, under an unchanged step, is not checked again. Steps are identified by job and step name, so adding, removing or reordering other steps does not move them.

A stored expression that reads data through a target computed from self or inputs is checked again when an edit could point it elsewhere:

  • a model edit that changes the model's name, version, tags, global arguments or inputs
  • a workflow edit that changes its inputs or a step's forEach

So an edit that changes target: ${{ "dev-db" }} to ${{ "prod-db" }} under data.latest(self.globalArguments.target, ...) needs read on prod-db's data.

A workflow edit that adds a step needs run on the step's model, or on its nested workflow. A step whose model is computed by an expression needs admin, and a computed nested workflow needs run on every workflow. Steps already stored are not checked again.

env

Reading the server's environment is an author's capability. On model.create and model.edit the writer already holds write on the model, so env is allowed. In model.method.run inputs, env needs write on the model; a caller with only run is refused and told to reference env in the model definition instead. evaluate and validate never resolve env.

Vault secrets

A vault.get(...) expression is not checked when it is written. It is checked when it is resolved during a serve run, against the principal that triggered the run, by the run-time rule:

  • A principal with write on a model or workflow may write any vault.get(...) into it. Whether a run reads that vault depends on the vault grants of whoever triggers the run.
  • model.method.run inputs may carry vault.get(...) from any caller. The value is masked in output.
  • Workflow run inputs never resolve vault.get(...).

While the policy holds no vault: grant, the run-time rule allows every vault operation except on reserved vaults. Then write on workflows or models, and run on a model whose method can surface its inputs, include access to every secret in the repository's user vaults. Grant them only to principals you trust with those secrets, or scope vault access.

This is the expression function. The vault.get serve request, which reads a vault directly, follows the request rule.

Limits

  • A computed target lets the runner choose. A stored data.latest(inputs.target, ...) reads whatever model a run passes as an input. A target taken from another model's data, steps.* or run.* follows whoever writes that data.
  • A runner can override global arguments. A workflow step's globalArgs, and a direct type run's inputs, replace a model's global arguments with only run. A direct run also saves them to the definition. A stored reference computed from self.globalArguments therefore reads whatever model the runner names. Do not compute a data target from a global argument when runners should not choose the model.
  • Only new text is checked. Expressions stored before this check existed are not checked.

Model method run inputs are evaluated as expressions; see Evaluated and Inert Sources.

CEL conditions

The --when flag adds a CEL expression that must evaluate to true for the grant to apply. Conditions are evaluated against resource fields at access-check time.

--when 'tags.env == "staging"'

See the CEL Expressions reference for the full syntax, operators, and available functions.

Resource fields can be passed to swamp access check using the --field flag for testing condition evaluation.

Condition cost limits

Grant conditions are subject to complexity limits. Conditions that exceed a limit are rejected at grant creation time, except the aggregate limit which is enforced at access-check time.

Limit Threshold Enforcement
AST depth ≤ 24 Write-time
Comprehension nesting ≤ 2 Write-time
Per-condition cost budget ≤ 500 Write-time
matches() pattern See below Write-time
Aggregate per-decision ≤ 100 Access-check

AST depth counts the maximum nesting depth of the parsed expression tree.

Comprehension nesting limits the levels of chained exists, all, filter, and map macros.

Per-condition cost budget is an estimated cost derived from AST node count and comprehension weight.

matches() restrictions: the pattern argument must be a string literal, not a variable. Patterns containing catastrophic backtracking constructs are rejected.

Aggregate per-decision limit: if more than 100 --when conditions are evaluated in a single access check, the request is denied.

Existing grants created before these limits were introduced are unaffected. Error messages identify which limit was exceeded and the actual value.

# Rejected: comprehension nesting exceeds limit (3 > 2)
--when 'tags.all(k, tags[k].all(v, v.all(c, c == "x")))'

Restricted model types

The --restricted-model-types flag locks specific model types behind the admin superuser grant, the same authorization path used for the built-in access-control model types.

swamp serve --auth-mode token --admins 'user:admin-id' \
  --restricted-model-types 'command/shell'

A model of a restricted type requires admin on access:* to:

  • create it, or run one of its methods, including a direct type run
  • edit it, including renaming or retagging it
  • delete it, by id or by name
  • delete or rename its data
  • add a workflow step that runs it, or change a stored step that runs it, including renaming or moving the step

A changed workflow step whose model is computed by an expression, or one that runs an access-control model, also needs admin on access:*.

Non-admin users who attempt any of these receive:

Access denied: <principal> does not have 'admin' on access:*

These are not restricted, and follow the principal's ordinary grants:

  • reads: get, search, output and history reads
  • swamp data gc, swamp data prune and swamp run gc
  • workflow input defaults and trigger inputs; a caller with run on a workflow already chooses its inputs at run time

Model types are normalized on parse. Denormalized variants (Command::Shell, COMMAND.SHELL, command.shell) all resolve to the canonical form and are caught. A leading @ is ignored when matching, on the list entry, on the requested type and on the stored type, so listing an extension type in either spelling restricts it:

--restricted-model-types '@acme/deploy'   # also restricts acme/deploy

Non-restricted model types are unaffected — non-admin users can still create and run them per their grants.

Multiple types can be restricted with a comma-separated list:

--restricted-model-types 'command/shell,custom/dangerous'

An entry that names no type, such as @ or ::, stops serve from starting:

Invalid restricted-model-types entry "::": it names no model type

At startup, serve warns about each entry without a leading @ that names no registered model type. An entry with a leading @ is not checked, because an extension type can be installed after startup:

restricted-model-types entry "acme/deplyo" names no registered model type — check the spelling, or write an extension type as @acme/deplyo

swamp serve check-config reports an entry that names no type as a failed check. It does not check whether a model type is registered. See Checking restriction entries.

Restricted commands

The --restricted-commands flag locks specific server commands behind admin authority. A restricted command requires admin on access:<command> to execute — non-admin users are denied even if they have grants covering the command's usual resource type.

swamp serve --auth-mode token --admins 'user:admin-id' \
  --restricted-commands 'datastore.namespace.list,extension.install'

Non-admin users who attempt a restricted command receive:

Access denied: <principal> does not have 'admin' on access:<command>

Multiple commands can be restricted with a comma-separated list:

--restricted-commands 'datastore.namespace.list,extension.install,vault.seal'

Command names match exactly. Serve warns at startup, and swamp serve check-config warns, about each entry that is not a server command:

restricted-commands entry "extension.instal" is not a server command and restricts nothing — command names match exactly

Restricted model types vs restricted commands

Use --restricted-model-types to control which models non-admins can create, run, edit, delete, or reference from a workflow step — for example, locking down command/shell so only admins can execute shell models. Use --restricted-commands to control which server operations are available — for example, preventing non-admin users from listing datastore namespaces or installing extensions. Both flags require --auth-mode token or oauth.

Audit log

Every serve audit request needs admin on access:audit:

Request Purpose Command
audit.timeline Timeline of agent commands swamp audit --server
audit.query Query the serve audit log swamp audit log --server
audit.subscribe Stream new audit events swamp audit log --follow --server
audit.export Bulk export for compliance swamp audit export --server
audit.report Run a compliance report swamp audit report --server
audit.alerts List alert rules and their state swamp audit alerts --server
audit.rotate-key Rotate the audit HMAC key swamp audit rotate-key --server
audit.verify Check the audit chain's integrity —

audit.unsubscribe has no check: it only ends the caller's own stream. A read grant on models is not enough. A caller without the grant is refused:

Access denied: user:bob does not have 'admin' on access:audit

See View the timeline on a server.

Grant files

A grant file is a YAML object with a grants list. Serve reads grant files from the repository's grants/ directory, from --grants-dir and from --grants-file.

# grants/platform.yaml
grants:
  - subject: "user:adam"
    effect: allow
    actions: [run, read]
    resource: "workflow:@acme/*"
  - subject: "group:ops"
    effect: allow
    actions: [run, read, write]
    resource: "model:*"
    condition: 'tags.env == "staging"'
  - subject: "user:monitor"
    effect: allow
    actions: [run]
    resource: "model:@acme/my-model"
    methods: [read, list]
Field Type Required Description
subject string Exactly one of the two One subject
subjects string[] Exactly one of the two Up to 100 subjects, no duplicates
effect string Yes allow or deny
actions string[] Yes One or more actions
resource string Exactly one of the two One resource selector
resources string[] Exactly one of the two Up to 100 resource selectors, no duplicates
condition string No A CEL condition, the file form of --when
methods string[] No Limit the grant to these model methods; omit for all methods

Subject and resource lists

subjects and resources expand when the file is parsed, into one grant per subject and resource pair with every other field identical. This entry is six grants:

grants:
  - subjects:
      - "user:alice"
      - "user:bob"
      - "user:carol"
    effect: allow
    actions: [read, write, run]
    resources: ["workflow:*", "model:*"]

Because the expanded grants are identical, rewriting repeated single-subject entries as one subjects entry changes no grants on reload.

A subjects list keeps membership in the reviewed file. A group: subject keeps it in the datastore, where it is changed with swamp access group without editing the file.

Invalid files

Any error rejects the whole file: serve refuses to start, swamp access reload rejects the reload, and the directory poller keeps the file's stored grants. No valid subject from a partly invalid subjects list is applied. swamp serve check-config reports the same errors before a deploy:

Grant files: 2 file(s) checked
  ✗ grants/bad.yaml: Schema error at grants.0.subjects: Cannot specify both 'subject' and 'subjects' — use one or the other
  ✗ grants/bad.yaml: Schema error at grants.1.resources: Cannot specify both 'resource' and 'resources' — use one or the other

A file that is a bare YAML list instead of an object with grants fails with Schema error at : Invalid input: expected object, received array.

Grant source types

Grants can originate from different sources:

Source Description
method Created via swamp access grant create
config Materialized from --admins at startup
file:<filename> Loaded from a YAML file in the grants/ directory
file:grants-dir/<filename> Loaded from a YAML file in the --grants-dir directory
file:grants-file Loaded from the --grants-file file
extension:* Contributed by an extension

The reconciler (triggered by swamp access reload) only creates, updates, and revokes file:* grants. Grants with source method, config, or extension:* are never modified by the reconciler. swamp access reload results name each file by the same source name, without the file: prefix.

The --grants-dir and --grants-file source names do not include the path the file was loaded from. Serve instances that share a datastore can mount the same files at different paths without revoking each other's grants. They must load the same files: an instance started without the --grants-dir, or with different files in it, revokes the other instance's file:grants-dir/ grants on its next reconcile.

Earlier releases stored these grants with the file's absolute path as their source. The first reconcile after an upgrade revokes them and creates the same grants, with new IDs, under the new source names. See Upgrading from path-based sources.

Policy snapshot

Grants and group memberships are compiled into a policy snapshot. The snapshot is rebuilt:

  • On swamp access reload
  • On every grant or group change when --grant-reload auto is active
  • On server start (including admin grant materialization from --admins)

Changes to grants and groups do not take effect until the snapshot is rebuilt.