Add s3tables shell and admin UI (#8172)
* Add shared s3tables manager * Add s3tables shell commands * Add s3tables admin API * Add s3tables admin UI * Fix admin s3tables namespace create * Rename table buckets menu * Centralize s3tables tag validation * Reuse s3tables manager in admin * Extract s3tables list limit * Add s3tables bucket ARN helper * Remove write middleware from s3tables APIs * Fix bucket link and policy hint * Fix table tag parsing and nav link * Disable namespace table link on invalid ARN * Improve s3tables error decode * Return flag parse errors for s3tables tag * Accept query params for namespace create * Bind namespace create form data * Read s3tables JS data from DOM * s3tables: allow empty region ARN * shell: pass s3tables account id * shell: require account for table buckets * shell: use bucket name for namespaces * shell: use bucket name for tables * shell: use bucket name for tags * admin: add table buckets links in file browser * s3api: reuse s3tables tag validation * admin: harden s3tables UI handlers * fix admin list table buckets * allow admin s3tables access * validate s3tables bucket tags * log s3tables bucket metadata errors * rollback table bucket on owner failure * show s3tables bucket owner * add s3tables iam conditions * Add s3tables user permissions UI * Authorize s3tables using identity actions * Add s3tables permissions to user modal * Disambiguate bucket scope in user permissions * Block table bucket names that match S3 buckets * Pretty-print IAM identity JSON * Include tags in s3tables permission context * admin: refactor S3 Tables inline JavaScript into a separate file * s3tables: extend IAM policy condition operators support * shell: use LookupEntry wrapper for s3tables bucket conflict check * admin: handle buildBucketPermissions validation in create/update flows
This commit is contained in:
@@ -1,11 +1,13 @@
|
||||
package s3tables
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
@@ -169,6 +171,44 @@ func (h *S3TablesHandler) getAccountID(r *http.Request) string {
|
||||
return h.accountID
|
||||
}
|
||||
|
||||
// getIdentityActions extracts the action list from the identity object in the request context.
|
||||
// Uses reflection to avoid import cycles with s3api package.
|
||||
func getIdentityActions(r *http.Request) []string {
|
||||
identityRaw := s3_constants.GetIdentityFromContext(r)
|
||||
if identityRaw == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use reflection to access the Actions field to avoid import cycle
|
||||
val := reflect.ValueOf(identityRaw)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
if val.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
|
||||
actionsField := val.FieldByName("Actions")
|
||||
if !actionsField.IsValid() || actionsField.Kind() != reflect.Slice {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert actions to string slice
|
||||
actions := make([]string, actionsField.Len())
|
||||
for i := 0; i < actionsField.Len(); i++ {
|
||||
action := actionsField.Index(i)
|
||||
// Action is likely a custom type (e.g., type Action string)
|
||||
// Convert to string using String() or direct string conversion
|
||||
if action.Kind() == reflect.String {
|
||||
actions[i] = action.String()
|
||||
} else if action.CanInterface() {
|
||||
// Try to convert via fmt.Sprint
|
||||
actions[i] = fmt.Sprint(action.Interface())
|
||||
}
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
// Request/Response helpers
|
||||
|
||||
func (h *S3TablesHandler) readRequestBody(r *http.Request, v interface{}) error {
|
||||
@@ -235,3 +275,29 @@ func isAuthError(err error) bool {
|
||||
var authErr *AuthError
|
||||
return errors.As(err, &authErr) || errors.Is(err, ErrAccessDenied)
|
||||
}
|
||||
|
||||
func (h *S3TablesHandler) readTags(ctx context.Context, client filer_pb.SeaweedFilerClient, path string) (map[string]string, error) {
|
||||
data, err := h.getExtendedAttribute(ctx, client, path, ExtendedKeyTags)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrAttributeNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
tags := make(map[string]string)
|
||||
if err := json.Unmarshal(data, &tags); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal tags: %w", err)
|
||||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func mapKeys(tags map[string]string) []string {
|
||||
if len(tags) == 0 {
|
||||
return nil
|
||||
}
|
||||
keys := make([]string, 0, len(tags))
|
||||
for key := range tags {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
)
|
||||
|
||||
// handleCreateTableBucket creates a new table bucket
|
||||
@@ -34,10 +35,30 @@ func (h *S3TablesHandler) handleCreateTableBucket(w http.ResponseWriter, r *http
|
||||
|
||||
bucketPath := getTableBucketPath(req.Name)
|
||||
|
||||
// Check if bucket already exists
|
||||
exists := false
|
||||
// Check if bucket already exists and ensure no conflict with object store buckets
|
||||
tableBucketExists := false
|
||||
s3BucketExists := false
|
||||
err := filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
_, err := filer_pb.LookupEntry(r.Context(), client, &filer_pb.LookupDirectoryEntryRequest{
|
||||
resp, err := client.GetFilerConfiguration(r.Context(), &filer_pb.GetFilerConfigurationRequest{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bucketsPath := resp.DirBuckets
|
||||
if bucketsPath == "" {
|
||||
bucketsPath = s3_constants.DefaultBucketsPath
|
||||
}
|
||||
_, err = filer_pb.LookupEntry(r.Context(), client, &filer_pb.LookupDirectoryEntryRequest{
|
||||
Directory: bucketsPath,
|
||||
Name: req.Name,
|
||||
})
|
||||
if err != nil {
|
||||
if !errors.Is(err, filer_pb.ErrNotFound) {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
s3BucketExists = true
|
||||
}
|
||||
_, err = filer_pb.LookupEntry(r.Context(), client, &filer_pb.LookupDirectoryEntryRequest{
|
||||
Directory: TablesPath,
|
||||
Name: req.Name,
|
||||
})
|
||||
@@ -47,7 +68,7 @@ func (h *S3TablesHandler) handleCreateTableBucket(w http.ResponseWriter, r *http
|
||||
}
|
||||
return err
|
||||
}
|
||||
exists = true
|
||||
tableBucketExists = true
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -57,7 +78,12 @@ func (h *S3TablesHandler) handleCreateTableBucket(w http.ResponseWriter, r *http
|
||||
return err
|
||||
}
|
||||
|
||||
if exists {
|
||||
if s3BucketExists {
|
||||
h.writeError(w, http.StatusConflict, ErrCodeBucketAlreadyExists, fmt.Sprintf("bucket name %s is already used by an object store bucket", req.Name))
|
||||
return fmt.Errorf("bucket name conflicts with object store bucket")
|
||||
}
|
||||
|
||||
if tableBucketExists {
|
||||
h.writeError(w, http.StatusConflict, ErrCodeBucketAlreadyExists, fmt.Sprintf("table bucket %s already exists", req.Name))
|
||||
return fmt.Errorf("bucket already exists")
|
||||
}
|
||||
|
||||
@@ -65,9 +65,13 @@ func (h *S3TablesHandler) handleGetTableBucket(w http.ResponseWriter, r *http.Re
|
||||
return err
|
||||
}
|
||||
|
||||
// Check permission
|
||||
bucketARN := h.generateTableBucketARN(metadata.OwnerAccountID, bucketName)
|
||||
principal := h.getAccountID(r)
|
||||
if !CanGetTableBucket(principal, metadata.OwnerAccountID, bucketPolicy) {
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("GetTableBucket", principal, metadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to get table bucket details")
|
||||
return ErrAccessDenied
|
||||
}
|
||||
@@ -91,10 +95,12 @@ func (h *S3TablesHandler) handleListTableBuckets(w http.ResponseWriter, r *http.
|
||||
return err
|
||||
}
|
||||
|
||||
// Check permission
|
||||
principal := h.getAccountID(r)
|
||||
accountID := h.getAccountID(r)
|
||||
if !CanListTableBuckets(principal, accountID, "") {
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("ListTableBuckets", principal, accountID, "", "", &PolicyContext{
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to list table buckets")
|
||||
return NewAuthError("ListTableBuckets", principal, "not authorized to list table buckets")
|
||||
}
|
||||
@@ -171,12 +177,28 @@ func (h *S3TablesHandler) handleListTableBuckets(w http.ResponseWriter, r *http.
|
||||
continue
|
||||
}
|
||||
|
||||
if metadata.OwnerAccountID != accountID {
|
||||
bucketPath := getTableBucketPath(entry.Entry.Name)
|
||||
bucketPolicy := ""
|
||||
policyData, err := h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyPolicy)
|
||||
if err != nil {
|
||||
if !errors.Is(err, ErrAttributeNotFound) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
bucketPolicy = string(policyData)
|
||||
}
|
||||
|
||||
bucketARN := h.generateTableBucketARN(metadata.OwnerAccountID, entry.Entry.Name)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("GetTableBucket", accountID, metadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TableBucketName: entry.Entry.Name,
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
continue
|
||||
}
|
||||
|
||||
buckets = append(buckets, TableBucketSummary{
|
||||
ARN: h.generateTableBucketARN(metadata.OwnerAccountID, entry.Entry.Name),
|
||||
ARN: bucketARN,
|
||||
Name: entry.Entry.Name,
|
||||
CreatedAt: metadata.CreatedAt,
|
||||
})
|
||||
@@ -267,9 +289,13 @@ func (h *S3TablesHandler) handleDeleteTableBucket(w http.ResponseWriter, r *http
|
||||
bucketPolicy = string(policyData)
|
||||
}
|
||||
|
||||
// 2. Check permission
|
||||
bucketARN := h.generateTableBucketARN(metadata.OwnerAccountID, bucketName)
|
||||
principal := h.getAccountID(r)
|
||||
if !CanDeleteTableBucket(principal, metadata.OwnerAccountID, bucketPolicy) {
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("DeleteTableBucket", principal, metadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
return NewAuthError("DeleteTableBucket", principal, fmt.Sprintf("not authorized to delete bucket %s", bucketName))
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ func (h *S3TablesHandler) handleCreateNamespace(w http.ResponseWriter, r *http.R
|
||||
bucketPath := getTableBucketPath(bucketName)
|
||||
var bucketMetadata tableBucketMetadata
|
||||
var bucketPolicy string
|
||||
var bucketTags map[string]string
|
||||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
data, err := h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyMetadata)
|
||||
if err != nil {
|
||||
@@ -62,6 +63,10 @@ func (h *S3TablesHandler) handleCreateNamespace(w http.ResponseWriter, r *http.R
|
||||
} else if !errors.Is(err, ErrAttributeNotFound) {
|
||||
return fmt.Errorf("failed to fetch bucket policy: %v", err)
|
||||
}
|
||||
bucketTags, err = h.readTags(r.Context(), client, bucketPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
@@ -75,9 +80,15 @@ func (h *S3TablesHandler) handleCreateNamespace(w http.ResponseWriter, r *http.R
|
||||
return err
|
||||
}
|
||||
|
||||
// Check permission
|
||||
bucketARN := h.generateTableBucketARN(bucketMetadata.OwnerAccountID, bucketName)
|
||||
principal := h.getAccountID(r)
|
||||
if !CanCreateNamespace(principal, bucketMetadata.OwnerAccountID, bucketPolicy) {
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("CreateNamespace", principal, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to create namespace in this bucket")
|
||||
return ErrAccessDenied
|
||||
}
|
||||
@@ -172,6 +183,7 @@ func (h *S3TablesHandler) handleGetNamespace(w http.ResponseWriter, r *http.Requ
|
||||
// Get namespace and bucket policy
|
||||
var metadata namespaceMetadata
|
||||
var bucketPolicy string
|
||||
var bucketTags map[string]string
|
||||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
data, err := h.getExtendedAttribute(r.Context(), client, namespacePath, ExtendedKeyMetadata)
|
||||
if err != nil {
|
||||
@@ -188,6 +200,10 @@ func (h *S3TablesHandler) handleGetNamespace(w http.ResponseWriter, r *http.Requ
|
||||
} else if !errors.Is(err, ErrAttributeNotFound) {
|
||||
return fmt.Errorf("failed to fetch bucket policy: %v", err)
|
||||
}
|
||||
bucketTags, err = h.readTags(r.Context(), client, bucketPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
@@ -201,9 +217,15 @@ func (h *S3TablesHandler) handleGetNamespace(w http.ResponseWriter, r *http.Requ
|
||||
return err
|
||||
}
|
||||
|
||||
// Check permission
|
||||
bucketARN := h.generateTableBucketARN(metadata.OwnerAccountID, bucketName)
|
||||
principal := h.getAccountID(r)
|
||||
if !CanGetNamespace(principal, metadata.OwnerAccountID, bucketPolicy) {
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("GetNamespace", principal, metadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchNamespace, "namespace not found")
|
||||
return ErrAccessDenied
|
||||
}
|
||||
@@ -247,6 +269,7 @@ func (h *S3TablesHandler) handleListNamespaces(w http.ResponseWriter, r *http.Re
|
||||
// Check permission (check bucket ownership)
|
||||
var bucketMetadata tableBucketMetadata
|
||||
var bucketPolicy string
|
||||
var bucketTags map[string]string
|
||||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
data, err := h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyMetadata)
|
||||
if err != nil {
|
||||
@@ -263,6 +286,10 @@ func (h *S3TablesHandler) handleListNamespaces(w http.ResponseWriter, r *http.Re
|
||||
} else if !errors.Is(err, ErrAttributeNotFound) {
|
||||
return fmt.Errorf("failed to fetch bucket policy: %v", err)
|
||||
}
|
||||
bucketTags, err = h.readTags(r.Context(), client, bucketPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
@@ -276,8 +303,14 @@ func (h *S3TablesHandler) handleListNamespaces(w http.ResponseWriter, r *http.Re
|
||||
return err
|
||||
}
|
||||
|
||||
bucketARN := h.generateTableBucketARN(bucketMetadata.OwnerAccountID, bucketName)
|
||||
principal := h.getAccountID(r)
|
||||
if !CanListNamespaces(principal, bucketMetadata.OwnerAccountID, bucketPolicy) {
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("ListNamespaces", principal, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchBucket, fmt.Sprintf("table bucket %s not found", bucketName))
|
||||
return ErrAccessDenied
|
||||
}
|
||||
@@ -419,6 +452,7 @@ func (h *S3TablesHandler) handleDeleteNamespace(w http.ResponseWriter, r *http.R
|
||||
// Check if namespace exists and get metadata for permission check
|
||||
var metadata namespaceMetadata
|
||||
var bucketPolicy string
|
||||
var bucketTags map[string]string
|
||||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
data, err := h.getExtendedAttribute(r.Context(), client, namespacePath, ExtendedKeyMetadata)
|
||||
if err != nil {
|
||||
@@ -435,6 +469,10 @@ func (h *S3TablesHandler) handleDeleteNamespace(w http.ResponseWriter, r *http.R
|
||||
} else if !errors.Is(err, ErrAttributeNotFound) {
|
||||
return fmt.Errorf("failed to fetch bucket policy: %v", err)
|
||||
}
|
||||
bucketTags, err = h.readTags(r.Context(), client, bucketPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
@@ -448,9 +486,15 @@ func (h *S3TablesHandler) handleDeleteNamespace(w http.ResponseWriter, r *http.R
|
||||
return err
|
||||
}
|
||||
|
||||
// Check permission
|
||||
bucketARN := h.generateTableBucketARN(metadata.OwnerAccountID, bucketName)
|
||||
principal := h.getAccountID(r)
|
||||
if !CanDeleteNamespace(principal, metadata.OwnerAccountID, bucketPolicy) {
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("DeleteNamespace", principal, metadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchNamespace, "namespace not found")
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
@@ -88,9 +88,13 @@ func (h *S3TablesHandler) handlePutTableBucketPolicy(w http.ResponseWriter, r *h
|
||||
return err
|
||||
}
|
||||
|
||||
// Check permission
|
||||
bucketARN := h.generateTableBucketARN(bucketMetadata.OwnerAccountID, bucketName)
|
||||
principal := h.getAccountID(r)
|
||||
if !CanPutTableBucketPolicy(principal, bucketMetadata.OwnerAccountID, "") {
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("PutTableBucketPolicy", principal, bucketMetadata.OwnerAccountID, "", bucketARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to put table bucket policy")
|
||||
return NewAuthError("PutTableBucketPolicy", principal, "not authorized to put table bucket policy")
|
||||
}
|
||||
@@ -161,9 +165,13 @@ func (h *S3TablesHandler) handleGetTableBucketPolicy(w http.ResponseWriter, r *h
|
||||
return err
|
||||
}
|
||||
|
||||
// Check permission
|
||||
bucketARN := h.generateTableBucketARN(bucketMetadata.OwnerAccountID, bucketName)
|
||||
principal := h.getAccountID(r)
|
||||
if !CanGetTableBucketPolicy(principal, bucketMetadata.OwnerAccountID, string(policy)) {
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("GetTableBucketPolicy", principal, bucketMetadata.OwnerAccountID, string(policy), bucketARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to get table bucket policy")
|
||||
return NewAuthError("GetTableBucketPolicy", principal, "not authorized to get table bucket policy")
|
||||
}
|
||||
@@ -232,9 +240,13 @@ func (h *S3TablesHandler) handleDeleteTableBucketPolicy(w http.ResponseWriter, r
|
||||
return err
|
||||
}
|
||||
|
||||
// Check permission
|
||||
bucketARN := h.generateTableBucketARN(bucketMetadata.OwnerAccountID, bucketName)
|
||||
principal := h.getAccountID(r)
|
||||
if !CanDeleteTableBucketPolicy(principal, bucketMetadata.OwnerAccountID, bucketPolicy) {
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("DeleteTableBucketPolicy", principal, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to delete table bucket policy")
|
||||
return NewAuthError("DeleteTableBucketPolicy", principal, "not authorized to delete table bucket policy")
|
||||
}
|
||||
@@ -326,9 +338,15 @@ func (h *S3TablesHandler) handlePutTablePolicy(w http.ResponseWriter, r *http.Re
|
||||
return err
|
||||
}
|
||||
|
||||
// Check permission
|
||||
tableARN := h.generateTableARN(metadata.OwnerAccountID, bucketName, namespaceName+"/"+tableName)
|
||||
principal := h.getAccountID(r)
|
||||
if !CanPutTablePolicy(principal, metadata.OwnerAccountID, bucketPolicy) {
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("PutTablePolicy", principal, metadata.OwnerAccountID, bucketPolicy, tableARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to put table policy")
|
||||
return NewAuthError("PutTablePolicy", principal, "not authorized to put table policy")
|
||||
}
|
||||
@@ -427,9 +445,15 @@ func (h *S3TablesHandler) handleGetTablePolicy(w http.ResponseWriter, r *http.Re
|
||||
return err
|
||||
}
|
||||
|
||||
// Check permission
|
||||
tableARN := h.generateTableARN(metadata.OwnerAccountID, bucketName, namespaceName+"/"+tableName)
|
||||
principal := h.getAccountID(r)
|
||||
if !CanGetTablePolicy(principal, metadata.OwnerAccountID, bucketPolicy) {
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("GetTablePolicy", principal, metadata.OwnerAccountID, bucketPolicy, tableARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to get table policy")
|
||||
return NewAuthError("GetTablePolicy", principal, "not authorized to get table policy")
|
||||
}
|
||||
@@ -510,9 +534,15 @@ func (h *S3TablesHandler) handleDeleteTablePolicy(w http.ResponseWriter, r *http
|
||||
return err
|
||||
}
|
||||
|
||||
// Check permission
|
||||
tableARN := h.generateTableARN(metadata.OwnerAccountID, bucketName, namespaceName+"/"+tableName)
|
||||
principal := h.getAccountID(r)
|
||||
if !CanDeleteTablePolicy(principal, metadata.OwnerAccountID, bucketPolicy) {
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("DeleteTablePolicy", principal, metadata.OwnerAccountID, bucketPolicy, tableARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to delete table policy")
|
||||
return NewAuthError("DeleteTablePolicy", principal, "not authorized to delete table policy")
|
||||
}
|
||||
@@ -558,6 +588,8 @@ func (h *S3TablesHandler) handleTagResource(w http.ResponseWriter, r *http.Reque
|
||||
// Read existing tags and merge, AND check permissions based on metadata ownership
|
||||
existingTags := make(map[string]string)
|
||||
var bucketPolicy string
|
||||
var bucketTags map[string]string
|
||||
requestTagKeys := mapKeys(req.Tags)
|
||||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
// Read metadata for ownership check
|
||||
data, err := h.getExtendedAttribute(r.Context(), client, resourcePath, ExtendedKeyMetadata)
|
||||
@@ -582,23 +614,36 @@ func (h *S3TablesHandler) handleTagResource(w http.ResponseWriter, r *http.Reque
|
||||
} else {
|
||||
bucketPolicy = string(policyData)
|
||||
}
|
||||
}
|
||||
|
||||
// Check Permission inside the closure because we just got the ID
|
||||
principal := h.getAccountID(r)
|
||||
if !CanManageTags(principal, ownerAccountID, bucketPolicy) {
|
||||
return NewAuthError("TagResource", principal, "not authorized to tag resource")
|
||||
bucketTags, err = h.readTags(r.Context(), client, bucketPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Read existing tags
|
||||
data, err = h.getExtendedAttribute(r.Context(), client, resourcePath, extendedKey)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrAttributeNotFound) {
|
||||
return nil // No existing tags, which is fine.
|
||||
if !errors.Is(err, ErrAttributeNotFound) {
|
||||
return err
|
||||
}
|
||||
return err // Propagate other errors.
|
||||
} else if err := json.Unmarshal(data, &existingTags); err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, &existingTags)
|
||||
|
||||
resourceARN := req.ResourceARN
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("TagResource", principal, ownerAccountID, bucketPolicy, resourceARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
TableBucketTags: bucketTags,
|
||||
RequestTags: req.Tags,
|
||||
TagKeys: requestTagKeys,
|
||||
ResourceTags: existingTags,
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
return NewAuthError("TagResource", principal, "not authorized to tag resource")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -662,6 +707,7 @@ func (h *S3TablesHandler) handleListTagsForResource(w http.ResponseWriter, r *ht
|
||||
|
||||
tags := make(map[string]string)
|
||||
var bucketPolicy string
|
||||
var bucketTags map[string]string
|
||||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
// Read metadata for ownership check
|
||||
data, err := h.getExtendedAttribute(r.Context(), client, resourcePath, ExtendedKeyMetadata)
|
||||
@@ -686,12 +732,10 @@ func (h *S3TablesHandler) handleListTagsForResource(w http.ResponseWriter, r *ht
|
||||
} else {
|
||||
bucketPolicy = string(policyData)
|
||||
}
|
||||
}
|
||||
|
||||
// Check Permission
|
||||
principal := h.getAccountID(r)
|
||||
if !CheckPermission("ListTagsForResource", principal, ownerAccountID, bucketPolicy) {
|
||||
return NewAuthError("ListTagsForResource", principal, "not authorized to list tags for resource")
|
||||
bucketTags, err = h.readTags(r.Context(), client, bucketPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
data, err = h.getExtendedAttribute(r.Context(), client, resourcePath, extendedKey)
|
||||
@@ -701,7 +745,22 @@ func (h *S3TablesHandler) handleListTagsForResource(w http.ResponseWriter, r *ht
|
||||
}
|
||||
return err // Propagate other errors.
|
||||
}
|
||||
return json.Unmarshal(data, &tags)
|
||||
if err := json.Unmarshal(data, &tags); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourceARN := req.ResourceARN
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("ListTagsForResource", principal, ownerAccountID, bucketPolicy, resourceARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
TableBucketTags: bucketTags,
|
||||
ResourceTags: tags,
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
return NewAuthError("ListTagsForResource", principal, "not authorized to list tags for resource")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
@@ -754,6 +813,7 @@ func (h *S3TablesHandler) handleUntagResource(w http.ResponseWriter, r *http.Req
|
||||
// Read existing tags, check permission
|
||||
tags := make(map[string]string)
|
||||
var bucketPolicy string
|
||||
var bucketTags map[string]string
|
||||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
// Read metadata for ownership check
|
||||
data, err := h.getExtendedAttribute(r.Context(), client, resourcePath, ExtendedKeyMetadata)
|
||||
@@ -778,12 +838,10 @@ func (h *S3TablesHandler) handleUntagResource(w http.ResponseWriter, r *http.Req
|
||||
} else {
|
||||
bucketPolicy = string(policyData)
|
||||
}
|
||||
}
|
||||
|
||||
// Check Permission
|
||||
principal := h.getAccountID(r)
|
||||
if !CanManageTags(principal, ownerAccountID, bucketPolicy) {
|
||||
return NewAuthError("UntagResource", principal, "not authorized to untag resource")
|
||||
bucketTags, err = h.readTags(r.Context(), client, bucketPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
data, err = h.getExtendedAttribute(r.Context(), client, resourcePath, extendedKey)
|
||||
@@ -793,7 +851,23 @@ func (h *S3TablesHandler) handleUntagResource(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, &tags)
|
||||
if err := json.Unmarshal(data, &tags); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resourceARN := req.ResourceARN
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("UntagResource", principal, ownerAccountID, bucketPolicy, resourceARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
TableBucketTags: bucketTags,
|
||||
TagKeys: req.TagKeys,
|
||||
ResourceTags: tags,
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
return NewAuthError("UntagResource", principal, "not authorized to untag resource")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -90,11 +90,13 @@ func (h *S3TablesHandler) handleCreateTable(w http.ResponseWriter, r *http.Reque
|
||||
bucketPath := getTableBucketPath(bucketName)
|
||||
namespacePolicy := ""
|
||||
bucketPolicy := ""
|
||||
bucketTags := map[string]string{}
|
||||
var data []byte
|
||||
var bucketMetadata tableBucketMetadata
|
||||
|
||||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
// Fetch bucket metadata to use correct owner for bucket policy evaluation
|
||||
data, err := h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyMetadata)
|
||||
data, err = h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyMetadata)
|
||||
if err == nil {
|
||||
if err := json.Unmarshal(data, &bucketMetadata); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal bucket metadata: %w", err)
|
||||
@@ -118,6 +120,11 @@ func (h *S3TablesHandler) handleCreateTable(w http.ResponseWriter, r *http.Reque
|
||||
} else if !errors.Is(err, ErrAttributeNotFound) {
|
||||
return fmt.Errorf("failed to fetch bucket policy: %v", err)
|
||||
}
|
||||
if tags, err := h.readTags(r.Context(), client, bucketPath); err != nil {
|
||||
return err
|
||||
} else if tags != nil {
|
||||
bucketTags = tags
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
@@ -127,11 +134,26 @@ func (h *S3TablesHandler) handleCreateTable(w http.ResponseWriter, r *http.Reque
|
||||
return err
|
||||
}
|
||||
|
||||
// Check authorization: namespace policy OR bucket policy OR ownership
|
||||
// Use namespace owner for namespace policy (consistent with namespace authorization)
|
||||
nsAllowed := CanCreateTable(accountID, namespaceMetadata.OwnerAccountID, namespacePolicy)
|
||||
// Use bucket owner for bucket policy (bucket policy applies to bucket-level operations)
|
||||
bucketAllowed := CanCreateTable(accountID, bucketMetadata.OwnerAccountID, bucketPolicy)
|
||||
bucketARN := h.generateTableBucketARN(bucketMetadata.OwnerAccountID, bucketName)
|
||||
identityActions := getIdentityActions(r)
|
||||
nsAllowed := CheckPermissionWithContext("CreateTable", accountID, namespaceMetadata.OwnerAccountID, namespacePolicy, bucketARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
RequestTags: req.Tags,
|
||||
TagKeys: mapKeys(req.Tags),
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
})
|
||||
bucketAllowed := CheckPermissionWithContext("CreateTable", accountID, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
RequestTags: req.Tags,
|
||||
TagKeys: mapKeys(req.Tags),
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
})
|
||||
|
||||
if !nsAllowed && !bucketAllowed {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to create table in this namespace")
|
||||
@@ -290,6 +312,8 @@ func (h *S3TablesHandler) handleGetTable(w http.ResponseWriter, r *http.Request,
|
||||
bucketPath := getTableBucketPath(bucketName)
|
||||
tablePolicy := ""
|
||||
bucketPolicy := ""
|
||||
bucketTags := map[string]string{}
|
||||
tableTags := map[string]string{}
|
||||
var bucketMetadata tableBucketMetadata
|
||||
|
||||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
@@ -310,6 +334,11 @@ func (h *S3TablesHandler) handleGetTable(w http.ResponseWriter, r *http.Request,
|
||||
} else if !errors.Is(err, ErrAttributeNotFound) {
|
||||
return fmt.Errorf("failed to fetch table policy: %v", err)
|
||||
}
|
||||
if tags, err := h.readTags(r.Context(), client, tablePath); err != nil {
|
||||
return err
|
||||
} else if tags != nil {
|
||||
tableTags = tags
|
||||
}
|
||||
|
||||
// Fetch bucket policy if it exists
|
||||
policyData, err = h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyPolicy)
|
||||
@@ -318,6 +347,11 @@ func (h *S3TablesHandler) handleGetTable(w http.ResponseWriter, r *http.Request,
|
||||
} else if !errors.Is(err, ErrAttributeNotFound) {
|
||||
return fmt.Errorf("failed to fetch bucket policy: %v", err)
|
||||
}
|
||||
if tags, err := h.readTags(r.Context(), client, bucketPath); err != nil {
|
||||
return err
|
||||
} else if tags != nil {
|
||||
bucketTags = tags
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
@@ -327,19 +361,31 @@ func (h *S3TablesHandler) handleGetTable(w http.ResponseWriter, r *http.Request,
|
||||
return err
|
||||
}
|
||||
|
||||
// Check authorization: table policy OR bucket policy OR ownership
|
||||
// Use table owner for table policy (table-level access control)
|
||||
tableAllowed := CanGetTable(accountID, metadata.OwnerAccountID, tablePolicy)
|
||||
// Use bucket owner for bucket policy (bucket-level access control)
|
||||
bucketAllowed := CanGetTable(accountID, bucketMetadata.OwnerAccountID, bucketPolicy)
|
||||
tableARN := h.generateTableARN(metadata.OwnerAccountID, bucketName, namespace+"/"+tableName)
|
||||
bucketARN := h.generateTableBucketARN(bucketMetadata.OwnerAccountID, bucketName)
|
||||
identityActions := getIdentityActions(r)
|
||||
tableAllowed := CheckPermissionWithContext("GetTable", accountID, metadata.OwnerAccountID, tablePolicy, tableARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespace,
|
||||
TableName: tableName,
|
||||
TableBucketTags: bucketTags,
|
||||
ResourceTags: tableTags,
|
||||
IdentityActions: identityActions,
|
||||
})
|
||||
bucketAllowed := CheckPermissionWithContext("GetTable", accountID, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespace,
|
||||
TableName: tableName,
|
||||
TableBucketTags: bucketTags,
|
||||
ResourceTags: tableTags,
|
||||
IdentityActions: identityActions,
|
||||
})
|
||||
|
||||
if !tableAllowed && !bucketAllowed {
|
||||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchTable, fmt.Sprintf("table %s not found", tableName))
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
tableARN := h.generateTableARN(metadata.OwnerAccountID, bucketName, namespace+"/"+tableName)
|
||||
|
||||
resp := &GetTableResponse{
|
||||
Name: metadata.Name,
|
||||
TableARN: tableARN,
|
||||
@@ -412,6 +458,7 @@ func (h *S3TablesHandler) handleListTables(w http.ResponseWriter, r *http.Reques
|
||||
var nsMeta namespaceMetadata
|
||||
var bucketMeta tableBucketMetadata
|
||||
var namespacePolicy, bucketPolicy string
|
||||
bucketTags := map[string]string{}
|
||||
|
||||
// Fetch namespace metadata and policy
|
||||
data, err := h.getExtendedAttribute(r.Context(), client, namespacePath, ExtendedKeyMetadata)
|
||||
@@ -446,10 +493,26 @@ func (h *S3TablesHandler) handleListTables(w http.ResponseWriter, r *http.Reques
|
||||
} else if !errors.Is(err, ErrAttributeNotFound) {
|
||||
return fmt.Errorf("failed to fetch bucket policy: %v", err)
|
||||
}
|
||||
if tags, err := h.readTags(r.Context(), client, bucketPath); err != nil {
|
||||
return err
|
||||
} else if tags != nil {
|
||||
bucketTags = tags
|
||||
}
|
||||
|
||||
// Authorize listing: namespace policy OR bucket policy OR ownership
|
||||
nsAllowed := CanListTables(accountID, nsMeta.OwnerAccountID, namespacePolicy)
|
||||
bucketAllowed := CanListTables(accountID, bucketMeta.OwnerAccountID, bucketPolicy)
|
||||
bucketARN := h.generateTableBucketARN(bucketMeta.OwnerAccountID, bucketName)
|
||||
identityActions := getIdentityActions(r)
|
||||
nsAllowed := CheckPermissionWithContext("ListTables", accountID, nsMeta.OwnerAccountID, namespacePolicy, bucketARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
})
|
||||
bucketAllowed := CheckPermissionWithContext("ListTables", accountID, bucketMeta.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
})
|
||||
if !nsAllowed && !bucketAllowed {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
@@ -460,6 +523,7 @@ func (h *S3TablesHandler) handleListTables(w http.ResponseWriter, r *http.Reques
|
||||
bucketPath := getTableBucketPath(bucketName)
|
||||
var bucketMeta tableBucketMetadata
|
||||
var bucketPolicy string
|
||||
bucketTags := map[string]string{}
|
||||
|
||||
// Fetch bucket metadata and policy
|
||||
data, err := h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyMetadata)
|
||||
@@ -477,9 +541,19 @@ func (h *S3TablesHandler) handleListTables(w http.ResponseWriter, r *http.Reques
|
||||
} else if !errors.Is(err, ErrAttributeNotFound) {
|
||||
return fmt.Errorf("failed to fetch bucket policy: %v", err)
|
||||
}
|
||||
if tags, err := h.readTags(r.Context(), client, bucketPath); err != nil {
|
||||
return err
|
||||
} else if tags != nil {
|
||||
bucketTags = tags
|
||||
}
|
||||
|
||||
// Authorize listing: bucket policy OR ownership
|
||||
if !CanListTables(accountID, bucketMeta.OwnerAccountID, bucketPolicy) {
|
||||
bucketARN := h.generateTableBucketARN(bucketMeta.OwnerAccountID, bucketName)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("ListTables", accountID, bucketMeta.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
}) {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
|
||||
@@ -731,6 +805,10 @@ func (h *S3TablesHandler) handleDeleteTable(w http.ResponseWriter, r *http.Reque
|
||||
// Check if table exists and enforce VersionToken if provided
|
||||
var metadata tableMetadataInternal
|
||||
var tablePolicy string
|
||||
var bucketPolicy string
|
||||
var bucketTags map[string]string
|
||||
var tableTags map[string]string
|
||||
var bucketMetadata tableBucketMetadata
|
||||
err = filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
data, err := h.getExtendedAttribute(r.Context(), client, tablePath, ExtendedKeyMetadata)
|
||||
if err != nil {
|
||||
@@ -759,6 +837,33 @@ func (h *S3TablesHandler) handleDeleteTable(w http.ResponseWriter, r *http.Reque
|
||||
tablePolicy = string(policyData)
|
||||
}
|
||||
|
||||
tableTags, err = h.readTags(r.Context(), client, tablePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bucketPath := getTableBucketPath(bucketName)
|
||||
data, err = h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyMetadata)
|
||||
if err == nil {
|
||||
if err := json.Unmarshal(data, &bucketMetadata); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal bucket metadata: %w", err)
|
||||
}
|
||||
} else if !errors.Is(err, ErrAttributeNotFound) {
|
||||
return fmt.Errorf("failed to fetch bucket metadata: %w", err)
|
||||
}
|
||||
policyData, err = h.getExtendedAttribute(r.Context(), client, bucketPath, ExtendedKeyPolicy)
|
||||
if err != nil {
|
||||
if !errors.Is(err, ErrAttributeNotFound) {
|
||||
return fmt.Errorf("failed to fetch bucket policy: %w", err)
|
||||
}
|
||||
} else {
|
||||
bucketPolicy = string(policyData)
|
||||
}
|
||||
bucketTags, err = h.readTags(r.Context(), client, bucketPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -773,9 +878,27 @@ func (h *S3TablesHandler) handleDeleteTable(w http.ResponseWriter, r *http.Reque
|
||||
return err
|
||||
}
|
||||
|
||||
// Check permission using table and bucket policies
|
||||
tableARN := h.generateTableARN(metadata.OwnerAccountID, bucketName, namespaceName+"/"+tableName)
|
||||
bucketARN := h.generateTableBucketARN(bucketMetadata.OwnerAccountID, bucketName)
|
||||
principal := h.getAccountID(r)
|
||||
if !CanDeleteTable(principal, metadata.OwnerAccountID, tablePolicy) {
|
||||
identityActions := getIdentityActions(r)
|
||||
tableAllowed := CheckPermissionWithContext("DeleteTable", principal, metadata.OwnerAccountID, tablePolicy, tableARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
TableBucketTags: bucketTags,
|
||||
ResourceTags: tableTags,
|
||||
IdentityActions: identityActions,
|
||||
})
|
||||
bucketAllowed := CheckPermissionWithContext("DeleteTable", principal, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
TableBucketTags: bucketTags,
|
||||
ResourceTags: tableTags,
|
||||
IdentityActions: identityActions,
|
||||
})
|
||||
if !tableAllowed && !bucketAllowed {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to delete table")
|
||||
return NewAuthError("DeleteTable", principal, "not authorized to delete table")
|
||||
}
|
||||
|
||||
98
weed/s3api/s3tables/manager.go
Normal file
98
weed/s3api/s3tables/manager.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package s3tables
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
)
|
||||
|
||||
// Manager provides reusable S3 Tables operations for shell/admin without HTTP routing.
|
||||
type Manager struct {
|
||||
handler *S3TablesHandler
|
||||
}
|
||||
|
||||
// NewManager creates a new Manager.
|
||||
func NewManager() *Manager {
|
||||
return &Manager{handler: NewS3TablesHandler()}
|
||||
}
|
||||
|
||||
// SetRegion sets the AWS region for ARN generation.
|
||||
func (m *Manager) SetRegion(region string) {
|
||||
m.handler.SetRegion(region)
|
||||
}
|
||||
|
||||
// SetAccountID sets the AWS account ID for ARN generation.
|
||||
func (m *Manager) SetAccountID(accountID string) {
|
||||
m.handler.SetAccountID(accountID)
|
||||
}
|
||||
|
||||
// Execute runs an S3 Tables operation and decodes the response into resp (if provided).
|
||||
func (m *Manager) Execute(ctx context.Context, filerClient FilerClient, operation string, req interface{}, resp interface{}, identity string) error {
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, "/", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/x-amz-json-1.1")
|
||||
httpReq.Header.Set("X-Amz-Target", "S3Tables."+operation)
|
||||
if identity != "" {
|
||||
httpReq.Header.Set(s3_constants.AmzAccountId, identity)
|
||||
httpReq = httpReq.WithContext(s3_constants.SetIdentityNameInContext(httpReq.Context(), identity))
|
||||
}
|
||||
recorder := httptest.NewRecorder()
|
||||
m.handler.HandleRequest(recorder, httpReq, filerClient)
|
||||
return decodeS3TablesHTTPResponse(recorder, resp)
|
||||
}
|
||||
|
||||
func decodeS3TablesHTTPResponse(recorder *httptest.ResponseRecorder, resp interface{}) error {
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
data, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if result.StatusCode >= http.StatusBadRequest {
|
||||
var errResp S3TablesError
|
||||
if len(data) > 0 {
|
||||
if jsonErr := json.Unmarshal(data, &errResp); jsonErr == nil && (errResp.Type != "" || errResp.Message != "") {
|
||||
return &errResp
|
||||
}
|
||||
}
|
||||
return &S3TablesError{Type: ErrCodeInternalError, Message: string(bytes.TrimSpace(data))}
|
||||
}
|
||||
if resp == nil || len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(data, resp); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ManagerClient adapts a SeaweedFilerClient to the FilerClient interface.
|
||||
type ManagerClient struct {
|
||||
client filer_pb.SeaweedFilerClient
|
||||
}
|
||||
|
||||
// NewManagerClient wraps a filer client.
|
||||
func NewManagerClient(client filer_pb.SeaweedFilerClient) *ManagerClient {
|
||||
return &ManagerClient{client: client}
|
||||
}
|
||||
|
||||
// WithFilerClient implements FilerClient.
|
||||
func (m *ManagerClient) WithFilerClient(streamingMode bool, fn func(client filer_pb.SeaweedFilerClient) error) error {
|
||||
if m.client == nil {
|
||||
return errors.New("nil filer client")
|
||||
}
|
||||
return fn(m.client)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||
)
|
||||
|
||||
// Permission represents a specific action permission
|
||||
@@ -65,24 +66,63 @@ func (pd *PolicyDocument) UnmarshalJSON(data []byte) error {
|
||||
}
|
||||
|
||||
type Statement struct {
|
||||
Effect string `json:"Effect"` // "Allow" or "Deny"
|
||||
Principal interface{} `json:"Principal"` // Can be string, []string, or map
|
||||
Action interface{} `json:"Action"` // Can be string or []string
|
||||
Resource interface{} `json:"Resource"` // Can be string or []string
|
||||
Effect string `json:"Effect"` // "Allow" or "Deny"
|
||||
Principal interface{} `json:"Principal"` // Can be string, []string, or map
|
||||
Action interface{} `json:"Action"` // Can be string or []string
|
||||
Resource interface{} `json:"Resource"` // Can be string or []string
|
||||
Condition map[string]map[string]interface{} `json:"Condition,omitempty"`
|
||||
}
|
||||
|
||||
type PolicyContext struct {
|
||||
Namespace string
|
||||
TableName string
|
||||
TableBucketName string
|
||||
IdentityActions []string
|
||||
RequestTags map[string]string
|
||||
ResourceTags map[string]string
|
||||
TableBucketTags map[string]string
|
||||
TagKeys []string
|
||||
SSEAlgorithm string
|
||||
KMSKeyArn string
|
||||
StorageClass string
|
||||
}
|
||||
|
||||
// CheckPermissionWithResource checks if a principal has permission to perform an operation on a specific resource
|
||||
func CheckPermissionWithResource(operation, principal, owner, resourcePolicy, resourceARN string) bool {
|
||||
return CheckPermissionWithContext(operation, principal, owner, resourcePolicy, resourceARN, nil)
|
||||
}
|
||||
|
||||
// CheckPermission checks if a principal has permission to perform an operation
|
||||
// (without resource-specific validation - for backward compatibility)
|
||||
func CheckPermission(operation, principal, owner, resourcePolicy string) bool {
|
||||
return CheckPermissionWithContext(operation, principal, owner, resourcePolicy, "", nil)
|
||||
}
|
||||
|
||||
// CheckPermissionWithContext checks permission with optional resource and condition context.
|
||||
func CheckPermissionWithContext(operation, principal, owner, resourcePolicy, resourceARN string, ctx *PolicyContext) bool {
|
||||
// Deny access if identities are empty
|
||||
if principal == "" || owner == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Admin always has permission.
|
||||
if principal == s3_constants.AccountAdminId {
|
||||
return true
|
||||
}
|
||||
|
||||
return checkPermission(operation, principal, owner, resourcePolicy, resourceARN, ctx)
|
||||
}
|
||||
|
||||
func checkPermission(operation, principal, owner, resourcePolicy, resourceARN string, ctx *PolicyContext) bool {
|
||||
// Owner always has permission
|
||||
if principal == owner {
|
||||
return true
|
||||
}
|
||||
|
||||
if hasIdentityPermission(operation, ctx) {
|
||||
return true
|
||||
}
|
||||
|
||||
// If no policy is provided, deny access (default deny)
|
||||
if resourcePolicy == "" {
|
||||
return false
|
||||
@@ -121,6 +161,10 @@ func CheckPermissionWithResource(operation, principal, owner, resourcePolicy, re
|
||||
continue
|
||||
}
|
||||
|
||||
if !matchesConditions(stmt.Condition, ctx) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Statement matches - check effect
|
||||
if stmt.Effect == "Allow" {
|
||||
hasAllow = true
|
||||
@@ -133,62 +177,29 @@ func CheckPermissionWithResource(operation, principal, owner, resourcePolicy, re
|
||||
return hasAllow
|
||||
}
|
||||
|
||||
// CheckPermission checks if a principal has permission to perform an operation
|
||||
// (without resource-specific validation - for backward compatibility)
|
||||
func CheckPermission(operation, principal, owner, resourcePolicy string) bool {
|
||||
// Deny access if identities are empty
|
||||
if principal == "" || owner == "" {
|
||||
func hasIdentityPermission(operation string, ctx *PolicyContext) bool {
|
||||
if ctx == nil || len(ctx.IdentityActions) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Owner always has permission
|
||||
if principal == owner {
|
||||
return true
|
||||
}
|
||||
|
||||
// If no policy is provided, deny access (default deny)
|
||||
if resourcePolicy == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// Normalize operation to full IAM-style action name (e.g., "s3tables:CreateTableBucket")
|
||||
// if not already prefixed
|
||||
fullAction := operation
|
||||
if !strings.Contains(operation, ":") {
|
||||
fullAction = "s3tables:" + operation
|
||||
}
|
||||
|
||||
// Parse and evaluate policy
|
||||
var policy PolicyDocument
|
||||
if err := json.Unmarshal([]byte(resourcePolicy), &policy); err != nil {
|
||||
return false
|
||||
candidates := []string{operation, fullAction}
|
||||
if ctx.TableBucketName != "" {
|
||||
candidates = append(candidates, operation+":"+ctx.TableBucketName, fullAction+":"+ctx.TableBucketName)
|
||||
}
|
||||
|
||||
// Evaluate policy statements
|
||||
// Default is deny, so we need an explicit allow
|
||||
hasAllow := false
|
||||
|
||||
for _, stmt := range policy.Statement {
|
||||
// Check if principal matches
|
||||
if !matchesPrincipal(stmt.Principal, principal) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if action matches (using normalized full action name)
|
||||
if !matchesAction(stmt.Action, fullAction) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Statement matches - check effect
|
||||
if stmt.Effect == "Allow" {
|
||||
hasAllow = true
|
||||
} else if stmt.Effect == "Deny" {
|
||||
// Explicit deny always wins
|
||||
return false
|
||||
for _, action := range ctx.IdentityActions {
|
||||
for _, candidate := range candidates {
|
||||
if action == candidate {
|
||||
return true
|
||||
}
|
||||
if strings.ContainsAny(action, "*?") && policy_engine.MatchesWildcard(action, candidate) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasAllow
|
||||
return false
|
||||
}
|
||||
|
||||
// matchesPrincipal checks if the principal matches the statement's principal
|
||||
@@ -271,6 +282,74 @@ func matchesActionPattern(pattern, action string) bool {
|
||||
return policy_engine.MatchesWildcard(pattern, action)
|
||||
}
|
||||
|
||||
func matchesConditions(conditions map[string]map[string]interface{}, ctx *PolicyContext) bool {
|
||||
if len(conditions) == 0 {
|
||||
return true
|
||||
}
|
||||
if ctx == nil {
|
||||
return false
|
||||
}
|
||||
for operator, conditionValues := range conditions {
|
||||
if !matchesConditionOperator(operator, conditionValues, ctx) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func matchesConditionOperator(operator string, conditionValues map[string]interface{}, ctx *PolicyContext) bool {
|
||||
evaluator, err := policy_engine.GetConditionEvaluator(operator)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for key, value := range conditionValues {
|
||||
contextVals := getConditionContextValues(key, ctx)
|
||||
if !evaluator.Evaluate(value, contextVals) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func getConditionContextValues(key string, ctx *PolicyContext) []string {
|
||||
switch key {
|
||||
case "s3tables:namespace":
|
||||
return []string{ctx.Namespace}
|
||||
case "s3tables:tableName":
|
||||
return []string{ctx.TableName}
|
||||
case "s3tables:tableBucketName":
|
||||
return []string{ctx.TableBucketName}
|
||||
case "s3tables:SSEAlgorithm":
|
||||
return []string{ctx.SSEAlgorithm}
|
||||
case "s3tables:KMSKeyArn":
|
||||
return []string{ctx.KMSKeyArn}
|
||||
case "s3tables:StorageClass":
|
||||
return []string{ctx.StorageClass}
|
||||
case "aws:TagKeys":
|
||||
return ctx.TagKeys
|
||||
}
|
||||
if strings.HasPrefix(key, "aws:RequestTag/") {
|
||||
tagKey := strings.TrimPrefix(key, "aws:RequestTag/")
|
||||
if val, ok := ctx.RequestTags[tagKey]; ok {
|
||||
return []string{val}
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(key, "aws:ResourceTag/") {
|
||||
tagKey := strings.TrimPrefix(key, "aws:ResourceTag/")
|
||||
if val, ok := ctx.ResourceTags[tagKey]; ok {
|
||||
return []string{val}
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(key, "s3tables:TableBucketTag/") {
|
||||
tagKey := strings.TrimPrefix(key, "s3tables:TableBucketTag/")
|
||||
if val, ok := ctx.TableBucketTags[tagKey]; ok {
|
||||
return []string{val}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// matchesResource checks if the resource ARN matches the statement's resource specification
|
||||
// Returns true if resource matches or if Resource is not specified (implicit match)
|
||||
func matchesResource(resourceSpec interface{}, resourceARN string) bool {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package s3tables
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMatchesActionPattern(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -88,3 +91,118 @@ func TestMatchesPrincipal(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluatePolicyWithConditions(t *testing.T) {
|
||||
policy := &PolicyDocument{
|
||||
Statement: []Statement{
|
||||
{
|
||||
Effect: "Allow",
|
||||
Principal: "*",
|
||||
Action: "s3tables:GetTable",
|
||||
Condition: map[string]map[string]interface{}{
|
||||
"StringEquals": {
|
||||
"s3tables:namespace": "default",
|
||||
},
|
||||
"StringLike": {
|
||||
"s3tables:tableName": "test_*",
|
||||
},
|
||||
"NumericGreaterThan": {
|
||||
"aws:RequestTag/priority": "10",
|
||||
},
|
||||
"Bool": {
|
||||
"aws:ResourceTag/is_public": "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
policyBytes, _ := json.Marshal(policy)
|
||||
policyStr := string(policyBytes)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx *PolicyContext
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
"all conditions match",
|
||||
&PolicyContext{
|
||||
Namespace: "default",
|
||||
TableName: "test_table",
|
||||
RequestTags: map[string]string{
|
||||
"priority": "15",
|
||||
},
|
||||
ResourceTags: map[string]string{
|
||||
"is_public": "true",
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"namespace mismatch",
|
||||
&PolicyContext{
|
||||
Namespace: "other",
|
||||
TableName: "test_table",
|
||||
RequestTags: map[string]string{
|
||||
"priority": "15",
|
||||
},
|
||||
ResourceTags: map[string]string{
|
||||
"is_public": "true",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"table name mismatch",
|
||||
&PolicyContext{
|
||||
Namespace: "default",
|
||||
TableName: "other_table",
|
||||
RequestTags: map[string]string{
|
||||
"priority": "15",
|
||||
},
|
||||
ResourceTags: map[string]string{
|
||||
"is_public": "true",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"numeric condition failure",
|
||||
&PolicyContext{
|
||||
Namespace: "default",
|
||||
TableName: "test_table",
|
||||
RequestTags: map[string]string{
|
||||
"priority": "5",
|
||||
},
|
||||
ResourceTags: map[string]string{
|
||||
"is_public": "true",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"bool condition failure",
|
||||
&PolicyContext{
|
||||
Namespace: "default",
|
||||
TableName: "test_table",
|
||||
RequestTags: map[string]string{
|
||||
"priority": "15",
|
||||
},
|
||||
ResourceTags: map[string]string{
|
||||
"is_public": "false",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// principal="user123", owner="owner123"
|
||||
result := CheckPermissionWithContext("s3tables:GetTable", "user123", "owner123", policyStr, "", tt.ctx)
|
||||
if result != tt.expected {
|
||||
t.Errorf("CheckPermissionWithContext() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ const (
|
||||
var (
|
||||
bucketARNPattern = regexp.MustCompile(`^arn:aws:s3tables:[^:]*:[^:]*:bucket/(` + bucketNamePatternStr + `)$`)
|
||||
tableARNPattern = regexp.MustCompile(`^arn:aws:s3tables:[^:]*:[^:]*:bucket/(` + bucketNamePatternStr + `)/table/(` + tableNamespacePatternStr + `)/(` + tableNamePatternStr + `)$`)
|
||||
tagPattern = regexp.MustCompile(`^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$`)
|
||||
)
|
||||
|
||||
// ARN parsing functions
|
||||
@@ -175,6 +176,80 @@ func validateBucketName(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateBucketName validates bucket name and returns an error if invalid.
|
||||
func ValidateBucketName(name string) error {
|
||||
return validateBucketName(name)
|
||||
}
|
||||
|
||||
// BuildBucketARN builds a bucket ARN with the provided region and account ID.
|
||||
// If region is empty, the ARN will omit the region field.
|
||||
func BuildBucketARN(region, accountID, bucketName string) (string, error) {
|
||||
if bucketName == "" {
|
||||
return "", fmt.Errorf("bucket name is required")
|
||||
}
|
||||
if err := validateBucketName(bucketName); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if accountID == "" {
|
||||
accountID = DefaultAccountID
|
||||
}
|
||||
return buildARN(region, accountID, fmt.Sprintf("bucket/%s", bucketName)), nil
|
||||
}
|
||||
|
||||
// BuildTableARN builds a table ARN with the provided region and account ID.
|
||||
func BuildTableARN(region, accountID, bucketName, namespace, tableName string) (string, error) {
|
||||
if bucketName == "" {
|
||||
return "", fmt.Errorf("bucket name is required")
|
||||
}
|
||||
if err := validateBucketName(bucketName); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if namespace == "" {
|
||||
return "", fmt.Errorf("namespace is required")
|
||||
}
|
||||
normalizedNamespace, err := validateNamespace([]string{namespace})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if tableName == "" {
|
||||
return "", fmt.Errorf("table name is required")
|
||||
}
|
||||
normalizedTable, err := validateTableName(tableName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if accountID == "" {
|
||||
accountID = DefaultAccountID
|
||||
}
|
||||
return buildARN(region, accountID, fmt.Sprintf("bucket/%s/table/%s/%s", bucketName, normalizedNamespace, normalizedTable)), nil
|
||||
}
|
||||
|
||||
func buildARN(region, accountID, resourcePath string) string {
|
||||
return fmt.Sprintf("arn:aws:s3tables:%s:%s:%s", region, accountID, resourcePath)
|
||||
}
|
||||
|
||||
// ValidateTags validates tags for S3 Tables.
|
||||
func ValidateTags(tags map[string]string) error {
|
||||
if len(tags) > 10 {
|
||||
return fmt.Errorf("validate tags: %d tags more than 10", len(tags))
|
||||
}
|
||||
for k, v := range tags {
|
||||
if len(k) > 128 {
|
||||
return fmt.Errorf("validate tags: tag key longer than 128")
|
||||
}
|
||||
if !tagPattern.MatchString(k) {
|
||||
return fmt.Errorf("validate tags key %s error, incorrect key", k)
|
||||
}
|
||||
if len(v) > 256 {
|
||||
return fmt.Errorf("validate tags: tag value longer than 256")
|
||||
}
|
||||
if !tagPattern.MatchString(v) {
|
||||
return fmt.Errorf("validate tags value %s error, incorrect value", v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidBucketName validates bucket name characters (kept for compatibility)
|
||||
// Deprecated: use validateBucketName instead
|
||||
func isValidBucketName(name string) bool {
|
||||
|
||||
@@ -4,10 +4,10 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3tables"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
)
|
||||
|
||||
@@ -78,25 +78,5 @@ func parseTagsHeader(tags string) (map[string]string, error) {
|
||||
}
|
||||
|
||||
func ValidateTags(tags map[string]string) error {
|
||||
if len(tags) > 10 {
|
||||
return fmt.Errorf("validate tags: %d tags more than 10", len(tags))
|
||||
}
|
||||
for k, v := range tags {
|
||||
if len(k) > 128 {
|
||||
return fmt.Errorf("validate tags: tag key longer than 128")
|
||||
}
|
||||
validateKey, err := regexp.MatchString(`^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$`, k)
|
||||
if !validateKey || err != nil {
|
||||
return fmt.Errorf("validate tags key %s error, incorrect key", k)
|
||||
}
|
||||
if len(v) > 256 {
|
||||
return fmt.Errorf("validate tags: tag value longer than 256")
|
||||
}
|
||||
validateValue, err := regexp.MatchString(`^([\p{L}\p{Z}\p{N}_.:/=+\-@]*)$`, v)
|
||||
if !validateValue || err != nil {
|
||||
return fmt.Errorf("validate tags value %s error, incorrect value", v)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return s3tables.ValidateTags(tags)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user