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)
This commit is contained in:
@@ -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
|
accountID := defaultAccountID // Default account ID
|
||||||
if accountId == "" {
|
if identity.Account != nil && identity.Account.Id != "" {
|
||||||
accountId = "000000000000" // Default 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())
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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,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
|
accountID := h.getAccountID()
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user