mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-17 20:57:27 +00:00
Enforce IAM for s3tables bucket creation
This commit is contained in:
@@ -48,6 +48,11 @@ func (st *S3TablesApiServer) SetDefaultAllow(allow bool) {
|
||||
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
|
||||
func (st *S3TablesApiServer) S3TablesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
st.handler.HandleRequest(w, r, st)
|
||||
@@ -64,6 +69,9 @@ func (s3a *S3ApiServer) registerS3TablesRoutes(router *mux.Router) {
|
||||
s3TablesApi := NewS3TablesApiServer(s3a)
|
||||
if s3a.iam != nil && s3a.iam.iamIntegration != nil {
|
||||
s3TablesApi.SetDefaultAllow(s3a.iam.iamIntegration.DefaultAllow())
|
||||
if s3Integration, ok := s3a.iam.iamIntegration.(*S3IAMIntegration); ok && s3Integration.iamManager != nil {
|
||||
s3TablesApi.SetIAMAuthorizer(s3Integration.iamManager)
|
||||
}
|
||||
} else {
|
||||
// If IAM is not configured, allow all access by default
|
||||
s3TablesApi.SetDefaultAllow(true)
|
||||
|
||||
@@ -44,9 +44,10 @@ const (
|
||||
|
||||
// S3TablesHandler handles S3 Tables API requests
|
||||
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
|
||||
|
||||
@@ -14,15 +14,6 @@ import (
|
||||
|
||||
// handleCreateTableBucket creates a new table bucket
|
||||
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
|
||||
if err := h.readRequestBody(r, &req); err != nil {
|
||||
h.writeError(w, http.StatusBadRequest, ErrCodeInvalidRequest, err.Error())
|
||||
@@ -35,6 +26,31 @@ func (h *S3TablesHandler) handleCreateTableBucket(w http.ResponseWriter, r *http
|
||||
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)
|
||||
|
||||
// Check if bucket already exists and ensure no conflict with object store buckets
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user