mirror of
https://github.com/ferdzo/fs.git
synced 2026-04-04 20:36:25 +00:00
S3 compatibility and openapi spec.
This commit is contained in:
@@ -51,6 +51,10 @@ Required when `AUTH_ENABLED=true`:
|
|||||||
|
|
||||||
Reference: `auth/README.md`
|
Reference: `auth/README.md`
|
||||||
|
|
||||||
|
Additional docs:
|
||||||
|
- Admin OpenAPI spec: `docs/admin-api-openapi.yaml`
|
||||||
|
- S3 compatibility matrix: `docs/s3-compatibility.md`
|
||||||
|
|
||||||
Health:
|
Health:
|
||||||
- `GET /healthz`
|
- `GET /healthz`
|
||||||
- `HEAD /healthz`
|
- `HEAD /healthz`
|
||||||
|
|||||||
@@ -178,8 +178,6 @@ func (s *Service) AuthenticateRequest(r *http.Request) (RequestContext, error) {
|
|||||||
authType = "sigv4-presign"
|
authType = "sigv4-presign"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin API authorization is enforced in admin handlers (bootstrap-only).
|
|
||||||
// We still require valid SigV4 credentials here, but skip S3 action policy checks.
|
|
||||||
if strings.HasPrefix(r.URL.Path, "/_admin/") {
|
if strings.HasPrefix(r.URL.Path, "/_admin/") {
|
||||||
return RequestContext{
|
return RequestContext{
|
||||||
Authenticated: true,
|
Authenticated: true,
|
||||||
|
|||||||
336
docs/admin-api-openapi.yaml
Normal file
336
docs/admin-api-openapi.yaml
Normal file
@@ -0,0 +1,336 @@
|
|||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: fs Admin API
|
||||||
|
version: 1.0.0
|
||||||
|
description: |
|
||||||
|
JSON admin API for managing local users and policies.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Base path is `/_admin/v1`.
|
||||||
|
- Requests must be AWS SigV4 signed.
|
||||||
|
- Only the bootstrap access key is authorized for admin endpoints.
|
||||||
|
servers:
|
||||||
|
- url: http://localhost:2600
|
||||||
|
description: Local development
|
||||||
|
security:
|
||||||
|
- AwsSigV4: []
|
||||||
|
paths:
|
||||||
|
/_admin/v1/users:
|
||||||
|
post:
|
||||||
|
summary: Create user
|
||||||
|
operationId: createUser
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CreateUserRequest'
|
||||||
|
responses:
|
||||||
|
'201':
|
||||||
|
description: User created
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserResponse'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/InvalidRequest'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'409':
|
||||||
|
$ref: '#/components/responses/UserAlreadyExists'
|
||||||
|
'503':
|
||||||
|
$ref: '#/components/responses/AuthDisabled'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalError'
|
||||||
|
get:
|
||||||
|
summary: List users
|
||||||
|
operationId: listUsers
|
||||||
|
parameters:
|
||||||
|
- name: limit
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
minimum: 1
|
||||||
|
maximum: 1000
|
||||||
|
default: 100
|
||||||
|
- name: cursor
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: User summaries
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserListResponse'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/InvalidRequest'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'503':
|
||||||
|
$ref: '#/components/responses/AuthDisabled'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalError'
|
||||||
|
|
||||||
|
/_admin/v1/users/{accessKeyId}:
|
||||||
|
get:
|
||||||
|
summary: Get user with policy
|
||||||
|
operationId: getUser
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/AccessKeyId'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: User details
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserResponse'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/UserNotFound'
|
||||||
|
'503':
|
||||||
|
$ref: '#/components/responses/AuthDisabled'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalError'
|
||||||
|
delete:
|
||||||
|
summary: Delete user
|
||||||
|
operationId: deleteUser
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/AccessKeyId'
|
||||||
|
responses:
|
||||||
|
'204':
|
||||||
|
description: User deleted
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/InvalidRequest'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/UserNotFound'
|
||||||
|
'503':
|
||||||
|
$ref: '#/components/responses/AuthDisabled'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalError'
|
||||||
|
|
||||||
|
/_admin/v1/users/{accessKeyId}/policy:
|
||||||
|
put:
|
||||||
|
summary: Replace user policy
|
||||||
|
operationId: setUserPolicy
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/AccessKeyId'
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SetPolicyRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: User details with updated policy
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserResponse'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/InvalidRequest'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/UserNotFound'
|
||||||
|
'503':
|
||||||
|
$ref: '#/components/responses/AuthDisabled'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalError'
|
||||||
|
|
||||||
|
/_admin/v1/users/{accessKeyId}/status:
|
||||||
|
put:
|
||||||
|
summary: Set user status
|
||||||
|
operationId: setUserStatus
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/AccessKeyId'
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SetStatusRequest'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: User details with updated status
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UserResponse'
|
||||||
|
'400':
|
||||||
|
$ref: '#/components/responses/InvalidRequest'
|
||||||
|
'403':
|
||||||
|
$ref: '#/components/responses/Forbidden'
|
||||||
|
'404':
|
||||||
|
$ref: '#/components/responses/UserNotFound'
|
||||||
|
'503':
|
||||||
|
$ref: '#/components/responses/AuthDisabled'
|
||||||
|
'500':
|
||||||
|
$ref: '#/components/responses/InternalError'
|
||||||
|
|
||||||
|
components:
|
||||||
|
securitySchemes:
|
||||||
|
AwsSigV4:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: Authorization
|
||||||
|
description: |
|
||||||
|
AWS Signature Version 4 headers are required (`Authorization`, `x-amz-date`,
|
||||||
|
and for payload-signed requests `x-amz-content-sha256`).
|
||||||
|
Only bootstrap credential is authorized for admin endpoints.
|
||||||
|
parameters:
|
||||||
|
AccessKeyId:
|
||||||
|
name: accessKeyId
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: User access key ID
|
||||||
|
responses:
|
||||||
|
InvalidRequest:
|
||||||
|
description: Invalid request input
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AdminError'
|
||||||
|
Forbidden:
|
||||||
|
description: Authenticated but not allowed
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AdminError'
|
||||||
|
UserAlreadyExists:
|
||||||
|
description: User already exists
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AdminError'
|
||||||
|
UserNotFound:
|
||||||
|
description: User not found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AdminError'
|
||||||
|
AuthDisabled:
|
||||||
|
description: Authentication subsystem disabled
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AdminError'
|
||||||
|
InternalError:
|
||||||
|
description: Internal server error
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AdminError'
|
||||||
|
schemas:
|
||||||
|
AdminError:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
code:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
requestId:
|
||||||
|
type: string
|
||||||
|
required: [code, message]
|
||||||
|
PolicyStatement:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
effect:
|
||||||
|
type: string
|
||||||
|
enum: [allow, deny]
|
||||||
|
actions:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
minItems: 1
|
||||||
|
bucket:
|
||||||
|
type: string
|
||||||
|
default: "*"
|
||||||
|
prefix:
|
||||||
|
type: string
|
||||||
|
default: "*"
|
||||||
|
required: [effect, actions]
|
||||||
|
Policy:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
principal:
|
||||||
|
type: string
|
||||||
|
description: Server-managed; overwritten with target access key ID.
|
||||||
|
statements:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/PolicyStatement'
|
||||||
|
minItems: 1
|
||||||
|
required: [statements]
|
||||||
|
CreateUserRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
accessKeyId:
|
||||||
|
type: string
|
||||||
|
secretKey:
|
||||||
|
type: string
|
||||||
|
description: If omitted, server generates one.
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [active, disabled]
|
||||||
|
default: active
|
||||||
|
policy:
|
||||||
|
$ref: '#/components/schemas/Policy'
|
||||||
|
required: [accessKeyId, policy]
|
||||||
|
SetPolicyRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
policy:
|
||||||
|
$ref: '#/components/schemas/Policy'
|
||||||
|
required: [policy]
|
||||||
|
SetStatusRequest:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [active, disabled]
|
||||||
|
required: [status]
|
||||||
|
UserListItem:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
accessKeyId:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: string
|
||||||
|
enum: [active, disabled]
|
||||||
|
createdAt:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
updatedAt:
|
||||||
|
type: integer
|
||||||
|
format: int64
|
||||||
|
required: [accessKeyId, status, createdAt, updatedAt]
|
||||||
|
UserListResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/UserListItem'
|
||||||
|
nextCursor:
|
||||||
|
type: string
|
||||||
|
required: [items]
|
||||||
|
UserResponse:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/UserListItem'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
policy:
|
||||||
|
$ref: '#/components/schemas/Policy'
|
||||||
|
secretKey:
|
||||||
|
type: string
|
||||||
|
description: Returned only on create.
|
||||||
53
docs/s3-compatibility.md
Normal file
53
docs/s3-compatibility.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# S3 Compatibility Matrix
|
||||||
|
|
||||||
|
This project is S3-compatible for a focused subset of operations.
|
||||||
|
|
||||||
|
## Implemented
|
||||||
|
|
||||||
|
### Service and account
|
||||||
|
- `GET /` list buckets
|
||||||
|
|
||||||
|
### Bucket
|
||||||
|
- `PUT /{bucket}` create bucket
|
||||||
|
- `HEAD /{bucket}` head bucket
|
||||||
|
- `DELETE /{bucket}` delete bucket (must be empty)
|
||||||
|
- `GET /{bucket}?list-type=2...` list objects v2
|
||||||
|
- `GET /{bucket}?location` get bucket location
|
||||||
|
- `POST /{bucket}?delete` delete multiple objects
|
||||||
|
|
||||||
|
### Object
|
||||||
|
- `PUT /{bucket}/{key}` put object
|
||||||
|
- `GET /{bucket}/{key}` get object
|
||||||
|
- `HEAD /{bucket}/{key}` head object
|
||||||
|
- `DELETE /{bucket}/{key}` delete object
|
||||||
|
- `GET /{bucket}/{key}` supports single-range requests
|
||||||
|
|
||||||
|
### Multipart upload
|
||||||
|
- `POST /{bucket}/{key}?uploads` initiate
|
||||||
|
- `PUT /{bucket}/{key}?uploadId=...&partNumber=N` upload part
|
||||||
|
- `GET /{bucket}/{key}?uploadId=...` list parts
|
||||||
|
- `POST /{bucket}/{key}?uploadId=...` complete
|
||||||
|
- `DELETE /{bucket}/{key}?uploadId=...` abort
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
- AWS SigV4 header auth
|
||||||
|
- AWS SigV4 presigned query auth
|
||||||
|
- `aws-chunked` payload decode for streaming uploads
|
||||||
|
|
||||||
|
## Partially Implemented / Differences
|
||||||
|
- Exact parity with AWS S3 error codes/headers is still evolving.
|
||||||
|
- Some S3 edge-case behaviors may differ (especially uncommon query/header combinations).
|
||||||
|
- Admin API is custom JSON (`/_admin/v1/*`).
|
||||||
|
|
||||||
|
## Not Implemented (Current)
|
||||||
|
- Bucket versioning
|
||||||
|
- Lifecycle rules
|
||||||
|
- Replication
|
||||||
|
- Object lock / legal hold / retention
|
||||||
|
- SSE-S3 / SSE-KMS / SSE-C
|
||||||
|
- ACL APIs and IAM-compatible policy APIs
|
||||||
|
- STS / temporary credentials
|
||||||
|
- Event notifications
|
||||||
|
- Tagging APIs
|
||||||
|
- CORS APIs
|
||||||
|
- Website hosting APIs
|
||||||
@@ -135,7 +135,7 @@ func (bs *BlobStore) saveBlob(chunkID string, data []byte) error {
|
|||||||
|
|
||||||
if err := os.Rename(tmpPath, fullPath); err != nil {
|
if err := os.Rename(tmpPath, fullPath); err != nil {
|
||||||
if _, statErr := os.Stat(fullPath); statErr == nil {
|
if _, statErr := os.Stat(fullPath); statErr == nil {
|
||||||
success = true
|
:q success = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|||||||
Reference in New Issue
Block a user