Browse Source

Embed role policies in AssumeRole STS tokens (#8421)

* Embed role policies in AssumeRole STS tokens

* Log STS policy lookup failures

* Use IAMManager provider

* Guard policy embedding role lookup
pull/7183/merge
Chris Lu 18 hours ago
committed by GitHub
parent
commit
2d65d7f499
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 12
      weed/iam/integration/iam_manager.go
  2. 10
      weed/s3api/s3_iam_middleware.go
  3. 35
      weed/s3api/s3api_sts.go
  4. 98
      weed/s3api/s3api_sts_assume_role_test.go

12
weed/iam/integration/iam_manager.go

@ -248,6 +248,18 @@ func (m *IAMManager) CreateRole(ctx context.Context, filerAddress string, roleNa
return m.roleStore.StoreRole(ctx, "", roleName, roleDef)
}
// GetRole retrieves a role definition by name.
func (m *IAMManager) GetRole(ctx context.Context, roleName string) (*RoleDefinition, error) {
if !m.initialized {
return nil, fmt.Errorf("IAM manager not initialized")
}
if roleName == "" {
return nil, fmt.Errorf("role name cannot be empty")
}
return m.roleStore.GetRole(ctx, m.getFilerAddress(), roleName)
}
// UpdateBucketPolicy updates the policy for a bucket
func (m *IAMManager) UpdateBucketPolicy(ctx context.Context, bucketName string, policyJSON []byte) error {
if !m.initialized {

10
weed/s3api/s3_iam_middleware.go

@ -47,6 +47,11 @@ type IAMIntegration interface {
DefaultAllow() bool
}
// IAMManagerProvider exposes the IAMManager backing an IAM integration.
type IAMManagerProvider interface {
GetIAMManager() *integration.IAMManager
}
// S3IAMIntegration provides IAM integration for S3 API
type S3IAMIntegration struct {
iamManager *integration.IAMManager
@ -70,6 +75,11 @@ func NewS3IAMIntegration(iamManager *integration.IAMManager, filerAddress string
}
}
// GetIAMManager returns the IAMManager backing this integration.
func (s3iam *S3IAMIntegration) GetIAMManager() *integration.IAMManager {
return s3iam.iamManager
}
// AuthenticateJWT authenticates JWT tokens using our STS service
func (s3iam *S3IAMIntegration) AuthenticateJWT(ctx context.Context, r *http.Request) (*IAMIdentity, s3err.ErrorCode) {

35
weed/s3api/s3api_sts.go

@ -5,6 +5,7 @@ package s3api
// AWS SDKs to obtain temporary credentials using OIDC/JWT tokens.
import (
"context"
"encoding/xml"
"errors"
"fmt"
@ -13,6 +14,7 @@ import (
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/iam/integration"
"github.com/seaweedfs/seaweedfs/weed/iam/ldap"
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
"github.com/seaweedfs/seaweedfs/weed/iam/utils"
@ -339,7 +341,7 @@ func (h *STSHandlers) handleAssumeRole(w http.ResponseWriter, r *http.Request) {
}
// Generate common STS components
stsCreds, assumedUser, err := h.prepareSTSCredentials(roleArn, roleSessionName, durationSeconds, sessionPolicyJSON, modifyClaims)
stsCreds, assumedUser, err := h.prepareSTSCredentials(r.Context(), roleArn, roleSessionName, durationSeconds, sessionPolicyJSON, modifyClaims)
if err != nil {
h.writeSTSErrorResponse(w, r, STSErrInternalError, err)
return
@ -480,7 +482,7 @@ func (h *STSHandlers) handleAssumeRoleWithLDAPIdentity(w http.ResponseWriter, r
claims.WithIdentityProvider("ldap", identity.UserID, identity.Provider)
}
stsCreds, assumedUser, err := h.prepareSTSCredentials(roleArn, roleSessionName, durationSeconds, sessionPolicyJSON, modifyClaims)
stsCreds, assumedUser, err := h.prepareSTSCredentials(r.Context(), roleArn, roleSessionName, durationSeconds, sessionPolicyJSON, modifyClaims)
if err != nil {
h.writeSTSErrorResponse(w, r, STSErrInternalError, err)
return
@ -499,7 +501,7 @@ func (h *STSHandlers) handleAssumeRoleWithLDAPIdentity(w http.ResponseWriter, r
}
// prepareSTSCredentials extracts common shared logic for credential generation
func (h *STSHandlers) prepareSTSCredentials(roleArn, roleSessionName string,
func (h *STSHandlers) prepareSTSCredentials(ctx context.Context, roleArn, roleSessionName string,
durationSeconds *int64, sessionPolicy string, modifyClaims func(*sts.STSSessionClaims)) (STSCredentials, *AssumedRoleUser, error) {
// Calculate duration
@ -546,6 +548,33 @@ func (h *STSHandlers) prepareSTSCredentials(roleArn, roleSessionName string,
WithSessionName(roleSessionName).
WithRoleInfo(effectiveRoleArn, fmt.Sprintf("%s:%s", roleName, roleSessionName), assumedRoleArn)
// If IAM integration is available, embed the role's attached policies into the session token.
// This makes the token self-sufficient for authorization even when role lookup is unavailable.
var policyManager *integration.IAMManager
if h.iam != nil && h.iam.iamIntegration != nil {
if provider, ok := h.iam.iamIntegration.(IAMManagerProvider); ok {
policyManager = provider.GetIAMManager()
}
}
if policyManager != nil {
roleNameForPolicies := utils.ExtractRoleNameFromArn(roleArn)
if roleNameForPolicies == "" {
roleNameForPolicies = utils.ExtractRoleNameFromPrincipal(roleArn)
}
if roleNameForPolicies != "" && len(claims.Policies) == 0 {
roleDef, err := policyManager.GetRole(ctx, roleNameForPolicies)
if err != nil {
glog.V(2).Infof("Failed to load role %q for policy embedding: %v", roleNameForPolicies, err)
} else if roleDef == nil {
glog.V(2).Infof("Role definition %q was missing for policy embedding", roleNameForPolicies)
} else if len(roleDef.AttachedPolicies) > 0 {
claims.WithPolicies(roleDef.AttachedPolicies)
}
}
}
if sessionPolicy != "" {
claims.WithSessionPolicy(sessionPolicy)
}

98
weed/s3api/s3api_sts_assume_role_test.go

@ -6,7 +6,10 @@ import (
"net/http"
"net/url"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/iam/integration"
"github.com/seaweedfs/seaweedfs/weed/iam/policy"
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
@ -75,7 +78,7 @@ func TestAssumeRole_CallerIdentityFallback(t *testing.T) {
}
}
stsCreds, assumedUser, err := stsHandlers.prepareSTSCredentials(fallbackRoleArn, "test-session", nil, "", modifyClaims)
stsCreds, assumedUser, err := stsHandlers.prepareSTSCredentials(context.Background(), fallbackRoleArn, "test-session", nil, "", modifyClaims)
require.NoError(t, err)
// Assertions
@ -107,7 +110,7 @@ func TestAssumeRole_CallerIdentityFallback(t *testing.T) {
fallbackRoleArn := callerIdentity.PrincipalArn
stsCreds, assumedUser, err := stsHandlers.prepareSTSCredentials(fallbackRoleArn, "nested-session", nil, "", nil)
stsCreds, assumedUser, err := stsHandlers.prepareSTSCredentials(context.Background(), fallbackRoleArn, "nested-session", nil, "", nil)
require.NoError(t, err)
// The role name should be extracted from the assumed role ARN ("admin")
@ -124,7 +127,7 @@ func TestAssumeRole_CallerIdentityFallback(t *testing.T) {
t.Run("Explicit RoleArn Provided", func(t *testing.T) {
explicitRoleArn := "arn:aws:iam::111122223333:role/TargetRole"
stsCreds, assumedUser, err := stsHandlers.prepareSTSCredentials(explicitRoleArn, "explicit-session", nil, "", nil)
stsCreds, assumedUser, err := stsHandlers.prepareSTSCredentials(context.Background(), explicitRoleArn, "explicit-session", nil, "", nil)
require.NoError(t, err)
// Role name should be "TargetRole"
@ -140,7 +143,7 @@ func TestAssumeRole_CallerIdentityFallback(t *testing.T) {
t.Run("Malformed ARN", func(t *testing.T) {
malformedArn := "invalid-arn"
stsCreds, assumedUser, err := stsHandlers.prepareSTSCredentials(malformedArn, "bad-session", nil, "", nil)
stsCreds, assumedUser, err := stsHandlers.prepareSTSCredentials(context.Background(), malformedArn, "bad-session", nil, "", nil)
require.NoError(t, err)
// Fallback behavior: use full string as role name if extraction fails
@ -151,3 +154,90 @@ func TestAssumeRole_CallerIdentityFallback(t *testing.T) {
assert.Equal(t, malformedArn, sessionInfo.RoleArn)
})
}
func TestAssumeRole_EmbedsRolePolicies(t *testing.T) {
t.Run("RoleWithAttachedPolicies", func(t *testing.T) {
ctx := context.Background()
manager := newTestSTSIntegrationManager(t)
writePolicy := &policy.PolicyDocument{
Version: "2012-10-17",
Statement: []policy.Statement{
{
Effect: "Allow",
Action: []string{"s3:*"},
Resource: []string{
"arn:aws:s3:::*",
"arn:aws:s3:::*/*",
},
},
},
}
require.NoError(t, manager.CreatePolicy(ctx, "", "S3WritePolicy", writePolicy))
roleName := "LakekeeperVendedRole"
require.NoError(t, manager.CreateRole(ctx, "", roleName, &integration.RoleDefinition{
RoleName: roleName,
AttachedPolicies: []string{"S3WritePolicy"},
}))
iam := &IdentityAccessManagement{
iamIntegration: NewS3IAMIntegration(manager, ""),
}
stsHandlers := NewSTSHandlers(manager.GetSTSService(), iam)
roleArn := fmt.Sprintf("arn:aws:iam::%s:role/%s", defaultAccountID, roleName)
stsCreds, _, err := stsHandlers.prepareSTSCredentials(ctx, roleArn, "test-session", nil, "", nil)
require.NoError(t, err)
sessionInfo, err := manager.GetSTSService().ValidateSessionToken(ctx, stsCreds.SessionToken)
require.NoError(t, err)
require.NotNil(t, sessionInfo)
assert.Equal(t, []string{"S3WritePolicy"}, sessionInfo.Policies)
})
t.Run("RoleWithoutAttachedPolicies", func(t *testing.T) {
ctx := context.Background()
manager := newTestSTSIntegrationManager(t)
roleName := "LakekeeperEmptyRole"
require.NoError(t, manager.CreateRole(ctx, "", roleName, &integration.RoleDefinition{
RoleName: roleName,
}))
iam := &IdentityAccessManagement{
iamIntegration: NewS3IAMIntegration(manager, ""),
}
stsHandlers := NewSTSHandlers(manager.GetSTSService(), iam)
roleArn := fmt.Sprintf("arn:aws:iam::%s:role/%s", defaultAccountID, roleName)
stsCreds, _, err := stsHandlers.prepareSTSCredentials(ctx, roleArn, "test-session", nil, "", nil)
require.NoError(t, err)
sessionInfo, err := manager.GetSTSService().ValidateSessionToken(ctx, stsCreds.SessionToken)
require.NoError(t, err)
assert.Empty(t, sessionInfo.Policies)
})
}
func newTestSTSIntegrationManager(t *testing.T) *integration.IAMManager {
t.Helper()
manager := integration.NewIAMManager()
config := &integration.IAMConfig{
STS: &sts.STSConfig{
TokenDuration: sts.FlexibleDuration{Duration: time.Hour},
MaxSessionLength: sts.FlexibleDuration{Duration: 12 * time.Hour},
Issuer: "test-issuer",
SigningKey: []byte("test-signing-key-at-least-32-bytes-long-for-security"),
},
Policy: &policy.PolicyEngineConfig{
DefaultEffect: "Deny",
StoreType: "memory",
},
Roles: &integration.RoleStoreConfig{
StoreType: "memory",
},
}
require.NoError(t, manager.Initialize(config, func() string { return "" }))
return manager
}
Loading…
Cancel
Save