# Object-store STS mint (control plane)

**Status:** implemented
**Related client PR:** [mem9-ai/drive9#845](https://github.com/mem9-ai/drive9/pull/845)

This is the server-only half of object-store access. The CLI, FUSE mount, and
rclone data plane live in `mem9-ai/drive9`. This repository never imports the
object I/O stack: file bytes stay laptop ↔ object store.

## What the server does

1. Org admins register **object backends** (`org_object_backends`) with a
   high-privilege identity (static key or role). Secrets are KMS-wrapped.
2. Org admins bind each tenant to a **customer prefix id**
   (`tenant_object_namespaces.namespace_id`). Empty means mint is refused.
3. A tenant owner key calls `POST /v1/object-credentials` with a URI. The
   server mints a short-lived session and returns it. Scoped workspace-zone
   tokens cannot mint.

Supported mint schemes: S3, COS, TOS, OSS, GCS, Azure. First-wave customer
storage is **S3 / TOS / COS**. OSS, Azure, and GCS are second-wave and this
control plane is less strict about their isolation edge cases.

Isolation is not the same for every scheme:

- **S3 / COS / TOS:** STS session policy is prefix-scoped (`bucket/prefix/*`).
  `ListBucket` / `GetBucket` is conditioned on the directory form
  (`prefix/` and `prefix/*`) so a namespace `cust` cannot list keys of
  `cust-evil`. rclone 1.69 lists with a trailing slash.
- **OSS:** same object ARN prefix, but list still uses a string prefix
  (`prefix` and `prefix/*`).
- **Azure:** container SAS. Namespace must equal the container; backend
  prefix must be empty. Isolation is one container per tenant.
- **GCS:** OAuth access token with the org service account's IAM. It is
  **not** prefix-scoped and is not downscoped with a Credential Access
  Boundary. Isolation is operational: one dedicated (or IAM-limited) SA
  per bucket. Namespace must equal the bucket; backend prefix must be
  empty. Prefix isolation would be a later CAB follow-up.

## Shared-schema compatibility

Shared tenants (`tidb_cloud_native_shared`) **do not write**
`tenant_tidbcloud_org_bindings`. Namespace is therefore a dedicated meta table
keyed by `tenant_id`, not a column on the dedicated org-binding row.

- Org membership for admin and mint uses the existing fork:
  dedicated → `tenant_tidbcloud_org_bindings`; shared → `db_pool.org_id` via
  `GetSharedDBForTenant`. The metrics sentinel `"guest"` is not an
  organization id; mint ignores it and looks up the real binding.
- `org_object_backends` is org-scoped control-plane data. It is not a tenant
  table and has no `fs_id` discriminator.
- Tenant delete (dedicated, shared, and unclaimed-shared discard) removes the
  namespace row in the same transaction as extract-config cleanup.

Do not add `object_namespace_id` to `tenant_tidbcloud_org_bindings`. That would
make shared tenants unable to mint without creating a binding row, which
`TestUpsertTenantTiDBCloudOrgBindingRejectsSharedTenant` forbids.

## HTTP surface

| Method | Path | Auth |
| --- | --- | --- |
| GET/POST | `/v1/admin/object-backends` | TiDB Cloud admin |
| GET/PATCH/DELETE | `/v1/admin/object-backends/{id}` | TiDB Cloud admin |
| GET/PUT/DELETE | `/v1/admin/tenants/{id}/object-namespace` | TiDB Cloud admin |
| POST | `/v1/object-credentials` | tenant owner key |

Inner control-plane IAM URIs:

- `/drive9/orgs/{org_id}/object-backend/{view,modify}`
- `/drive9/orgs/{org_id}/object-namespace/{view,modify}`

## Mint egress

STS endpoints and GCS `token_uri` are customer-controlled. Mint talks to
them with the same SSRF policy as tenant extractor URLs: no env proxy, no
redirects, and a dial-time reject of link-local / RFC1918 / CGNAT metadata
(including Aliyun `100.100.100.200`). Loopback HTTP/HTTPS is allowed for
local STS (MinIO). GCS mint ignores `token_uri` in the service-account JSON
and always uses Google's JWT token URL. Raw STS HTTP error bodies are not
returned to the tenant.
