Bucket Policy
S3-style access control on a bucket. See Core Concepts → Policy for the BucketPolicy type signature.
| RPC | HTTP |
|---|---|
SetBucketPolicy | PUT /admin/buckets/:name/policy |
GetBucketPolicy | GET /admin/buckets/:name/policy |
DeleteBucketPolicy | DELETE /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:
| Field | What is actually accepted |
|---|---|
actions | Exactly five names, matched literally: s3:GetObject, s3:PutObject, s3:DeleteObject, s3:ListBucket, s3:HeadObject. There is no wildcard action — s3:* is rejected by SetBucketPolicy with INVALID_ARGUMENT. |
resources | Bare 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. |
effect | POLICY_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_modeisCUSTOM, and only onapi.data.dodil.io— the control-plane S3 proxy is what evaluates access mode and policy. Theobject.uk-lon-1.dodil.iobyte plane does not evaluate them today: it authenticates every request and refuses unrecognised credentials with401 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/Resourceare 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/*becomespublic/*, and a bucket-levelarn:aws:s3:::kb-prodbecomes*. An ARN naming a different bucket is a400rather than a statement that could never match. Condition,NotAction,NotResourceandNotPrincipalare a hard400naming 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
HTTP
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
HTTP
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
HTTP
curl -sS -X DELETE "https://api.data.dodil.io/admin/buckets/kb-prod/policy" \
-H "Authorization: Bearer $DODIL_TOKEN"Response
Empty (DeleteBucketPolicyResponse {}).
See also
- Core Concepts → Policy —
BucketPolicytype - Buckets · CORS · Objects
- Conventions — auth, headers, error envelope
grpcurlreference — full flag set + reflection-disabled fallbacks