# PutObjectAcl

PutObjectAcl sets an object's access control list (ACL) - who may do what with the object. On
STRG, objects share their bucket's ACL rather than carrying one of their own: `private` when
the bucket is *Protected*, and `public-read` when its *Bucket Protection* is off. A request that
names the ACL the bucket already has succeeds and leaves everything as it was, which keeps tools
that set an object's ACL after each upload working as they are. One setting per bucket decides who
can read its objects, and [PutBucketAcl](https://docs.strg.com/s3-api/PutBucketAcl) is the way to change it for every
object at once. [GetObjectAcl](https://docs.strg.com/s3-api/GetObjectAcl) reads an object's ACL.

**Info: Good to know on STRG**

- The ACL travels in the `x-amz-acl` header, as `private` or `public-read` - the same value as the
  bucket's ACL.
- A canned ACL different from the bucket's answers `501 NotImplemented`, so a client knows straight
  away that the object follows its bucket.
- An `AccessControlPolicy` body and the `x-amz-grant-*` headers answer `501 NotImplemented` too:
  the canned ACL is the way to say it.
- The key needs *Manage buckets*, a permission for keys with access to all buckets, rather than a
  write permission on objects.
- `x-amz-expected-bucket-owner` is welcome and has nothing to check: a key only ever reaches
  buckets of its own account.

## When you'd use it

- **Tools written for Amazon S3.** Upload tools, sync clients and backup scripts that set an
  object's ACL right after uploading it - `private` for a nightly backup, `public-read` for a
  website's images - carry on as usual, as long as they ask for the ACL their bucket already has.
- **Confirming a setting.** A script or an agent publishing files can send the ACL it expects
  the object to have; a `200 OK` confirms the bucket gives the object exactly that access.

## Request

```http
PUT /photos/2024/cat.jpg?acl HTTP/1.1
Host: my-bucket.s3.eu-central-1.strg.com
Authorization: AWS4-HMAC-SHA256 Credential=...
x-amz-date: 20260926T120000Z
x-amz-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-amz-acl: private
Content-Length: 0
```

The path-style form is `PUT /my-bucket/photos/2024/cat.jpg?acl` on `s3.eu-central-1.strg.com` -
[Addressing](https://docs.strg.com/s3-api/overview#addressing) explains both.

### Permissions

The access key needs **Manage buckets** (`buckets:write`), a permission held by keys with access
to all buckets. For a key limited to a key prefix, the object's key starts with that prefix.
Anything else answers `403 AccessDenied`. [Access keys](https://docs.strg.com/access-keys) has the details.

### URI parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Bucket` | String | Yes | The bucket that holds the object. |
| `Key` | String | Yes | The object's key. A key containing a `.` or `..` path segment (`a/../b`) cannot be put in a URL and answers `400 InvalidURI`; its ACL is its bucket's already. |
| `acl` | Flag | Yes | Selects the ACL subresource. Sent with no value. |
| `versionId` | String | No | A specific version of the object. Without it, the current version is used. |

Any other query parameter answers `501 NotImplemented` (presigning's `X-Amz-*` parameters aside).

### Request headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `x-amz-acl` | String | Yes | The canned ACL: `private` or `public-read`, matching the bucket's ACL. A value different from the bucket's answers `501 NotImplemented`, and so does any other canned ACL (`public-read-write`, `authenticated-read`, `bucket-owner-full-control`, ...). |
| `x-amz-grant-full-control` | String | No | STRG answers `501 NotImplemented`; the object's access is its bucket's. |
| `x-amz-grant-read` | String | No | STRG answers `501 NotImplemented`; the object's access is its bucket's. |
| `x-amz-grant-read-acp` | String | No | STRG answers `501 NotImplemented`; the object's access is its bucket's. |
| `x-amz-grant-write` | String | No | STRG answers `501 NotImplemented`; the object's access is its bucket's. |
| `x-amz-grant-write-acp` | String | No | STRG answers `501 NotImplemented`; the object's access is its bucket's. |
| `Content-MD5` | String | No | Welcome. The request has no body, so there is nothing for it to protect. |
| `x-amz-sdk-checksum-algorithm` | String | No | Welcome, together with the checksum header the SDK sends beside it. |
| `x-amz-request-payer` | String | No | Has no effect: STRG has no Requester Pays buckets, so there is never a need to send it. |
| `x-amz-expected-bucket-owner` | String | No | Welcome, with nothing to check: an access key only ever reaches its own account's buckets. |

Every `x-amz-*` header in the request is part of its signature.

### Request body

None. The ACL is said entirely by `x-amz-acl`, so an `AccessControlPolicy` document answers
`501 NotImplemented`.

## Response

### Response headers

| Name | Description |
| --- | --- |
| `x-amz-request-id` | The request ID, handy for a support request. |
| `x-amz-version-id` | The version of the object whose ACL was set. |

`x-amz-request-charged` is never returned, since STRG has no Requester Pays.

### Response body

None. A successful request answers `200 OK` with an empty body.

## Errors

These are the errors this request can return, and the situation behind each one.

| Code | HTTP status | When |
| --- | --- | --- |
| `NotImplemented` | 501 | `x-amz-acl` differs from the bucket's ACL, or the request carries an `AccessControlPolicy` body, an `x-amz-grant-*` header, a canned ACL other than `private` or `public-read`, or a query parameter other than `acl` and `versionId` (presigning's `X-Amz-*` parameters aside). |
| `NoSuchKey` | 404 | The object does not exist. |
| `NoSuchBucket` | 404 | The bucket does not exist, it is in a region other than the endpoint's, or the account has no storage in this endpoint's region yet. |
| `AccessDenied` | 403 | The access key lacks *Manage buckets*, the object's key is outside its key prefix, the account is not active, an `x-amz-*` header is unsigned, or the request is unsigned. |
| `InvalidAccessKeyId` | 403 | The access key does not exist, has expired or been revoked - or it was created moments ago and is not usable yet in this region where the account already stores data, in which case the same request works shortly afterwards. |
| `InvalidURI` | 400 | The key contains a `.` or `..` path segment. |
| `SignatureDoesNotMatch` | 403 | The request signature does not match. |
| `RequestTimeTooSkewed` | 403 | The request time is more than 15 minutes from the server's. |
| `AuthorizationHeaderMalformed` | 400 | The signature's credential scope names a region other than the endpoint's. |
| `SlowDown` | 503 | Too many requests. The `Retry-After` header says when a retry is welcome; the AWS SDKs retry on their own. |
| `ServiceUnavailable` | 503 | A temporary failure; the same request can be retried. |
| `InternalError` | 500 | An unexpected failure; the same request can be retried. |

## Differences from Amazon S3

The table lists each place where STRG behaves differently, and what that means for a client.

| Aspect | Amazon S3 | STRG |
| --- | --- | --- |
| Per-object ACLs | Each object has its own ACL | An object's ACL is always its bucket's - one setting per bucket, changed with [PutBucketAcl](https://docs.strg.com/s3-api/PutBucketAcl) |
| `x-amz-acl` | Any canned ACL, applied to the object | `private` or `public-read`, matching the bucket's current ACL. Any other value answers `501 NotImplemented` |
| Explicit grants | `x-amz-grant-*` headers or an `AccessControlPolicy` body | `501 NotImplemented`: the canned ACL says it all |
| Permission | `WRITE_ACP` on the object (`s3:PutObjectAcl`) | *Manage buckets*, on a key with access to all buckets |
| Bucket in another region | `301` redirect with `x-amz-bucket-region` | `404 NoSuchBucket`. A bucket is reached through its own regional endpoint |
| Keys with `.` or `..` segments | Allowed | `400 InvalidURI`. Such a key's ACL is its bucket's already |
| `x-amz-expected-bucket-owner` | Checked against the bucket owner | Welcome, with nothing to check |

## Examples

Setting the ACL of `photos/2024/cat.jpg` in a *Protected* bucket, whose ACL is `private`:

**AWS CLI**

```bash
aws s3api put-object-acl \
  --bucket my-bucket \
  --key photos/2024/cat.jpg \
  --acl private \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1
```

**Python (boto3)**

```python
import boto3

s3 = boto3.client(
    "s3",
    endpoint_url="https://s3.eu-central-1.strg.com",
    region_name="eu-central-1",
)

s3.put_object_acl(Bucket="my-bucket", Key="photos/2024/cat.jpg", ACL="private")
```

**JavaScript (AWS SDK v3)**

```javascript
import { PutObjectAclCommand, S3Client } from "@aws-sdk/client-s3";

const client = new S3Client({
  endpoint: "https://s3.eu-central-1.strg.com",
  region: "eu-central-1",
});

await client.send(
  new PutObjectAclCommand({ Bucket: "my-bucket", Key: "photos/2024/cat.jpg", ACL: "private" }),
);
```

A script that does not know the bucket's setting can read the bucket's ACL first and send the
same one, so the request succeeds whichever *Bucket Protection* setting the bucket has:

**AWS CLI**

```bash
ACL=$(aws s3api get-bucket-acl \
  --bucket my-bucket \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1 \
  --query "length(Grants[?Grantee.URI=='http://acs.amazonaws.com/groups/global/AllUsers'])" \
  --output text)

if [ "$ACL" -gt 0 ]; then ACL=public-read; else ACL=private; fi

aws s3api put-object-acl \
  --bucket my-bucket \
  --key photos/2024/cat.jpg \
  --acl "$ACL" \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1
```

**Python (boto3)**

```python
import boto3

ALL_USERS = "http://acs.amazonaws.com/groups/global/AllUsers"

s3 = boto3.client(
    "s3",
    endpoint_url="https://s3.eu-central-1.strg.com",
    region_name="eu-central-1",
)

grants = s3.get_bucket_acl(Bucket="my-bucket")["Grants"]
public = any(grant["Grantee"].get("URI") == ALL_USERS for grant in grants)

s3.put_object_acl(
    Bucket="my-bucket",
    Key="photos/2024/cat.jpg",
    ACL="public-read" if public else "private",
)
```

**JavaScript (AWS SDK v3)**

```javascript
import { GetBucketAclCommand, PutObjectAclCommand, S3Client } from "@aws-sdk/client-s3";

const ALL_USERS = "http://acs.amazonaws.com/groups/global/AllUsers";

const client = new S3Client({
  endpoint: "https://s3.eu-central-1.strg.com",
  region: "eu-central-1",
});

const { Grants = [] } = await client.send(new GetBucketAclCommand({ Bucket: "my-bucket" }));
const isPublic = Grants.some((grant) => grant.Grantee?.URI === ALL_USERS);

await client.send(
  new PutObjectAclCommand({
    Bucket: "my-bucket",
    Key: "photos/2024/cat.jpg",
    ACL: isPublic ? "public-read" : "private",
  }),
);
```

Reading the bucket's ACL takes **Read bucket settings**, which every access key holds -
[GetBucketAcl](https://docs.strg.com/s3-api/GetBucketAcl) has the details.

## Related

- [GetObjectAcl](https://docs.strg.com/s3-api/GetObjectAcl)
- [PutBucketAcl](https://docs.strg.com/s3-api/PutBucketAcl)
- [GetBucketAcl](https://docs.strg.com/s3-api/GetBucketAcl)
- [PutObject](https://docs.strg.com/s3-api/PutObject)
- [Access keys](https://docs.strg.com/access-keys)
- [S3 API overview](https://docs.strg.com/s3-api/overview)
