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)
|
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
|
// UpdateBucketPolicy updates the policy for a bucket
|
||||||
func (m *IAMManager) UpdateBucketPolicy(ctx context.Context, bucketName string, policyJSON []byte) error {
|
func (m *IAMManager) UpdateBucketPolicy(ctx context.Context, bucketName string, policyJSON []byte) error {
|
||||||
if !m.initialized {
|
if !m.initialized {
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ type IAMIntegration interface {
|
|||||||
DefaultAllow() bool
|
DefaultAllow() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IAMManagerProvider exposes the IAMManager backing an IAM integration.
|
||||||
|
type IAMManagerProvider interface {
|
||||||
|
GetIAMManager() *integration.IAMManager
|
||||||
|
}
|
||||||
|
|
||||||
// S3IAMIntegration provides IAM integration for S3 API
|
// S3IAMIntegration provides IAM integration for S3 API
|
||||||
type S3IAMIntegration struct {
|
type S3IAMIntegration struct {
|
||||||
iamManager *integration.IAMManager
|
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
|
// AuthenticateJWT authenticates JWT tokens using our STS service
|
||||||
func (s3iam *S3IAMIntegration) AuthenticateJWT(ctx context.Context, r *http.Request) (*IAMIdentity, s3err.ErrorCode) {
|
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.
|
// AWS SDKs to obtain temporary credentials using OIDC/JWT tokens.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
"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/ldap"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
|
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/iam/utils"
|
"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
|
// 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 {
|
if err != nil {
|
||||||
h.writeSTSErrorResponse(w, r, STSErrInternalError, err)
|
h.writeSTSErrorResponse(w, r, STSErrInternalError, err)
|
||||||
return
|
return
|
||||||
@@ -480,7 +482,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, durationSeconds, sessionPolicyJSON, modifyClaims)
|
stsCreds, assumedUser, err := h.prepareSTSCredentials(r.Context(), roleArn, roleSessionName, durationSeconds, sessionPolicyJSON, modifyClaims)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.writeSTSErrorResponse(w, r, STSErrInternalError, err)
|
h.writeSTSErrorResponse(w, r, STSErrInternalError, err)
|
||||||
return
|
return
|
||||||
@@ -499,7 +501,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 string,
|
func (h *STSHandlers) prepareSTSCredentials(ctx context.Context, roleArn, roleSessionName string,
|
||||||
durationSeconds *int64, sessionPolicy string, modifyClaims func(*sts.STSSessionClaims)) (STSCredentials, *AssumedRoleUser, error) {
|
durationSeconds *int64, sessionPolicy string, modifyClaims func(*sts.STSSessionClaims)) (STSCredentials, *AssumedRoleUser, error) {
|
||||||
|
|
||||||
// Calculate duration
|
// Calculate duration
|
||||||
@@ -546,6 +548,33 @@ func (h *STSHandlers) prepareSTSCredentials(roleArn, roleSessionName string,
|
|||||||
WithSessionName(roleSessionName).
|
WithSessionName(roleSessionName).
|
||||||
WithRoleInfo(effectiveRoleArn, fmt.Sprintf("%s:%s", roleName, roleSessionName), assumedRoleArn)
|
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 != "" {
|
if sessionPolicy != "" {
|
||||||
claims.WithSessionPolicy(sessionPolicy)
|
claims.WithSessionPolicy(sessionPolicy)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"testing"
|
"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/iam/sts"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
"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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Assertions
|
// Assertions
|
||||||
@@ -107,7 +110,7 @@ func TestAssumeRole_CallerIdentityFallback(t *testing.T) {
|
|||||||
|
|
||||||
fallbackRoleArn := callerIdentity.PrincipalArn
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// The role name should be extracted from the assumed role ARN ("admin")
|
// 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) {
|
t.Run("Explicit RoleArn Provided", func(t *testing.T) {
|
||||||
explicitRoleArn := "arn:aws:iam::111122223333:role/TargetRole"
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Role name should be "TargetRole"
|
// Role name should be "TargetRole"
|
||||||
@@ -140,7 +143,7 @@ func TestAssumeRole_CallerIdentityFallback(t *testing.T) {
|
|||||||
t.Run("Malformed ARN", func(t *testing.T) {
|
t.Run("Malformed ARN", func(t *testing.T) {
|
||||||
malformedArn := "invalid-arn"
|
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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Fallback behavior: use full string as role name if extraction fails
|
// 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)
|
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