Skip to Content
We are live but in Staging 🎉

Bucket Policy

S3-style access control on a bucket. See Core Concepts → Policy for the BucketPolicy type signature.

RPCHTTP
SetBucketPolicyPUT /admin/buckets/:name/policy
GetBucketPolicyGET /admin/buckets/:name/policy
DeleteBucketPolicyDELETE /admin/buckets/:name/policy

gRPC setup — grpcurl, endpoints, reflection, and field-name casing — is covered once in Conventions → Using gRPC.

The vocabulary is small — read this before writing a policy

K3 borrows S3’s shape, not its whole grammar. Four constraints decide whether a statement does anything at all:

FieldWhat is actually accepted
actionsExactly five names, matched literally: s3:GetObject, s3:PutObject, s3:DeleteObject, s3:ListBucket, s3:HeadObject. There is no wildcard actions3:* is rejected by SetBucketPolicy with INVALID_ARGUMENT.
resourcesBare object-key globs, not ARNs and not bucket-qualified paths: public/*, reports/2024-??.csv, *. Only * (any run of characters, including /) and ? (one character) are special. Omitted or empty → *.
principal.aws"*" (anyone, including anonymous), "authenticated" (any authenticated caller), or "org:<org_id>". Anything else is INVALID_ARGUMENT. Only the first entry is read at evaluation time — put one principal per statement.
effectPOLICY_EFFECT_ALLOW or POLICY_EFFECT_DENY. Omitted (UNSPECIFIED) is stored as Deny — the unparseable case is never the permissive one.

Evaluation is the S3 algorithm (k3-core/src/acl/policy.rs): collect statements whose action and some resource glob and principal all match; an explicit DENY returns immediately; any surviving ALLOW grants; no matching statement is an implicit deny.

Where a policy is enforced. A policy is consulted only when the bucket’s access_mode is CUSTOM, and only on api.data.dodil.io — the control-plane S3 proxy is what evaluates access mode and policy. The object.uk-lon-1.dodil.io byte plane does not evaluate them today: it authenticates every request and refuses unrecognised credentials with 401 AccessDenied, but it does not read your policy. Treat bucket policy as the control for the anonymous / cross-tenant sharing door, not as a second lock on your own signed traffic.

SetBucketPolicy

The HTTP body is the BucketPolicy value (the proto’s body: "policy" mapping makes the policy the top-level body, not a wrapper).

A successful SetBucketPolicy also flips the bucket’s access_mode to BUCKET_ACCESS_MODE_CUSTOM — you do not need a separate UpdateBucket. The policy must carry at least one statement; an empty statements array is INVALID_ARGUMENT.

AWS-shaped documents — HTTP only

The HTTP door normalizes an AWS-shaped document before deserializing it (services/storage/s3_policy_compat.rs), so a policy pasted straight out of the AWS console works:

  • Version / Statement / Sid / Effect / Principal / Action / Resource are accepted alongside the proto spellings.
  • Effect: "Allow" / "Deny" is case-insensitive and maps to the enum.
  • A bare string is accepted wherever a list is; a single statement object is accepted instead of an array; Principal: "*" is accepted instead of {"AWS": ["*"]}.
  • ARNs are reduced to key globs: arn:aws:s3:::kb-prod/public/* becomes public/*, and a bucket-level arn:aws:s3:::kb-prod becomes *. An ARN naming a different bucket is a 400 rather than a statement that could never match.
  • Condition, NotAction, NotResource and NotPrincipal are a hard 400 naming the key. K3 does not evaluate them, and honouring the rest of the document while dropping them would grant strictly more access than its author wrote.

This tolerance is the REST door’s alone. gRPC SetBucketPolicy and dodil data bucket policy set take the proto shape only — an ARN sent that way is stored verbatim and will never match a key.

Request

curl -sS -X PUT "https://api.data.dodil.io/admin/buckets/kb-prod/policy" \ -H "Authorization: Bearer $DODIL_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "version": "2024-01-01", "statements": [ { "sid": "allow-public-read", "effect": "POLICY_EFFECT_ALLOW", "principal": { "aws": ["*"] }, "actions": ["s3:GetObject"], "resources": ["public/*"] } ] }'

Response

A BucketPolicy — see Core Concepts → Policy.

GetBucketPolicy

A bucket with no policy is NOT_FOUND (“no policy set on this bucket”), not an empty BucketPolicy. Effects come back in the proto spelling (POLICY_EFFECT_ALLOW) even if you set them AWS-style, and resources come back as the stored globs even if you set them as ARNs — the response is the canonical form, so a get → edit → set round trip is safe.

Request

curl -sS "https://api.data.dodil.io/admin/buckets/kb-prod/policy" \ -H "Authorization: Bearer $DODIL_TOKEN"

Response

A BucketPolicy — see Core Concepts → Policy.

DeleteBucketPolicy

Clears the policy and forces access_mode back to BUCKET_ACCESS_MODE_PRIVATE — it does not restore whatever mode the bucket had before the policy was set. If the bucket was meant to stay PUBLIC, follow up with UpdateBucket.

Request

curl -sS -X DELETE "https://api.data.dodil.io/admin/buckets/kb-prod/policy" \ -H "Authorization: Bearer $DODIL_TOKEN"

Response

Empty (DeleteBucketPolicyResponse {}).


See also