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.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 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,listThis 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,deleteandrenameswamp model getandeditswamp model output get,dataandlogsswamp model method history getandlogs- 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-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 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 dataandswamp model output logsreturn data artifact content, so they need areadondata:for the owning model, asswamp data getdoes, as well as themodel:read. A principal granted only model reads cannot read artifact content this way.- Outputs and runs of an access-control model need
adminonaccess: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:
*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 |
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(adminonmodelformigrate) admits the request. - Otherwise an allow for the action on
vault:Nadmits it, exceptputwith refresh options, which needsadminondata:vault: 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. 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.
- 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 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
writeon a model 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 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.*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 model's global arguments with onlyrun. A direct run also saves them to the definition. A stored reference computed fromself.globalArgumentstherefore 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 pruneandswamp run gc- workflow input defaults and trigger inputs; a caller with
runon 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/deployNon-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 typeAt 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/deplyoswamp 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 exactlyRestricted 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: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: "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 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