Browse Source

Enforce IAM for s3tables bucket creation

pull/8388/head
Chris Lu 2 days ago
parent
commit
966ada47ad
  1. 8
      weed/s3api/s3api_tables.go
  2. 7
      weed/s3api/s3tables/handler.go
  3. 34
      weed/s3api/s3tables/handler_bucket_create.go
  4. 209
      weed/s3api/s3tables/iam.go

8
weed/s3api/s3api_tables.go

@ -48,6 +48,11 @@ func (st *S3TablesApiServer) SetDefaultAllow(allow bool) {
st.handler.SetDefaultAllow(allow) st.handler.SetDefaultAllow(allow)
} }
// SetIAMAuthorizer injects the IAM authorizer for S3 Tables IAM checks.
func (st *S3TablesApiServer) SetIAMAuthorizer(authorizer s3tables.IAMAuthorizer) {
st.handler.SetIAMAuthorizer(authorizer)
}
// S3TablesHandler handles S3 Tables API requests // S3TablesHandler handles S3 Tables API requests
func (st *S3TablesApiServer) S3TablesHandler(w http.ResponseWriter, r *http.Request) { func (st *S3TablesApiServer) S3TablesHandler(w http.ResponseWriter, r *http.Request) {
st.handler.HandleRequest(w, r, st) st.handler.HandleRequest(w, r, st)
@ -64,6 +69,9 @@ func (s3a *S3ApiServer) registerS3TablesRoutes(router *mux.Router) {
s3TablesApi := NewS3TablesApiServer(s3a) s3TablesApi := NewS3TablesApiServer(s3a)
if s3a.iam != nil && s3a.iam.iamIntegration != nil { if s3a.iam != nil && s3a.iam.iamIntegration != nil {
s3TablesApi.SetDefaultAllow(s3a.iam.iamIntegration.DefaultAllow()) s3TablesApi.SetDefaultAllow(s3a.iam.iamIntegration.DefaultAllow())
if s3Integration, ok := s3a.iam.iamIntegration.(*S3IAMIntegration); ok && s3Integration.iamManager != nil {
s3TablesApi.SetIAMAuthorizer(s3Integration.iamManager)
}
} else { } else {
// If IAM is not configured, allow all access by default // If IAM is not configured, allow all access by default
s3TablesApi.SetDefaultAllow(true) s3TablesApi.SetDefaultAllow(true)

7
weed/s3api/s3tables/handler.go

@ -44,9 +44,10 @@ const (
// S3TablesHandler handles S3 Tables API requests // S3TablesHandler handles S3 Tables API requests
type S3TablesHandler struct { type S3TablesHandler struct {
region string
accountID string
defaultAllow bool // Whether to allow access by default (for zero-config IAM)
region string
accountID string
defaultAllow bool // Whether to allow access by default (for zero-config IAM)
iamAuthorizer IAMAuthorizer
} }
// NewS3TablesHandler creates a new S3 Tables handler // NewS3TablesHandler creates a new S3 Tables handler

34
weed/s3api/s3tables/handler_bucket_create.go

@ -14,15 +14,6 @@ import (
// handleCreateTableBucket creates a new table bucket // handleCreateTableBucket creates a new table bucket
func (h *S3TablesHandler) handleCreateTableBucket(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error { func (h *S3TablesHandler) handleCreateTableBucket(w http.ResponseWriter, r *http.Request, filerClient FilerClient) error {
// Check permission
principal := h.getAccountID(r)
if !CheckPermissionWithContext("CreateTableBucket", principal, principal, "", "", &PolicyContext{
DefaultAllow: h.defaultAllow,
}) {
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to create table buckets")
return NewAuthError("CreateTableBucket", principal, "not authorized to create table buckets")
}
var req CreateTableBucketRequest var req CreateTableBucketRequest
if err := h.readRequestBody(r, &req); err != nil { if err := h.readRequestBody(r, &req); err != nil {
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error()) h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
@ -35,6 +26,31 @@ func (h *S3TablesHandler) handleCreateTableBucket(w http.ResponseWriter, r *http
return err return err
} }
principal := h.getAccountID(r)
identityActions := getIdentityActions(r)
if h.shouldUseIAM(r, identityActions) && !h.defaultAllow {
ownerAccountID := h.getAccountID(r)
tableBucketARN := h.generateTableBucketARN(ownerAccountID, req.Name)
s3BucketARN := fmt.Sprintf("arn:aws:s3:::%s", req.Name)
allowed, err := h.authorizeIAMAction(r, "s3tables:CreateTableBucket", tableBucketARN, s3BucketARN)
if err != nil || !allowed {
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to create table buckets")
return NewAuthError("CreateTableBucket", principal, "not authorized to create table buckets")
}
} else {
owner := h.accountID
if owner == "" {
owner = DefaultAccountID
}
if !CheckPermissionWithContext("CreateTableBucket", principal, owner, "", "", &PolicyContext{
IdentityActions: identityActions,
DefaultAllow: h.defaultAllow,
}) {
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to create table buckets")
return NewAuthError("CreateTableBucket", principal, "not authorized to create table buckets")
}
}
bucketPath := GetTableBucketPath(req.Name) bucketPath := GetTableBucketPath(req.Name)
// Check if bucket already exists and ensure no conflict with object store buckets // Check if bucket already exists and ensure no conflict with object store buckets

209
weed/s3api/s3tables/iam.go

@ -0,0 +1,209 @@
package s3tables
import (
"context"
"fmt"
"net/http"
"reflect"
"strings"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/iam/integration"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
)
// IAMAuthorizer allows s3tables handlers to evaluate IAM policies without importing s3api.
type IAMAuthorizer interface {
IsActionAllowed(ctx context.Context, request *integration.ActionRequest) (bool, error)
}
// SetIAMAuthorizer injects the IAM authorizer for policy-based access checks.
func (h *S3TablesHandler) SetIAMAuthorizer(authorizer IAMAuthorizer) {
h.iamAuthorizer = authorizer
}
func (h *S3TablesHandler) shouldUseIAM(r *http.Request, identityActions []string) bool {
if h.iamAuthorizer == nil || r == nil {
return false
}
if hasSessionToken(r) {
return true
}
return len(identityActions) == 0
}
func hasSessionToken(r *http.Request) bool {
if r.Header.Get("X-SeaweedFS-Session-Token") != "" {
return true
}
if r.Header.Get("X-Amz-Security-Token") != "" {
return true
}
return r.URL.Query().Get("X-Amz-Security-Token") != ""
}
func (h *S3TablesHandler) authorizeIAMAction(r *http.Request, action string, resources ...string) (bool, error) {
if h.iamAuthorizer == nil {
return false, nil
}
principal := r.Header.Get("X-SeaweedFS-Principal")
if principal == "" {
principal = getIdentityPrincipalArn(r)
}
if principal == "" {
return false, fmt.Errorf("missing principal for IAM authorization")
}
if !strings.Contains(action, ":") {
action = "s3tables:" + action
}
sessionToken := r.Header.Get("X-SeaweedFS-Session-Token")
if sessionToken == "" {
sessionToken = r.Header.Get("X-Amz-Security-Token")
if sessionToken == "" {
sessionToken = r.URL.Query().Get("X-Amz-Security-Token")
}
}
requestContext := buildIAMRequestContext(r, getIdentityClaims(r))
policyNames := getIdentityPolicyNames(r)
var lastErr error
for _, resource := range resources {
if resource == "" {
continue
}
allowed, err := h.iamAuthorizer.IsActionAllowed(r.Context(), &integration.ActionRequest{
Principal: principal,
Action: action,
Resource: resource,
SessionToken: sessionToken,
RequestContext: requestContext,
PolicyNames: policyNames,
})
if err != nil {
lastErr = err
glog.V(2).Infof("S3Tables: IAM authorization error action=%s resource=%s principal=%s: %v", action, resource, principal, err)
continue
}
if allowed {
return true, nil
}
}
return false, lastErr
}
func getIdentityPrincipalArn(r *http.Request) string {
identityRaw := s3_constants.GetIdentityFromContext(r)
if identityRaw == nil {
return ""
}
val := reflect.ValueOf(identityRaw)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return ""
}
field := val.FieldByName("PrincipalArn")
if field.IsValid() && field.Kind() == reflect.String {
return field.String()
}
return ""
}
func getIdentityPolicyNames(r *http.Request) []string {
identityRaw := s3_constants.GetIdentityFromContext(r)
if identityRaw == nil {
return nil
}
val := reflect.ValueOf(identityRaw)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return nil
}
field := val.FieldByName("PolicyNames")
if !field.IsValid() || field.Kind() != reflect.Slice {
return nil
}
policies := make([]string, 0, field.Len())
for i := 0; i < field.Len(); i++ {
item := field.Index(i)
if item.Kind() == reflect.String {
policies = append(policies, item.String())
} else if item.CanInterface() {
policies = append(policies, fmt.Sprint(item.Interface()))
}
}
if len(policies) == 0 {
return nil
}
return policies
}
func getIdentityClaims(r *http.Request) map[string]interface{} {
identityRaw := s3_constants.GetIdentityFromContext(r)
if identityRaw == nil {
return nil
}
val := reflect.ValueOf(identityRaw)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return nil
}
field := val.FieldByName("Claims")
if !field.IsValid() || field.Kind() != reflect.Map || field.IsNil() {
return nil
}
if field.Type().Key().Kind() != reflect.String {
return nil
}
claims := make(map[string]interface{}, field.Len())
for _, key := range field.MapKeys() {
if key.Kind() != reflect.String {
continue
}
val := field.MapIndex(key)
if !val.IsValid() {
continue
}
claims[key.String()] = val.Interface()
}
if len(claims) == 0 {
return nil
}
return claims
}
func buildIAMRequestContext(r *http.Request, claims map[string]interface{}) map[string]interface{} {
ctx := make(map[string]interface{})
if ua := r.Header.Get("User-Agent"); ua != "" {
ctx["userAgent"] = ua
}
if referer := r.Header.Get("Referer"); referer != "" {
ctx["referer"] = referer
}
if requestTime := r.Context().Value("requestTime"); requestTime != nil {
ctx["requestTime"] = requestTime
}
for k, v := range claims {
if _, exists := ctx[k]; !exists {
ctx[k] = v
}
if !strings.Contains(k, ":") {
jwtKey := "jwt:" + k
if _, exists := ctx[jwtKey]; !exists {
ctx[jwtKey] = v
}
}
}
if len(ctx) == 0 {
return nil
}
return ctx
}
Loading…
Cancel
Save