s3tables: Fix ListTables authorization and policy parsing

Make ListTables authorization consistent with GetTable/CreateTable:

1. ListTables authorization now evaluates policies instead of owner-only checks:
   - For namespace listing: checks namespace policy AND bucket policy
   - For bucket-wide listing: checks bucket policy
   - Uses CanListTables permission framework

2. Remove owner-only filter in listTablesWithClient that prevented policy-based
   sharing of tables. Authorization is now enforced at the handler level, so all
   tables in the namespace/bucket are returned to authorized callers (who have
   access either via ownership or policy).

3. Add flexible PolicyDocument.UnmarshalJSON to support both single-object and
   array forms of Statement field:
   - Handles: {"Statement": {...}}
   - Handles: {"Statement": [{...}, {...}]}
   - Improves AWS IAM compatibility

This ensures cross-account table listing works when delegated via bucket/namespace
policies, consistent with the authorization model for other operations.
This commit is contained in:
Chris Lu
2026-01-28 18:27:37 -08:00
parent 25b0f86bda
commit f5d26b803b
2 changed files with 103 additions and 6 deletions

View File

@@ -2,6 +2,7 @@ package s3tables
import (
"encoding/json"
"fmt"
"strings"
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
@@ -16,6 +17,53 @@ type PolicyDocument struct {
Statement []Statement `json:"Statement"`
}
// UnmarshalJSON handles both single statement object and array of statements
// AWS allows {"Statement": {...}} or {"Statement": [{...}]}
func (pd *PolicyDocument) UnmarshalJSON(data []byte) error {
type Alias PolicyDocument
aux := &struct {
Statement interface{} `json:"Statement"`
*Alias
}{
Alias: (*Alias)(pd),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
// Handle Statement as either a single object or array
switch s := aux.Statement.(type) {
case map[string]interface{}:
// Single statement object - unmarshal to one Statement
stmtData, err := json.Marshal(s)
if err != nil {
return fmt.Errorf("failed to marshal single statement: %w", err)
}
var stmt Statement
if err := json.Unmarshal(stmtData, &stmt); err != nil {
return fmt.Errorf("failed to unmarshal single statement: %w", err)
}
pd.Statement = []Statement{stmt}
case []interface{}:
// Array of statements - normal handling
stmtData, err := json.Marshal(s)
if err != nil {
return fmt.Errorf("failed to marshal statement array: %w", err)
}
if err := json.Unmarshal(stmtData, &pd.Statement); err != nil {
return fmt.Errorf("failed to unmarshal statement array: %w", err)
}
case nil:
// No statements
pd.Statement = []Statement{}
default:
return fmt.Errorf("Statement must be an object or array, got %T", aux.Statement)
}
return nil
}
type Statement struct {
Effect string `json:"Effect"` // "Allow" or "Deny"
Principal interface{} `json:"Principal"` // Can be string, []string, or map