Browse Source

s3api: fix AccessDenied by correctly propagating principal ARN in vended tokens (#8330)

* s3api: fix AccessDenied by correctly propagating principal ARN in vended tokens

* s3api: update TestLoadS3ApiConfiguration to match standardized ARN format

* s3api: address PR review comments (nil-safety and cleanup)

* s3api: address second round of PR review comments (cleanups and naming conventions)

* s3api: address third round of PR review comments (unify default account ID and duplicate log)

* s3api: address fourth round of PR review comments (define defaultAccountID as constant)
pull/8339/head
Chris Lu 1 week ago
committed by GitHub
parent
commit
c433fee36a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 26
      weed/s3api/auth_credentials.go
  2. 7
      weed/s3api/auth_credentials_test.go
  3. 11
      weed/s3api/s3api_bucket_policy_arn_test.go
  4. 44
      weed/s3api/s3api_sts.go

26
weed/s3api/auth_credentials.go

@ -100,6 +100,9 @@ type Account struct {
Id string Id string
} }
// Default account ID for all automated SeaweedFS accounts and fallback
const defaultAccountID = "000000000000"
// Predefined Accounts // Predefined Accounts
var ( var (
// AccountAdmin is used as the default account for IAM-Credentials access without Account configured // AccountAdmin is used as the default account for IAM-Credentials access without Account configured
@ -809,7 +812,6 @@ func (iam *IdentityAccessManagement) MergeS3ApiConfiguration(config *iam_pb.S3Ap
iam.nameToIdentity = nameToIdentity iam.nameToIdentity = nameToIdentity
iam.accessKeyIdent = accessKeyIdent iam.accessKeyIdent = accessKeyIdent
iam.policies = policies iam.policies = policies
iam.accessKeyIdent = accessKeyIdent
// Update authentication state based on whether identities exist // Update authentication state based on whether identities exist
// Once enabled, keep it enabled (one-way toggle) // Once enabled, keep it enabled (one-way toggle)
authJustEnabled := iam.updateAuthenticationState(len(identities)) authJustEnabled := iam.updateAuthenticationState(len(identities))
@ -1010,11 +1012,11 @@ func generatePrincipalArn(identityName string) string {
// Handle special cases // Handle special cases
switch identityName { switch identityName {
case AccountAnonymous.Id: case AccountAnonymous.Id:
return "arn:aws:iam::user/anonymous"
return "*" // Use universal wildcard for anonymous allowed by bucket policy
case AccountAdmin.Id: case AccountAdmin.Id:
return "arn:aws:iam::user/admin"
return fmt.Sprintf("arn:aws:iam::%s:user/admin", defaultAccountID)
default: default:
return fmt.Sprintf("arn:aws:iam::user/%s", identityName)
return fmt.Sprintf("arn:aws:iam::%s:user/%s", defaultAccountID, identityName)
} }
} }
@ -1406,7 +1408,12 @@ func buildPrincipalARN(identity *Identity, r *http.Request) string {
return "*" // Anonymous return "*" // Anonymous
} }
// Check if this is the anonymous user identity (authenticated as anonymous)
// Priority 1: Use principal ARN if explicitly set (from STS JWT or IAM user)
if identity.PrincipalArn != "" {
return identity.PrincipalArn
}
// Priority 2: Check if this is the anonymous user identity (authenticated as anonymous)
// S3 policies expect Principal: "*" for anonymous access // S3 policies expect Principal: "*" for anonymous access
if identity.Name == s3_constants.AccountAnonymousId || if identity.Name == s3_constants.AccountAnonymousId ||
(identity.Account != nil && identity.Account.Id == s3_constants.AccountAnonymousId) { (identity.Account != nil && identity.Account.Id == s3_constants.AccountAnonymousId) {
@ -1415,9 +1422,9 @@ func buildPrincipalARN(identity *Identity, r *http.Request) string {
// Build an AWS-compatible principal ARN // Build an AWS-compatible principal ARN
// Format: arn:aws:iam::account-id:user/user-name // Format: arn:aws:iam::account-id:user/user-name
accountId := identity.Account.Id
if accountId == "" {
accountId = "000000000000" // Default account ID
accountID := defaultAccountID // Default account ID
if identity.Account != nil && identity.Account.Id != "" {
accountID = identity.Account.Id
} }
userName := identity.Name userName := identity.Name
@ -1425,7 +1432,7 @@ func buildPrincipalARN(identity *Identity, r *http.Request) string {
userName = "unknown" userName = "unknown"
} }
return fmt.Sprintf("arn:aws:iam::%s:user/%s", accountId, userName)
return fmt.Sprintf("arn:aws:iam::%s:user/%s", accountID, userName)
} }
// GetCredentialManager returns the credential manager instance // GetCredentialManager returns the credential manager instance
@ -1435,7 +1442,6 @@ func (iam *IdentityAccessManagement) GetCredentialManager() *credential.Credenti
// LoadS3ApiConfigurationFromCredentialManager loads configuration using the credential manager // LoadS3ApiConfigurationFromCredentialManager loads configuration using the credential manager
func (iam *IdentityAccessManagement) LoadS3ApiConfigurationFromCredentialManager() error { func (iam *IdentityAccessManagement) LoadS3ApiConfigurationFromCredentialManager() error {
glog.V(1).Infof("IAM: reloading configuration from credential manager")
glog.V(1).Infof("Loading S3 API configuration from credential manager") glog.V(1).Infof("Loading S3 API configuration from credential manager")
s3ApiConfiguration, err := iam.credentialManager.LoadConfiguration(context.Background()) s3ApiConfiguration, err := iam.credentialManager.LoadConfiguration(context.Background())

7
weed/s3api/auth_credentials_test.go

@ -1,6 +1,7 @@
package s3api package s3api
import ( import (
"fmt"
"os" "os"
"reflect" "reflect"
"sync" "sync"
@ -294,7 +295,7 @@ func TestLoadS3ApiConfiguration(t *testing.T) {
expectIdent: &Identity{ expectIdent: &Identity{
Name: "notSpecifyAccountId", Name: "notSpecifyAccountId",
Account: &AccountAdmin, Account: &AccountAdmin,
PrincipalArn: "arn:aws:iam::user/notSpecifyAccountId",
PrincipalArn: fmt.Sprintf("arn:aws:iam::%s:user/notSpecifyAccountId", defaultAccountID),
Actions: []Action{ Actions: []Action{
"Read", "Read",
"Write", "Write",
@ -320,7 +321,7 @@ func TestLoadS3ApiConfiguration(t *testing.T) {
expectIdent: &Identity{ expectIdent: &Identity{
Name: "specifiedAccountID", Name: "specifiedAccountID",
Account: &specifiedAccount, Account: &specifiedAccount,
PrincipalArn: "arn:aws:iam::user/specifiedAccountID",
PrincipalArn: fmt.Sprintf("arn:aws:iam::%s:user/specifiedAccountID", defaultAccountID),
Actions: []Action{ Actions: []Action{
"Read", "Read",
"Write", "Write",
@ -338,7 +339,7 @@ func TestLoadS3ApiConfiguration(t *testing.T) {
expectIdent: &Identity{ expectIdent: &Identity{
Name: "anonymous", Name: "anonymous",
Account: &AccountAnonymous, Account: &AccountAnonymous,
PrincipalArn: "arn:aws:iam::user/anonymous",
PrincipalArn: "*",
Actions: []Action{ Actions: []Action{
"Read", "Read",
"Write", "Write",

11
weed/s3api/s3api_bucket_policy_arn_test.go

@ -1,6 +1,7 @@
package s3api package s3api
import ( import (
"fmt"
"testing" "testing"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
@ -62,6 +63,14 @@ func TestBuildPrincipalARN(t *testing.T) {
identity: nil, identity: nil,
expected: "*", expected: "*",
}, },
{
name: "explicit principal ARN",
identity: &Identity{
Name: "test-user",
PrincipalArn: "arn:aws:iam::123456789012:role/MyRole",
},
expected: "arn:aws:iam::123456789012:role/MyRole",
},
{ {
name: "anonymous user by name", name: "anonymous user by name",
identity: &Identity{ identity: &Identity{
@ -100,7 +109,7 @@ func TestBuildPrincipalARN(t *testing.T) {
Id: "", Id: "",
}, },
}, },
expected: "arn:aws:iam::000000000000:user/test-user",
expected: fmt.Sprintf("arn:aws:iam::%s:user/test-user", defaultAccountID),
}, },
{ {
name: "identity without name", name: "identity without name",

44
weed/s3api/s3api_sts.go

@ -44,9 +44,6 @@ const (
const ( const (
minDurationSeconds = int64(900) // 15 minutes minDurationSeconds = int64(900) // 15 minutes
maxDurationSeconds = int64(43200) // 12 hours maxDurationSeconds = int64(43200) // 12 hours
// Default account ID for federated users
defaultAccountId = "111122223333"
) )
// parseDurationSeconds parses and validates the DurationSeconds parameter // parseDurationSeconds parses and validates the DurationSeconds parameter
@ -88,6 +85,13 @@ func NewSTSHandlers(stsService *sts.STSService, iam *IdentityAccessManagement) *
} }
} }
func (h *STSHandlers) getAccountID() string {
if h.stsService != nil && h.stsService.Config != nil && h.stsService.Config.AccountId != "" {
return h.stsService.Config.AccountId
}
return defaultAccountID
}
// HandleSTSRequest is the main entry point for STS requests // HandleSTSRequest is the main entry point for STS requests
// It routes requests based on the Action parameter // It routes requests based on the Action parameter
func (h *STSHandlers) HandleSTSRequest(w http.ResponseWriter, r *http.Request) { func (h *STSHandlers) HandleSTSRequest(w http.ResponseWriter, r *http.Request) {
@ -287,7 +291,7 @@ func (h *STSHandlers) handleAssumeRole(w http.ResponseWriter, r *http.Request) {
} }
// Generate common STS components // Generate common STS components
stsCreds, assumedUser, err := h.prepareSTSCredentials(roleArn, roleSessionName, identity.PrincipalArn, durationSeconds, nil)
stsCreds, assumedUser, err := h.prepareSTSCredentials(roleArn, roleSessionName, durationSeconds, nil)
if err != nil { if err != nil {
h.writeSTSErrorResponse(w, r, STSErrInternalError, err) h.writeSTSErrorResponse(w, r, STSErrInternalError, err)
return return
@ -396,14 +400,7 @@ func (h *STSHandlers) handleAssumeRoleWithLDAPIdentity(w http.ResponseWriter, r
glog.V(2).Infof("AssumeRoleWithLDAPIdentity: user %s authenticated successfully, groups=%v", glog.V(2).Infof("AssumeRoleWithLDAPIdentity: user %s authenticated successfully, groups=%v",
ldapUsername, identity.Groups) ldapUsername, identity.Groups)
// Verify that the identity is allowed to assume the role
// We create a temporary identity to represent the LDAP user for permission checking
// The checking logic will verify if the role's trust policy allows this principal
// Use configured account ID or default to "111122223333" for federated users
accountId := defaultAccountId
if h.stsService != nil && h.stsService.Config != nil && h.stsService.Config.AccountId != "" {
accountId = h.stsService.Config.AccountId
}
accountID := h.getAccountID()
ldapUserIdentity := &Identity{ ldapUserIdentity := &Identity{
Name: identity.UserID, Name: identity.UserID,
@ -412,7 +409,7 @@ func (h *STSHandlers) handleAssumeRoleWithLDAPIdentity(w http.ResponseWriter, r
EmailAddress: identity.Email, EmailAddress: identity.Email,
Id: identity.UserID, Id: identity.UserID,
}, },
PrincipalArn: fmt.Sprintf("arn:aws:iam::%s:user/%s", accountId, identity.UserID),
PrincipalArn: fmt.Sprintf("arn:aws:iam::%s:user/%s", accountID, identity.UserID),
} }
// Verify that the identity is allowed to assume the role by checking the Trust Policy // Verify that the identity is allowed to assume the role by checking the Trust Policy
@ -428,7 +425,7 @@ func (h *STSHandlers) handleAssumeRoleWithLDAPIdentity(w http.ResponseWriter, r
claims.WithIdentityProvider("ldap", identity.UserID, identity.Provider) claims.WithIdentityProvider("ldap", identity.UserID, identity.Provider)
} }
stsCreds, assumedUser, err := h.prepareSTSCredentials(roleArn, roleSessionName, ldapUserIdentity.PrincipalArn, durationSeconds, modifyClaims)
stsCreds, assumedUser, err := h.prepareSTSCredentials(roleArn, roleSessionName, durationSeconds, modifyClaims)
if err != nil { if err != nil {
h.writeSTSErrorResponse(w, r, STSErrInternalError, err) h.writeSTSErrorResponse(w, r, STSErrInternalError, err)
return return
@ -447,7 +444,7 @@ func (h *STSHandlers) handleAssumeRoleWithLDAPIdentity(w http.ResponseWriter, r
} }
// prepareSTSCredentials extracts common shared logic for credential generation // prepareSTSCredentials extracts common shared logic for credential generation
func (h *STSHandlers) prepareSTSCredentials(roleArn, roleSessionName, principalArn string,
func (h *STSHandlers) prepareSTSCredentials(roleArn, roleSessionName string,
durationSeconds *int64, modifyClaims func(*sts.STSSessionClaims)) (STSCredentials, *AssumedRoleUser, error) { durationSeconds *int64, modifyClaims func(*sts.STSSessionClaims)) (STSCredentials, *AssumedRoleUser, error) {
// Calculate duration // Calculate duration
@ -470,10 +467,17 @@ func (h *STSHandlers) prepareSTSCredentials(roleArn, roleSessionName, principalA
roleName = roleArn // Fallback to full ARN if extraction fails roleName = roleArn // Fallback to full ARN if extraction fails
} }
accountID := h.getAccountID()
// Construct AssumedRoleUser ARN - this will be used as the principal for the vended token
assumedRoleArn := fmt.Sprintf("arn:aws:sts::%s:assumed-role/%s/%s", accountID, roleName, roleSessionName)
// Create session claims with role information // Create session claims with role information
// SECURITY: Use the assumedRoleArn as the principal in the token.
// This ensures that subsequent requests using this token are correctly identified as the assumed role.
claims := sts.NewSTSSessionClaims(sessionId, h.stsService.Config.Issuer, expiration). claims := sts.NewSTSSessionClaims(sessionId, h.stsService.Config.Issuer, expiration).
WithSessionName(roleSessionName). WithSessionName(roleSessionName).
WithRoleInfo(roleArn, fmt.Sprintf("%s:%s", roleName, roleSessionName), principalArn)
WithRoleInfo(roleArn, fmt.Sprintf("%s:%s", roleName, roleSessionName), assumedRoleArn)
// Apply custom claims if provided (e.g., LDAP identity) // Apply custom claims if provided (e.g., LDAP identity)
if modifyClaims != nil { if modifyClaims != nil {
@ -495,12 +499,6 @@ func (h *STSHandlers) prepareSTSCredentials(roleArn, roleSessionName, principalA
accessKeyId := stsCredsDet.AccessKeyId accessKeyId := stsCredsDet.AccessKeyId
secretAccessKey := stsCredsDet.SecretAccessKey secretAccessKey := stsCredsDet.SecretAccessKey
// Get account ID from STS config or use default
accountId := defaultAccountId
if h.stsService != nil && h.stsService.Config != nil && h.stsService.Config.AccountId != "" {
accountId = h.stsService.Config.AccountId
}
stsCreds := STSCredentials{ stsCreds := STSCredentials{
AccessKeyId: accessKeyId, AccessKeyId: accessKeyId,
SecretAccessKey: secretAccessKey, SecretAccessKey: secretAccessKey,
@ -510,7 +508,7 @@ func (h *STSHandlers) prepareSTSCredentials(roleArn, roleSessionName, principalA
assumedUser := &AssumedRoleUser{ assumedUser := &AssumedRoleUser{
AssumedRoleId: fmt.Sprintf("%s:%s", roleName, roleSessionName), AssumedRoleId: fmt.Sprintf("%s:%s", roleName, roleSessionName),
Arn: fmt.Sprintf("arn:aws:sts::%s:assumed-role/%s/%s", accountId, roleName, roleSessionName),
Arn: assumedRoleArn,
} }
return stsCreds, assumedUser, nil return stsCreds, assumedUser, nil

Loading…
Cancel
Save