Skip to content

Access policies

Access policies define what authenticated users can do in S3 Lens — which providers, buckets, prefixes, and actions they may use. Policies are built into validated CompiledPolicy rule sets at startup in the s3lens-policy crate; changes require a restart.

See Authentication for OIDC setup and identity mapping. For route-to-action mapping and permission endpoints, see Authorization reference.

When policy.enabled is true (and auth.enabled is true):

  1. Every API request must include a valid session
  2. Middleware resolves the caller’s policies and roles once per request into Grants
  3. Handlers check the requested operation against allow and deny rules
  4. Explicit deny beats allow; if nothing matches, access is denied

When policy.enabled is false, authenticated users can perform any action. When auth.enabled is false, all requests are allowed (local development only).

A resource is a pathprovider/bucket/prefix — and scope is implied by depth:

on: selector Covers
"*" everything — all providers, buckets, and objects
"prod" provider prod, all its buckets, and all their objects
"prod/media" that bucket and every object in it
"prod/media/uploads/*" only keys under uploads/ in that bucket
"prod/team-*" every team-* bucket on prod, and their objects
policy:
enabled: true
defaults: true # built-ins: viewer, editor, admin
policies:
garage-viewer:
allow:
- actions: [providers:read, buckets:read, objects:read]
on: "garage-local"
superuser:
allow:
- actions: ["*"]
on: "*"
media-editor:
allow:
- actions: [objects:read, objects:write, objects:presign]
on: "garage-local/media"
deny:
- actions: [objects:delete]
on: "garage-local/media"
team-a-space:
allow:
- actions: [buckets:create, buckets:delete]
on: "prod/team-a-*"
uploader:
allow:
- actions: [providers:read]
on: "garage-local"
- actions: [buckets:read]
on: "garage-local/demo"
- actions: [objects:read, objects:write]
on: "garage-local/demo/uploads/*"
legacy-readonly:
iam:
file: policies/legacy-readonly.json
inline-iam:
iam:
inline: |
{ "Version": "2012-10-17",
"Statement": [{ "Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::demo/public/*" }] }

Use canonical names only plus "*" for all actions. Word aliases (read, write, delete, admin) are rejected at startup with an error listing valid actions.

Action What it allows
providers:read See a provider in listings
buckets:read See a bucket in listings
buckets:create Create buckets (scoped by bucket pattern in on:)
buckets:delete Delete buckets
buckets:policy Read, replace, and remove native S3 bucket policies
objects:read List, download, bulk download
objects:write Upload, copy, rename
objects:delete Delete and bulk delete
objects:presign Mint presigned URLs (composite — see below)

When defaults: true, S3 Lens registers three named policies using the same format as user policies:

Policy Grants
viewer providers:read, buckets:read, objects:read on "*"
editor viewer actions plus objects:write, objects:delete, objects:presign, buckets:create, buckets:policy on "*"
admin actions: ["*"] on "*"

Set defaults: false to omit built-ins and define every policy yourself.

Providers and buckets appear in listings only when providers:read / buckets:read cover them. Object-only grants do not imply listing visibility — include the read grants in your recipes.

Boilerplate for prefix-scoped uploaders (MinIO/AWS-style):

allow:
- actions: [providers:read]
on: "garage-local" # provider appears in provider list
- actions: [buckets:read]
on: "garage-local/demo" # bucket appears in bucket list
- actions: [objects:read, objects:write]
on: "garage-local/demo/uploads/*"

Prefix-scoped object grants still allow entering a bucket’s object listing; results are filtered per key (key_readable). Deep links to allowed keys work even without buckets:read.

A deny on on: "prod/media" blocks object writes and deletes in that bucket even when another policy would allow them. Deny wins across all bound compiled policies — the first matching deny short-circuits.

buckets:create and buckets:delete match against the bucket name in the selector’s bucket segment. Use trailing wildcards for multi-tenant self-service:

team-a-space:
allow:
- actions: [buckets:create, buckets:delete]
on: "prod/team-a-*" # may create/delete team-a-docs, team-a-media, …

Reserve a namespace with deny:

deny:
- actions: [buckets:create]
on: "prod/prod-*" # block buckets named prod-*

objects:presign is a capability modifier, not a superset of read or write:

  • Presigned download — route requires objects:presign and objects:read on the key
  • Presigned upload — requires objects:presign and objects:write on the key

A deny on objects:read blocks presigned downloads even when objects:presign is allowed.

  • "*" as a full segment matches anything
  • Trailing "team-*" matches prefix patterns on bucket or provider names
  • Mid-segment wildcards like "log*s" are startup errors, never silent no-match

Prefix trap: "uploads/*" covers keys under uploads/; "uploads*" (no slash) is a string prefix that also matches uploads-evil.txt — always use the slash form for directory prefixes.

roles:
editor:
policies: [garage-viewer, media-editor]
contributor:
policies: [uploader]
auth:
bindings:
- groups: [s3lens-editors]
role: editor
- subjects: [alice]
policies: [uploader]

Roles are flat — a role bundles policies, never other roles.

IAM JSON is an alternate policy body under iam: file or iam: inline (not a separate config section). Supported: Version: 2012-10-17, Allow/Deny, Action, Resource. Unsupported fields fail at startup:

Field Status
Condition Rejected
Principal Rejected
NotAction Rejected
NotResource Rejected

Resource ARNs map to on: selectors (arn:aws:s3:::bucket → bucket scope; arn:aws:s3:::bucket/prefix/* → object prefix scope).

  • Deny anywhere wins across all bound compiled policies
  • PolicyEngine + request-scoped Grants (resolved once in middleware)
  • Permission endpoints use a single allowed_actions pass per resource context