# PutObjectLegalHold

PutObjectLegalHold places a legal hold on an object version (`ON`) or lifts it (`OFF`). While a
version is held, it cannot be deleted or overwritten, and it stays that way until the hold is
lifted. A hold has no end date and is independent of any retention period, which makes it a good
fit when nobody knows yet how long something needs to be kept. Legal holds exist in buckets with
[Object Lock](https://docs.strg.com/object-locks) enabled and are set per object version - there is no bucket
default for them. [GetObjectLegalHold](https://docs.strg.com/s3-api/GetObjectLegalHold) reads a hold back.

**Info: Good to know on STRG**

- Each bucket is reached through its own regional endpoint, and a bucket in another region
  answers `404 NoSuchBucket` rather than a redirect.
- `x-amz-expected-bucket-owner` is welcome and has nothing to check: a key only ever reaches
  buckets of its own account.
- The request body is sent in one piece: an `aws-chunked` body is answered `501 NotImplemented`.
- A key with a `.` or `..` path segment (`a/../b`) cannot be put in a URL, so STRG answers
  `400 InvalidURI` for it.

## When you'd use it

- **A dispute or an investigation.** When a team is asked to preserve a set of contracts or
  emails, a script places a hold on each version involved, and lifts the holds once the matter is
  closed - however long that turns out to take.
- **Pausing a clean-up for one object.** A bucket whose old versions a lifecycle rule or a nightly
  job removes can keep a single version safe from both by holding it, without changing the rule.
- **An agent keeping evidence.** An agent asked to make sure a report is not deleted while it is
  being reviewed places a hold on that version, and the reviewer, or the agent later on, lifts it.

## Request

```http
PUT /photos/2024/cat.jpg?legal-hold 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: <SHA-256 of the body>
Content-Type: application/xml
Content-MD5: <base64 MD5 of the body>

<?xml version="1.0" encoding="UTF-8"?>
<LegalHold xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
  <Status>ON</Status>
</LegalHold>
```

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

### Permissions

The access key needs **Set legal holds** (`object-legal-hold:write`). A key limited to certain
buckets can hold it too, and then sets legal holds in those buckets; a key limited to a key prefix
sets them for objects under that prefix. [Access keys](https://docs.strg.com/access-keys) has the details.

### URI parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Bucket` | String | Yes | The bucket holding the object. |
| `Key` | String | Yes | The object's key. A key with a `.` or `..` path segment (`a/../b`) is answered `400 InvalidURI`. |
| `legal-hold` | Flag | Yes | Selects the legal hold subresource. Sent with no value. |
| `versionId` | String | No | The version to hold or release. Without it, the current version. |

A query parameter other than these is answered `501 NotImplemented`, so a client knows straight
away; presigning's `X-Amz-*` parameters and the JavaScript SDK's `x-id` are always welcome.

### Request headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Content-MD5` | String | No | Base64 MD5 of the body. When it is sent, STRG checks it, and a mismatch is answered `400 BadDigest`. The SDKs compute an integrity header on their own. |
| `x-amz-sdk-checksum-algorithm` | String | No | Set by the SDKs to name the checksum they send with the body. |
| `x-amz-expected-bucket-owner` | String | No | Welcome, with nothing to check: an access key only ever reaches its own account's buckets. |
| `x-amz-request-payer` | String | No | Not applicable: STRG has no Requester Pays buckets. |

### Request body

An XML document of up to 2 MiB; a larger one is answered `400 MaxMessageLengthExceeded`.

| Element | Type | Required | Description |
| --- | --- | --- | --- |
| `LegalHold` | Container | Yes | The root element. |
| `Status` | String | Yes | `ON` to place the hold, `OFF` to lift it. |

## Response

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

### Response headers

| Name | Description |
| --- | --- |
| `x-amz-request-id` | The request ID, handy for a support request. |
| `x-amz-request-charged` | Not sent: STRG has no Requester Pays buckets. |

### Response body

None.

## Errors

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

| Code | HTTP status | When |
| --- | --- | --- |
| `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. |
| `AccessDenied` | 403 | The access key lacks **Set legal holds**, or the bucket or key is outside its buckets or prefix. |
| `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 a 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. |
| `BadDigest` | 400 | `Content-MD5` does not match the body. |
| `XAmzContentSHA256Mismatch` | 400 | `x-amz-content-sha256` does not match the body. |
| `MalformedXML` | 400 | The body is not valid UTF-8. |
| `MaxMessageLengthExceeded` | 400 | The body is larger than 2 MiB. |
| `NotImplemented` | 501 | The request carries a query parameter other than `legal-hold` and `versionId` (presigning's `X-Amz-*` parameters and the JavaScript SDK's `x-id` aside), or an `aws-chunked` body. |
| `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. |

In a bucket without Object Lock there are no legal holds to set, so the request answers with a
`4xx` error and changes nothing.

## Differences from Amazon S3

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

| Aspect | Amazon S3 | STRG |
| --- | --- | --- |
| Bucket in another region | `301` redirect with `x-amz-bucket-region` | `404 NoSuchBucket`. A bucket is reached through its own regional endpoint |
| Nothing stored in the endpoint's region yet | Any valid key reaches the endpoint | `404 NoSuchBucket` |
| `x-amz-expected-bucket-owner` | Checked against the bucket owner | Welcome, with nothing to check |
| `aws-chunked` request body | Accepted | `501 NotImplemented`: the body is sent in one piece |
| Keys with `.` or `..` segments | Allowed | `400 InvalidURI`: such a key cannot be put in a URL, so its legal hold cannot be set |

**Danger: A held version stays, and stays billed**

Nobody can delete a version under a legal hold until the hold is lifted, and until then it is
billed as stored data. A hold that is no longer needed ends with `Status` `OFF`, after which the
version can be deleted as usual - unless a retention period still covers it.

## Examples

Placing a legal hold on the current version of `photos/2024/cat.jpg`:

**AWS CLI**

```bash
aws s3api put-object-legal-hold \
  --bucket my-bucket \
  --key photos/2024/cat.jpg \
  --legal-hold Status=ON \
  --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_legal_hold(
    Bucket="my-bucket",
    Key="photos/2024/cat.jpg",
    LegalHold={"Status": "ON"},
)
```

**JavaScript (AWS SDK v3)**

```javascript
import { PutObjectLegalHoldCommand, 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 PutObjectLegalHoldCommand({
    Bucket: "my-bucket",
    Key: "photos/2024/cat.jpg",
    LegalHold: { Status: "ON" },
  }),
);
```

Lifting the legal hold on a specific version, whose ID [ListObjectVersions](https://docs.strg.com/s3-api/ListObjectVersions)
lists:

**AWS CLI**

```bash
aws s3api put-object-legal-hold \
  --bucket my-bucket \
  --key photos/2024/cat.jpg \
  --version-id "$VERSION_ID" \
  --legal-hold Status=OFF \
  --endpoint-url https://s3.eu-central-1.strg.com \
  --region eu-central-1
```

**Python (boto3)**

```python
import os

import boto3

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

s3.put_object_legal_hold(
    Bucket="my-bucket",
    Key="photos/2024/cat.jpg",
    VersionId=os.environ["VERSION_ID"],
    LegalHold={"Status": "OFF"},
)
```

**JavaScript (AWS SDK v3)**

```javascript
import { PutObjectLegalHoldCommand, 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 PutObjectLegalHoldCommand({
    Bucket: "my-bucket",
    Key: "photos/2024/cat.jpg",
    VersionId: process.env.VERSION_ID,
    LegalHold: { Status: "OFF" },
  }),
);
```

An object can also be uploaded already under a legal hold: [PutObject](https://docs.strg.com/s3-api/PutObject),
[CopyObject](https://docs.strg.com/s3-api/CopyObject) and [CreateMultipartUpload](https://docs.strg.com/s3-api/CreateMultipartUpload) take
`x-amz-object-lock-legal-hold: ON`, which needs **Set legal holds** as well.

## Related

- [GetObjectLegalHold](https://docs.strg.com/s3-api/GetObjectLegalHold)
- [GetObjectRetention](https://docs.strg.com/s3-api/GetObjectRetention), [PutObjectRetention](https://docs.strg.com/s3-api/PutObjectRetention)
- [GetObjectLockConfiguration](https://docs.strg.com/s3-api/GetObjectLockConfiguration), [PutObjectLockConfiguration](https://docs.strg.com/s3-api/PutObjectLockConfiguration)
- [Object Lock](https://docs.strg.com/object-locks), including [locking individual objects](https://docs.strg.com/object-locks#locking-individual-objects)
- [Access keys](https://docs.strg.com/access-keys)
- [S3 API overview](https://docs.strg.com/s3-api/overview)
