Admin API

The Admin API provides HTTP endpoints for managing users, credentials, and system information. All endpoints (except login) require a JWT token with SuperUser role.

Authentication

First, obtain a JWT token:

TOKEN=$(curl -s -k -X POST https://localhost:9000/api/admin/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"root","password":"password12345"}' | jq -r '.token')

Use the token in subsequent requests:

curl -s -k https://localhost:9000/api/admin/users \
  -H "Authorization: Bearer $TOKEN"

Endpoints

Login

POST /api/admin/login

No authentication required.

curl -s -k -X POST https://localhost:9000/api/admin/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"root","password":"password12345"}'

Response:

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "expires_at": "2026-02-11T10:00:00Z"
}

A wrong password and an unknown account both answer 401 with code InvalidCredentials — identical responses, so the endpoint cannot be used to find out which account names exist.

List Users

GET /api/admin/users

curl -s -k https://localhost:9000/api/admin/users \
  -H "Authorization: Bearer $TOKEN"

Response: Array of user objects (password hashes and secret keys are never returned).

Create User

POST /api/admin/users

curl -s -k -X POST https://localhost:9000/api/admin/users \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"username":"alice","password":"alice123","role":"Writer"}'

Request body:

Field Type Required Description
username string Yes 3-32 characters, alphanumeric and underscore
password string Yes User password
role string Yes Reader, Writer, or SuperUser

Get User

GET /api/admin/users/{id}

curl -s -k https://localhost:9000/api/admin/users/<user-id> \
  -H "Authorization: Bearer $TOKEN"

Update User

PUT /api/admin/users/{id}

curl -s -k -X PUT https://localhost:9000/api/admin/users/<user-id> \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"role":"Reader"}'

Request body (all fields optional):

Field Type Description
password string New password
role string New role (Reader, Writer, SuperUser)
is_active boolean Enable or disable the account

Delete User

DELETE /api/admin/users/{id}

curl -s -k -X DELETE https://localhost:9000/api/admin/users/<user-id> \
  -H "Authorization: Bearer $TOKEN"

Response: 204 No Content

Generate S3 Credentials

POST /api/admin/users/{id}/credentials

curl -s -k -X POST https://localhost:9000/api/admin/users/<user-id>/credentials \
  -H "Authorization: Bearer $TOKEN"

Response:

{
  "access_key": "S4AKxxxxxxxxxxxxxxxxxxxx",
  "secret_key": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

Important: The secret_key is shown only once. Store it securely — it cannot be retrieved again.

Delete S3 Credentials

DELETE /api/admin/users/{id}/credentials

curl -s -k -X DELETE https://localhost:9000/api/admin/users/<user-id>/credentials \
  -H "Authorization: Bearer $TOKEN"

Response: 204 No Content

Create Bucket

PUT /api/admin/buckets/{name}

curl -s -k -X PUT https://localhost:9000/api/admin/buckets/my-bucket \
  -H "Authorization: Bearer $TOKEN"

Response:

{ "name": "my-bucket" }

The name must satisfy the same S3 rules the S3 API enforces (3–63 characters, lowercase letters, digits, hyphens and dots, starting with a letter or digit and not ending with a hyphen) — otherwise 400 InvalidBucketName. A name already in use answers 409 BucketAlreadyExists.

Delete Bucket

DELETE /api/admin/buckets/{name}

Deletes a bucket. By default the bucket must be empty — a bucket that still holds objects answers 409 BucketNotEmpty, and an unknown bucket answers 404 NoSuchBucket.

curl -s -k -X DELETE https://localhost:9000/api/admin/buckets/my-bucket \
  -H "Authorization: Bearer $TOKEN"

Force delete — recursively deletes all objects in the bucket first:

curl -s -k -X DELETE "https://localhost:9000/api/admin/buckets/my-bucket?force=true" \
  -H "Authorization: Bearer $TOKEN"

Response: 204 No Content

Parameter Type Default Description
force bool false When true, all objects are deleted before removing the bucket

List Bucket Objects

GET /api/admin/buckets/{name}/objects

curl -s -k "https://localhost:9000/api/admin/buckets/my-bucket/objects?max-keys=100" \
  -H "Authorization: Bearer $TOKEN"

Query parameters:

Parameter Type Description
prefix string Filter objects by key prefix
max-keys integer Objects and directories per page: default 50, from 1 to 1000
continuation-token string Opaque token from the previous page's next_continuation_token

Keys below prefix that contain another / are grouped into a directory, listed once in common_prefixes; a page that ends with a directory continues past all of it. The answer is { "objects": [...], "common_prefixes": [...], "is_truncated": bool, "next_continuation_token": "...", "complete": bool }. On a cluster node the bucket is listed from every node of the pool and merged by key, as S3 ListObjects is, and complete is false when a node did not answer; on a single node it is always true.

An unknown bucket answers 404 NoSuchBucket; an existing bucket with nothing in it answers 200 with an empty objects array.

Bucket Stats

GET /api/admin/bucket-stats

Returns every bucket with its number of objects and their bytes.

curl -s -k https://localhost:9000/api/admin/bucket-stats \
  -H "Authorization: Bearer $TOKEN"
{
  "buckets": [
    { "name": "photos", "objects_count": 1205, "storage_used_bytes": 4823110, "created_at": 1790812345000000000 }
  ],
  "total_storage_bytes": 4823110,
  "complete": true
}

storage_used_bytes is the size of the objects as clients see them, before deduplication and replication; created_at is in nanoseconds since the epoch. On a cluster node the numbers are the pool's, whichever node is asked: buckets and objects are listed from every node of the pool and merged by key, so an object counts once, at the size of its newest version, wherever its copies are. complete is false when a node of the pool did not answer: objects only that node holds are then missing from the numbers. On a single node it is always true. Every bucket is listed in full, page by page, so the request takes longer the more objects the buckets hold.

Error Responses

Every failure returns JSON with a human-readable error and a stable, machine-readable code. Branch on code, not on the message text:

{ "error": "Bucket 'my-bucket' is not empty. Delete its objects first, or repeat the request with ?force=true",
  "code": "BucketNotEmpty" }
Code Status Meaning
Unauthorized 401 Token missing, malformed or expired
InvalidCredentials 401 Login rejected: unknown account or wrong password
Forbidden 403 Authenticated, but the role is not SuperUser
UserNotFound 404 No user with this id or name
UserAlreadyExists 409 A user with this name already exists
InvalidUsername 400 Username breaks the format rules
NoS3Credentials 404 The user has no S3 credentials issued
NoSuchBucket 404 No bucket with this name
BucketAlreadyExists 409 A bucket with this name already exists
BucketNotEmpty 409 The bucket still holds objects and force was not set
InvalidBucketName 400 Bucket name breaks the S3 naming rules
InvalidData 400 Request body or parameters could not be interpreted
InternalError 500 Server-side fault (storage or IAM subsystem)

500 means a fault on the server, never a mistake in the request — anything the caller can correct itself is reported as 4xx.