Change ARN generation to use resource OwnerAccountID instead of caller identity (h.getAccountID(r)). This ensures ARNs are stable and consistent regardless of which principal accesses the resource. Updated generateTableBucketARN and generateTableARN function signatures to accept ownerAccountID parameter. All call sites updated to pass the resource owner's account ID from metadata. This prevents ARN inconsistency issues when multiple principals have access to the same resource via policies.
239 lines
6.8 KiB
Go
239 lines
6.8 KiB
Go
package s3tables
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
)
|
|
|
|
const (
|
|
TablesPath = "/tables"
|
|
DefaultAccountID = "000000000000"
|
|
DefaultRegion = "us-east-1"
|
|
|
|
// Extended entry attributes for metadata storage
|
|
ExtendedKeyMetadata = "s3tables.metadata"
|
|
ExtendedKeyPolicy = "s3tables.policy"
|
|
ExtendedKeyTags = "s3tables.tags"
|
|
|
|
// Maximum request body size (10MB)
|
|
maxRequestBodySize = 10 * 1024 * 1024
|
|
)
|
|
|
|
var (
|
|
ErrVersionTokenMismatch = errors.New("version token mismatch")
|
|
ErrAccessDenied = errors.New("access denied")
|
|
)
|
|
|
|
type ResourceType string
|
|
|
|
const (
|
|
ResourceTypeBucket ResourceType = "bucket"
|
|
ResourceTypeTable ResourceType = "table"
|
|
)
|
|
|
|
// S3TablesHandler handles S3 Tables API requests
|
|
type S3TablesHandler struct {
|
|
region string
|
|
accountID string
|
|
}
|
|
|
|
// NewS3TablesHandler creates a new S3 Tables handler
|
|
func NewS3TablesHandler() *S3TablesHandler {
|
|
return &S3TablesHandler{
|
|
region: DefaultRegion,
|
|
accountID: DefaultAccountID,
|
|
}
|
|
}
|
|
|
|
// SetRegion sets the AWS region for ARN generation
|
|
func (h *S3TablesHandler) SetRegion(region string) {
|
|
if region != "" {
|
|
h.region = region
|
|
}
|
|
}
|
|
|
|
// SetAccountID sets the AWS account ID for ARN generation
|
|
func (h *S3TablesHandler) SetAccountID(accountID string) {
|
|
if accountID != "" {
|
|
h.accountID = accountID
|
|
}
|
|
}
|
|
|
|
// FilerClient interface for filer operations
|
|
type FilerClient interface {
|
|
WithFilerClient(streamingMode bool, fn func(client filer_pb.SeaweedFilerClient) error) error
|
|
}
|
|
|
|
// HandleRequest is the main entry point for S3 Tables API requests
|
|
func (h *S3TablesHandler) HandleRequest(w http.ResponseWriter, r *http.Request, filerClient FilerClient) {
|
|
// S3 Tables API uses x-amz-target header to specify the operation
|
|
target := r.Header.Get("X-Amz-Target")
|
|
if target == "" {
|
|
// Try to get from query parameter for CLI compatibility
|
|
target = r.URL.Query().Get("Action")
|
|
}
|
|
|
|
// Extract operation name (e.g., "S3Tables.CreateTableBucket" -> "CreateTableBucket")
|
|
operation := target
|
|
if idx := strings.LastIndex(target, "."); idx != -1 {
|
|
operation = target[idx+1:]
|
|
}
|
|
|
|
glog.V(3).Infof("S3Tables: handling operation %s", operation)
|
|
|
|
var err error
|
|
switch operation {
|
|
// Table Bucket operations
|
|
case "CreateTableBucket":
|
|
err = h.handleCreateTableBucket(w, r, filerClient)
|
|
case "GetTableBucket":
|
|
err = h.handleGetTableBucket(w, r, filerClient)
|
|
case "ListTableBuckets":
|
|
err = h.handleListTableBuckets(w, r, filerClient)
|
|
case "DeleteTableBucket":
|
|
err = h.handleDeleteTableBucket(w, r, filerClient)
|
|
|
|
// Table Bucket Policy operations
|
|
case "PutTableBucketPolicy":
|
|
err = h.handlePutTableBucketPolicy(w, r, filerClient)
|
|
case "GetTableBucketPolicy":
|
|
err = h.handleGetTableBucketPolicy(w, r, filerClient)
|
|
case "DeleteTableBucketPolicy":
|
|
err = h.handleDeleteTableBucketPolicy(w, r, filerClient)
|
|
|
|
// Namespace operations
|
|
case "CreateNamespace":
|
|
err = h.handleCreateNamespace(w, r, filerClient)
|
|
case "GetNamespace":
|
|
err = h.handleGetNamespace(w, r, filerClient)
|
|
case "ListNamespaces":
|
|
err = h.handleListNamespaces(w, r, filerClient)
|
|
case "DeleteNamespace":
|
|
err = h.handleDeleteNamespace(w, r, filerClient)
|
|
|
|
// Table operations
|
|
case "CreateTable":
|
|
err = h.handleCreateTable(w, r, filerClient)
|
|
case "GetTable":
|
|
err = h.handleGetTable(w, r, filerClient)
|
|
case "ListTables":
|
|
err = h.handleListTables(w, r, filerClient)
|
|
case "DeleteTable":
|
|
err = h.handleDeleteTable(w, r, filerClient)
|
|
|
|
// Table Policy operations
|
|
case "PutTablePolicy":
|
|
err = h.handlePutTablePolicy(w, r, filerClient)
|
|
case "GetTablePolicy":
|
|
err = h.handleGetTablePolicy(w, r, filerClient)
|
|
case "DeleteTablePolicy":
|
|
err = h.handleDeleteTablePolicy(w, r, filerClient)
|
|
|
|
// Tagging operations
|
|
case "TagResource":
|
|
err = h.handleTagResource(w, r, filerClient)
|
|
case "ListTagsForResource":
|
|
err = h.handleListTagsForResource(w, r, filerClient)
|
|
case "UntagResource":
|
|
err = h.handleUntagResource(w, r, filerClient)
|
|
|
|
default:
|
|
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, fmt.Sprintf("Unknown operation: %s", operation))
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
glog.Errorf("S3Tables: error handling %s: %v", operation, err)
|
|
}
|
|
}
|
|
|
|
// Principal/authorization helpers
|
|
|
|
// getAccountID returns the authenticated account ID from the request or the handler's default.
|
|
// This is also used as the principal for permission checks, ensuring alignment between
|
|
// the caller identity and ownership verification when IAM is enabled.
|
|
func (h *S3TablesHandler) getAccountID(r *http.Request) string {
|
|
if identityName := s3_constants.GetIdentityNameFromContext(r); identityName != "" {
|
|
return identityName
|
|
}
|
|
if accountID := r.Header.Get(s3_constants.AmzAccountId); accountID != "" {
|
|
return accountID
|
|
}
|
|
return h.accountID
|
|
}
|
|
|
|
// Request/Response helpers
|
|
|
|
func (h *S3TablesHandler) readRequestBody(r *http.Request, v interface{}) error {
|
|
defer r.Body.Close()
|
|
|
|
// Limit request body size to prevent unbounded reads
|
|
limitedReader := io.LimitReader(r.Body, maxRequestBodySize+1)
|
|
body, err := io.ReadAll(limitedReader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read request body: %w", err)
|
|
}
|
|
|
|
// Check if body exceeds size limit
|
|
if len(body) > maxRequestBodySize {
|
|
return fmt.Errorf("request body too large: exceeds maximum size of %d bytes", maxRequestBodySize)
|
|
}
|
|
|
|
if len(body) == 0 {
|
|
return nil
|
|
}
|
|
|
|
if err := json.Unmarshal(body, v); err != nil {
|
|
return fmt.Errorf("failed to decode request: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Response writing helpers
|
|
|
|
func (h *S3TablesHandler) writeJSON(w http.ResponseWriter, status int, data interface{}) {
|
|
w.Header().Set("Content-Type", "application/x-amz-json-1.1")
|
|
w.WriteHeader(status)
|
|
if data != nil {
|
|
if err := json.NewEncoder(w).Encode(data); err != nil {
|
|
glog.Errorf("S3Tables: failed to encode response: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (h *S3TablesHandler) writeError(w http.ResponseWriter, status int, code, message string) {
|
|
w.Header().Set("Content-Type", "application/x-amz-json-1.1")
|
|
w.WriteHeader(status)
|
|
errorResponse := map[string]interface{}{
|
|
"__type": code,
|
|
"message": message,
|
|
}
|
|
if err := json.NewEncoder(w).Encode(errorResponse); err != nil {
|
|
glog.Errorf("S3Tables: failed to encode error response: %v", err)
|
|
}
|
|
}
|
|
|
|
// ARN generation helpers
|
|
|
|
func (h *S3TablesHandler) generateTableBucketARN(ownerAccountID, bucketName string) string {
|
|
return fmt.Sprintf("arn:aws:s3tables:%s:%s:bucket/%s", h.region, ownerAccountID, bucketName)
|
|
}
|
|
|
|
func (h *S3TablesHandler) generateTableARN(ownerAccountID, bucketName, tableID string) string {
|
|
return fmt.Sprintf("arn:aws:s3tables:%s:%s:bucket/%s/table/%s", h.region, ownerAccountID, bucketName, tableID)
|
|
}
|
|
|
|
func isAuthError(err error) bool {
|
|
var authErr *AuthError
|
|
return errors.As(err, &authErr) || errors.Is(err, ErrAccessDenied)
|
|
}
|