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
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user