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:<name> |
Users in an IdP group that swamp-club captures through SSO |
user:<id> is the principal id: the OAuth sub in oauth mode, the username
in token mode. In oauth mode a grant file can also name a user as
username:<name>; see Usernames in grant files.
Local groups are resolved at policy reload time. IdP groups come from the
groups field of the OAuth provider's userinfo response, which holds the IdP
groups swamp-club captures through SSO. Without SSO it is empty, and
idp-group: grants match nobody.
Collectives are not a grant subject. They come from the userinfo field named by
--groups-field (default: collectives), decide admission through
--allowed-collectives, and reach grant conditions only as CEL
principal.collectives: an idp-group:<collective-slug> grant does not match a
collective's members. To give a team without SSO one grant, create a local group
and grant to group:<name>, or grant each member by user: or username:.
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 behavior method or workflow |
read |
Read data outputs, behavior definitions, vaults |
write |
Write data, create behaviors |
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.comTo 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.comThe 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's optional methods list scopes it to requests that name one of those
methods. A grant without methods, or with an empty list, is unscoped.
| Grant | Matches |
|---|---|
methods absent or empty |
Every request on the matched resource |
Allow with methods |
Only a request whose method is in the list. A request that names no method is not admitted |
Deny with methods |
A request whose method is in the list, and every request that names no method |
A scoped deny refuses requests that name no method, so it cannot be stepped around by one.
methods is valid on behavior:, vault: and access: grants:
| Kind | Requests that name a method |
|---|---|
behavior |
A method run, its cancel and attach, describing a method, and reads of what one run produced: its output, data and logs, its method history and history logs. Getting, creating, editing, deleting, retagging, validating and evaluating a behavior name no method |
vault |
Every vault request, by its vault verb |
access |
A run of a control-plane or admin-only behavior names its method. Admin checks that are not a method run, such as restricted commands and the admin superuser, name none |
workflow |
None |
data |
None |
A scoped allow on a behavior therefore does not let its holder read or change
the behavior's definition, and a scoped allow on access:* admits runs of the
methods it names but is not the admin superuser. Add an unscoped grant for the
requests that name no method.
Data and output a run saves to the datastore name no method. They are decided as
any method: a scoped run allow admits them, and a scoped deny refuses them.
swamp access grant create --subject user:alice --allow run \
--on 'behavior:hello' --methods read,listThis grant allows alice to run the read and list methods on behavior
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: "behavior:*"
methods: [read, list]Vault verbs
On a vault grant, methods names vault verbs. Each vault.* request names its
verb, the request type without vault.:
| Action | Verbs |
|---|---|
read |
get, describe, inspect, list-keys, read-secret, audit-trail, search |
write |
put, delete, annotate, create, edit (rename and repair included) |
admin |
migrate |
Vault operations in a serve run are judged with these verbs:
| Operation | Verb |
|---|---|
| Reading a secret | get |
| Listing a vault's keys | list-keys |
| Writing a secret | put |
| Deleting a secret | delete |
| Adding or removing an annotation | annotate |
| Reading an annotation or refresh hook | describe |
| Setting or removing a refresh hook | edit |
A grant that allows write with methods: [put] lets its holder store secrets
in the vault, but not read them, delete them or change a refresh hook:
swamp access grant create --subject user:ci --allow write \
--on 'vault:prod-db' --methods put# grants/ci-vault.yaml
grants:
- subject: "user:ci"
effect: allow
actions: [write]
resource: "vault:prod-db"
methods: [put]A put with refresh options still needs admin on data:vault, whatever the
vault grant names. Check a scoped grant with swamp access check --method:
$ swamp access check --subject user:ci --action write --on vault:prod-db --method put
ALLOW via grant 9965a355… (user:ci → write → vault:prod-db method put)
$ swamp access check --subject user:ci --action write --on vault:prod-db --method delete
DENY (implicit) — no matching grants for user:ci write vault:prod-db method deleteMethods that never match
swamp access grant create refuses methods on a workflow or data grant, and a
vault method that is not a vault verb. A grant file or stored grant with them
still loads with a warning, and its allow matches nothing;
swamp serve check-config reports the warning:
Grant files: 1 file(s) checked
! grants/methods.yaml entry 1: methods has no effect on a workflow grant: workflow requests name no method, so a methods-scoped allow matches nothing and a deny applies to every request; remove methodsConditions on methods
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 |
behavior:hello |
A specific behavior |
behavior:* |
All behaviors except access-control behaviors |
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.
model is the stored spelling of the behavior kind: behavior:hello and
model:hello are one selector, and serve stores and lists it as model:hello.
Any other kind, including behaviour and plurals, is refused.
Access-control records
Swamp stores its own control-plane state as behavior data in the repository, beside user behaviors. These behavior types hold it:
| Purpose | Behavior 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 behavior, 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 behavior:<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,deleteandrenameswamp behavior getandeditswamp behavior output get,dataandlogsswamp behavior history getandlogs- cancel and attach of a run on one of these behaviors
A read, write or run grant on data:* or behavior:* does not reach
these records. A request for one record is refused:
Access denied: user:bob does not have 'admin' on access:swamp/server-tokenswamp 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 behavior:* |
None |
Creating a behavior 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 behaviors 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 behavior 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 behavior, ns and tags for its data, tags for a workflow). A request by
the UUID of prod-db is denied by deny behavior: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 behavior 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 behavior output get, data and logs, swamp behavior history get and
logs, and swamp workflow history get and logs take an output or run id
prefix, or a behavior 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 behavior or workflow that owns it, never the prefix.
- When several behaviors 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 behavior or workflow and against the owner of the latest output or run it returns.
- Outputs of a deleted behavior are checked on the behavior 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 behavior output dataandswamp behavior output logsreturn data artifact content, so they need areadondata:for the owning behavior, asswamp data getdoes, as well as thebehavior:read. A principal granted only behavior reads cannot read artifact content this way.- Outputs and runs of an access-control behavior need
adminonaccess:swamp/<type>. Once the behavior 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 behavior type and a definition name instead of an existing behavior. Serve authorizes the requested behavior, 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 behavior lock whatever kind of method it runs.
Not yet covered
These forms still authorize differently:
*resources (swamp-club#2675)
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 |
The run-time rule bounds the vault reads Swamp makes for a run. It does not
bound a child process the method starts or a nested swamp, which reads the
host's vaults outside the run's scope; see
Secrets in Subprocesses and Serve Runs.
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:
- A deny for the action on
data:vault,data:Norvault:Nrefuses. - Otherwise
read/writeondata:vaultordata:N(adminon abehavior:resource, stored asmodel:, formigrate) admits the request. - Otherwise an allow for the action on
vault:Nadmits it, when the allow has nomethodslist or its list names the request's vault verb.putwith refresh options is the exception: it needsadminondata:vault, since a refresh hook is a shell command serve runs on its host. - 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. Each operation is also
judged with its vault verb, which a grant's methods list
matches. For vault N and action A:
- Reserved vaults. A vault whose name starts with
_, such as_token-secrets, is refused in every serve run unless the principal holdsadminonaccess:*. - Inert without vault grants. While the policy holds no
vault:grant, the operation is allowed. - Deny. A vault deny for A that matches N refuses.
- 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
and, if it has a
methodslist, the operation's verb. - 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 principalEach 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 behavior it belongs to: other behaviors' data, other behaviors' 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 behavior's data by name: data.latest, data.version, data.listVersions, data.findBySpec, model.<name>.resource, file.contents |
read on that data |
A behavior's definition: model.<name>.input, model.<name>.definition |
read on the behavior |
A computed behavior name, data.query, data.findByTag, the whole model map, a ns: or *: prefix, more than 32 named behaviors |
read on all data |
env |
write on the behavior; see env |
vault.get(...) |
Nothing when written; checked at run time. See Vault secrets |
Reading a behavior'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 behavior edit that changes the behavior'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 behavior, or on its
nested workflow. A step whose behavior 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 behavior, so env is
allowed. In model.method.run inputs, env needs write on the behavior; a
caller with only run is refused and told to reference env in the behavior
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
writeon a behavior or workflow may write anyvault.get(...)into it. Whether a run reads that vault depends on the vault grants of whoever triggers the run. model.method.runinputs may carryvault.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 behaviors, and
run on a behavior 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 behavior a run passes as an input. A target taken from another behavior's data,steps.*orrun.*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 behavior's global arguments with onlyrun. A direct run also saves them to the definition. A stored reference computed fromself.globalArgumentstherefore reads whatever behavior the runner names. Do not compute a data target from a global argument when runners should not choose the behavior. - Only new text is checked. Expressions stored before this check existed are not checked.
Behavior 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 behavior types
The --restricted-model-types flag locks specific behavior types behind the
admin superuser grant, the same authorization path used for the built-in
access-control behavior types.
swamp serve --auth-mode token --admins 'user:admin-id' \
--restricted-model-types 'command/shell'A behavior 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 behavior is computed by an expression, or one that
runs an access-control behavior, 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 pruneandswamp run gc- workflow input defaults and trigger inputs; a caller with
runon a workflow already chooses its inputs at run time
Behavior 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/deployNon-restricted behavior 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 typeAt startup, serve warns about each entry without a leading @ that names no
registered behavior 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/deplyoswamp serve check-config reports an entry that names no type as a failed
check. It does not check whether a behavior 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 exactlyRestricted behavior types vs restricted commands
Use --restricted-model-types to control which behaviors 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 behaviors. 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 behaviors is not enough. A caller without the grant is refused:
Access denied: user:bob does not have 'admin' on access:auditSee 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: "behavior:*"
condition: 'tags.env == "staging"'
- subject: "user:monitor"
effect: allow
actions: [run]
resource: "behavior:@acme/my-behavior"
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 behavior methods or vault verbs; omit for all. See 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:*", "behavior:*"]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.
Usernames in grant files
In oauth mode a grant file can name a user by username with username:<name>,
in subject or subjects:
grants:
- subjects: [username:alice, username:bob]
effect: allow
actions: [read, run]
resource: workflow:*username: is accepted only in grant files and only in oauth mode. Serve
resolves each name with the OAuth provider's /api/auth/resolve-user endpoint
and stores an ordinary user:<sub> grant. The grants API and
swamp access grant create do not accept username:.
In token and none modes a file that names username: is rejected. With
--auth-mode token a user principal is its username, so write user:<name>:
Grant files: 1 file(s) checked
✗ grants/team.yaml: Subject "username:alice": username subjects need --auth-mode oauth (this serve runs with --auth-mode token). With --auth-mode token a user principal is its username, so write user:<name>Pinning
Serve resolves each name once and pins the result in the _token-secrets vault,
apart from the admin and allowed-user resolutions.
- A pinned name is never looked up again. A grant follows the account, not the name: after a rename the grant still applies to the same account, and whoever registers a freed name inherits neither its allows nor its denies. Changing the file to the account's new name changes no grant.
- A name the provider reports as not found is recorded and never looked up again. Every file that names it is rejected until the name is removed or changed.
- Any other lookup failure records nothing. The name is looked up again when the
file changes, on
swamp access reload, or when serve restarts. - A pin is forgotten only when no grant file names it. To re-point a name, or to retry one recorded as not found, remove it from every file, reload or restart serve, then add it back.
- A
username:entry and auser:entry, or two usernames, that resolve to the samesubare duplicates and reject the file. - Pinned names need no provider, so a provider outage does not affect them. Serve instances that share a datastore share the pins.
Looking up new names
| When | Credential used for new names |
|---|---|
| Serve startup | The collective API key (SWAMP_API_KEY or --club-api-key-file), else a device grant |
Auto-reload and swamp access reload |
The collective API key only |
A serve run by a process manager cannot complete a device grant, so it needs the collective API key to start with a new name in a grant file. Without the key while serve runs, a file that names a new username is held back with its stored grants unchanged until serve restarts; the auto-reload tries it again only when the file changes.
One load looks up at most 200 new names. A file that names a new name past the cap is rejected.
swamp access grant list shows the username beside the sub, and audit records
name the actor by username. swamp access can-i reports the matching grant as
user:<sub>.
check-config
In oauth mode swamp serve check-config looks every name up afresh and cannot
read serve's pins, so a name serve recorded as not found can show as resolved
there. A name that does not resolve fails the check:
Grant files: 1 file(s) checked
✗ grants/team.yaml: Subject "username:alcie": Username 'alcie' not found on https://swamp-club.com (looked up with https://swamp-club.com/api/auth/resolve-user)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 otherA 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 autois active - On server start (including admin grant materialization from
--admins)
Changes to grants and groups do not take effect until the snapshot is rebuilt.
Related
- Access Commands — CLI reference for grant, group, and token commands
- Serve Flags
— token authentication failure reasons, including
invalid-principal - CEL Expressions — full CEL syntax reference
- swamp serve Architecture — why deny-wins, why default-deny, why hard refusals