hyperfile 0.9.1

A file-like interface abstraction layer provides random read and write capabilities for S3
Documentation
# S3 permissions

What an identity needs to use a hyperfile container, by what it does with
it. Everything a container stores is under its own prefix — `P/` below, for
a container at `s3://bucket/P` — and the WAL, if enabled, under the prefix
of its own `root_uri`.

## General purpose buckets

| To | Actions | Resource |
|---|---|---|
| Open read-only and read, including `open_cno` | `s3:GetObject` | `arn:aws:s3:::bucket/P/*` |
| | `s3:ListBucket`, with `s3:prefix` limited to `P/*` | `arn:aws:s3:::bucket` |
| Also create, write, flush, change attributes | `s3:PutObject` | `arn:aws:s3:::bucket/P/*` |
| Also `unlink` | `s3:DeleteObject` | `arn:aws:s3:::bucket/P/*` |
| The WAL | `s3:GetObject`, `s3:PutObject`, `s3:DeleteObject` | the WAL prefix, `/*` |
| | `s3:ListBucket`, with `s3:prefix` limited to it | `arn:aws:s3:::bucket` |

A minimal writer's policy, then:

```json
{
  "Version": "2012-10-17",
  "Statement": [
    { "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::bucket/P/*" },
    { "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::bucket",
      "Condition": { "StringLike": { "s3:prefix": "P/*" } } }
  ]
}
```

Notes:

- `s3:ListBucket` is needed even though a container that is only read and
  written is never listed. Hyperfile learns that an object does not exist
  from a `HeadObject` answered 404 — whether the inode is there before a
  create, whether the next checkpoint was written before an open — and
  without `s3:ListBucket` S3 answers 403 for a missing object instead.
  Create and open then fail with `PermissionDenied` on the `HeadObject` of
  the inode.
- `s3:PutObject` covers a segment written as a multipart upload: creating
  it, its parts, and completing it. Aborting one after a failure is
  `s3:AbortMultipartUpload`, which S3 allows the identity that created the
  upload by default; it has to be granted only where it is denied, or in a
  VPC endpoint policy, which does not extend that default. Without it a
  failed upload is left open, the flush's error says so, and only a
  lifecycle rule for incomplete multipart uploads removes it.
- Every write hyperfile makes is conditional: `PutObject` and
  `CompleteMultipartUpload` carry `If-None-Match: *` or `If-Match`. A bucket
  policy that refuses unconditional writes — [Enforce conditional
  writes](https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-writes-enforce.html),
  with `s3:ObjectCreationOperation` exempting the multipart create and
  parts — does not get in its way.
- Without the WAL nothing is deleted outside `unlink`. With it, the log is
  truncated as checkpoints are published and transaction markers are
  removed. Without `s3:DeleteObject` those deletes fail with a warning and
  the log grows, and aborting a transaction fails, since that deletes its
  records.
- The cleaner (`hyperfile-cleaner`, a separate tool) removes segments and
  lists the container: `s3:DeleteObject`, and `s3:ListBucket` on `P/*`.
- On a bucket encrypted with SSE-KMS, the identity also needs
  `kms:Decrypt` and `kms:GenerateDataKey` on the key.

## Directory buckets (S3 Express One Zone)

Requests to a directory bucket are authorised by the session the SDK opens
with `s3express:CreateSession`, not action by action:

| To | Session |
|---|---|
| Open read-only and read | `s3express:CreateSession`, `s3express:SessionMode` `ReadOnly` |
| Everything else, WAL included | `s3express:CreateSession`, `s3express:SessionMode` `ReadWrite` |

on `arn:aws:s3express:region:account:bucket/bucket-name`. A `ReadWrite`
session covers aborting a multipart upload and deleting. Without the
permission to open a session at all, the SDK's first request fails before
reaching the bucket, and hyperfile reports it as a dispatch failure
(`ConnectionReset`), not as `PermissionDenied`.

## How this was checked

Each row was run against a general purpose and a directory bucket with
temporary credentials restricted by a session policy to that row: create
and flush, reopen and a multipart flush, a read-only open that reads it all
back, a multipart flush with a part refused so that it is aborted, and
`unlink`. Without `s3:ListBucket`, every step failed at the inode's
`HeadObject`; with it, all but `unlink` succeeded, and failed uploads were
aborted without `s3:AbortMultipartUpload` being granted; `unlink` needed
`s3:DeleteObject`. A read-only identity (`s3:GetObject` and
`s3:ListBucket`, or a `ReadOnly` session) opened and read a container
another identity wrote. With writes allowed only when they carry
`If-None-Match` or `If-Match`, every step succeeded in both the default and
`wal` builds. The WAL rows are from the source, not from such a run.