mirror of
https://github.com/ferdzo/fs.git
synced 2026-04-04 17:16:26 +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`
|
||||
|
||||
Additional docs:
|
||||
- Admin OpenAPI spec: `docs/admin-api-openapi.yaml`
|
||||
- S3 compatibility matrix: `docs/s3-compatibility.md`
|
||||
|
||||
Health:
|
||||
- `GET /healthz`
|
||||
- `HEAD /healthz`
|
||||
|
||||
@@ -178,8 +178,6 @@ func (s *Service) AuthenticateRequest(r *http.Request) (RequestContext, error) {
|
||||
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/") {
|
||||
return RequestContext{
|
||||
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 _, statErr := os.Stat(fullPath); statErr == nil {
|
||||
success = true
|
||||
:q success = true
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user