mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-16 11:30:44 +02:00
* sts: enforce session-policy explicit deny during role chaining A chained AssumeRole caller authenticates with an STS session token whose inline session policy can explicitly deny sts:AssumeRole. The deny check only evaluated the caller's named policies, so such a session could still chain into any role its trust policy admits. Validate the session token in the deny check and honor an explicit Deny in the inline session policy too. * test(sts): integration coverage for AssumeRole authorization Add an end-to-end AssumeRole authorization test (real weed mini + boto3): a non-admin caller assumes a role its trust policy admits, an explicit identity-side deny is blocked, and a session policy's explicit deny blocks role chaining. * sts: skip OIDC tokens and reject revoked sessions in the chaining deny check Review follow-ups on the session-policy deny check: - Guard session validation with !isOIDCToken so a bearer token our STS service cannot validate does not error into a false deny. - Reject a revoked session before evaluating its policy, restoring the revocation enforcement the AssumeRole path lost when it stopped routing through IsActionAllowed.
1613 lines
58 KiB
Go
1613 lines
58 KiB
Go
package integration
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/oidc"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/policy"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/providers"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/utils"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
|
)
|
|
|
|
// maxPoliciesForEvaluation defines an upper bound on the number of policies that
|
|
// will be evaluated for a single request. This protects against pathological or
|
|
// malicious inputs that attempt to create extremely large policy lists.
|
|
const maxPoliciesForEvaluation = 1024
|
|
|
|
// IAMManager orchestrates all IAM components
|
|
type IAMManager struct {
|
|
stsService *sts.STSService
|
|
policyEngine *policy.PolicyEngine
|
|
roleStore RoleStore
|
|
userStore UserStore
|
|
oidcProviderStore OIDCProviderStore
|
|
oidcAuditSink OIDCProviderAuditSink
|
|
revocationStore SessionRevocationStore
|
|
filerAddressProvider func() string // Function to get current filer address
|
|
initialized bool
|
|
runtimePolicyMu sync.Mutex
|
|
runtimePolicyNames map[string]struct{}
|
|
}
|
|
|
|
// SetOIDCProviderAuditSink configures the lifecycle event sink. When nil
|
|
// (default), GlogAuditSink is used so events still surface in logs.
|
|
func (m *IAMManager) SetOIDCProviderAuditSink(sink OIDCProviderAuditSink) {
|
|
m.oidcAuditSink = sink
|
|
}
|
|
|
|
// emitOIDCAudit logs a lifecycle event. Errors are swallowed: an audit
|
|
// failure must never block an IAM mutation that has already succeeded.
|
|
func (m *IAMManager) emitOIDCAudit(ctx context.Context, eventType OIDCProviderAuditEventType, arn, url string, detail map[string]string) {
|
|
sink := m.oidcAuditSink
|
|
if sink == nil {
|
|
sink = GlogAuditSink{}
|
|
}
|
|
event := &OIDCProviderAuditEvent{
|
|
Type: eventType,
|
|
ARN: arn,
|
|
URL: url,
|
|
Detail: detail,
|
|
OccurredAt: time.Now().UTC(),
|
|
}
|
|
if err := sink.Emit(ctx, event); err != nil {
|
|
glog.Warningf("OIDC audit emit %s for %s: %v", eventType, arn, err)
|
|
}
|
|
}
|
|
|
|
// SetSessionRevocationStore configures the per-session revocation list. When
|
|
// nil, RevokeSession returns an error and IsSessionRevoked is a no-op (every
|
|
// session is considered live until natural expiry). Operators who want
|
|
// revocation must wire a store explicitly.
|
|
func (m *IAMManager) SetSessionRevocationStore(store SessionRevocationStore) {
|
|
m.revocationStore = store
|
|
}
|
|
|
|
// RevokeSession marks a session as revoked using its JTI (which equals the
|
|
// session ID for STS-issued tokens).
|
|
func (m *IAMManager) RevokeSession(ctx context.Context, jti string, expiresAt time.Time, reason string) error {
|
|
if m.revocationStore == nil {
|
|
return fmt.Errorf("session revocation store not configured")
|
|
}
|
|
if jti == "" {
|
|
return fmt.Errorf("jti cannot be empty")
|
|
}
|
|
return m.revocationStore.Revoke(ctx, m.getFilerAddress(), &RevocationEntry{
|
|
JTI: jti,
|
|
ExpiresAt: expiresAt,
|
|
Reason: reason,
|
|
})
|
|
}
|
|
|
|
// IsSessionRevoked returns true if the given JTI has been revoked. Returns
|
|
// false (with nil error) when no revocation store is configured.
|
|
func (m *IAMManager) IsSessionRevoked(ctx context.Context, jti string) (bool, error) {
|
|
if m.revocationStore == nil || jti == "" {
|
|
return false, nil
|
|
}
|
|
return m.revocationStore.IsRevoked(ctx, m.getFilerAddress(), jti)
|
|
}
|
|
|
|
// PurgeRevokedSessions removes revocation entries whose underlying session
|
|
// has already expired. Safe to call on a cron schedule.
|
|
func (m *IAMManager) PurgeRevokedSessions(ctx context.Context) (int, error) {
|
|
if m.revocationStore == nil {
|
|
return 0, nil
|
|
}
|
|
return m.revocationStore.Purge(ctx, m.getFilerAddress(), time.Now().UTC())
|
|
}
|
|
|
|
// SetOIDCProviderStore configures the IAM-managed OIDC provider store. When
|
|
// nil, OIDC provider IAM actions return ServiceNotReady. The store is the
|
|
// source of truth for AssumeRoleWithWebIdentity provider resolution once
|
|
// Phase 2b lands; in Phase 2a it is read-only and populated from static
|
|
// configuration at boot.
|
|
func (m *IAMManager) SetOIDCProviderStore(store OIDCProviderStore) {
|
|
m.oidcProviderStore = store
|
|
}
|
|
|
|
// GetOIDCProviderStore returns the configured store (may be nil).
|
|
func (m *IAMManager) GetOIDCProviderStore() OIDCProviderStore {
|
|
return m.oidcProviderStore
|
|
}
|
|
|
|
// GetOIDCProvider returns the record for the given ARN, or an error if the
|
|
// store is not configured or the record is missing.
|
|
func (m *IAMManager) GetOIDCProvider(ctx context.Context, arn string) (*OIDCProviderRecord, error) {
|
|
if m.oidcProviderStore == nil {
|
|
return nil, fmt.Errorf("OIDC provider store not configured")
|
|
}
|
|
return m.oidcProviderStore.GetProviderByARN(ctx, m.getFilerAddress(), arn)
|
|
}
|
|
|
|
// ListOIDCProviders enumerates all configured OIDC providers.
|
|
func (m *IAMManager) ListOIDCProviders(ctx context.Context) ([]*OIDCProviderRecord, error) {
|
|
if m.oidcProviderStore == nil {
|
|
return nil, fmt.Errorf("OIDC provider store not configured")
|
|
}
|
|
return m.oidcProviderStore.ListProviders(ctx, m.getFilerAddress())
|
|
}
|
|
|
|
// CreateOIDCProvider persists a new IAM-managed OIDC provider record. Refuses
|
|
// to overwrite an existing record so callers see EntityAlreadyExists semantics.
|
|
func (m *IAMManager) CreateOIDCProvider(ctx context.Context, rec *OIDCProviderRecord) error {
|
|
if m.oidcProviderStore == nil {
|
|
return fmt.Errorf("OIDC provider store not configured")
|
|
}
|
|
if rec == nil {
|
|
return fmt.Errorf("record cannot be nil")
|
|
}
|
|
if err := validateOIDCProviderRecord(rec); err != nil {
|
|
return err
|
|
}
|
|
existing, err := m.oidcProviderStore.GetProviderByARN(ctx, m.getFilerAddress(), rec.ARN)
|
|
if err == nil && existing != nil {
|
|
return fmt.Errorf("%w: %s", ErrOIDCProviderAlreadyExists, rec.ARN)
|
|
}
|
|
if err != nil && !errors.Is(err, ErrOIDCProviderNotFound) {
|
|
return fmt.Errorf("lookup existing OIDC provider %q: %w", rec.ARN, err)
|
|
}
|
|
now := time.Now().UTC()
|
|
rec.CreatedAt = now
|
|
rec.UpdatedAt = now
|
|
if err := m.oidcProviderStore.StoreProvider(ctx, m.getFilerAddress(), rec); err != nil {
|
|
return err
|
|
}
|
|
m.refreshOIDCProvidersBestEffort(ctx, "CreateOIDCProvider", rec.ARN)
|
|
m.emitOIDCAudit(ctx, OIDCAuditEventCreated, rec.ARN, rec.URL, nil)
|
|
return nil
|
|
}
|
|
|
|
// DeleteOIDCProvider removes the IAM-managed record. Idempotent.
|
|
func (m *IAMManager) DeleteOIDCProvider(ctx context.Context, arn string) error {
|
|
if m.oidcProviderStore == nil {
|
|
return fmt.Errorf("OIDC provider store not configured")
|
|
}
|
|
if err := m.oidcProviderStore.DeleteProvider(ctx, m.getFilerAddress(), arn); err != nil {
|
|
return err
|
|
}
|
|
m.refreshOIDCProvidersBestEffort(ctx, "DeleteOIDCProvider", arn)
|
|
m.emitOIDCAudit(ctx, OIDCAuditEventDeleted, arn, "", nil)
|
|
return nil
|
|
}
|
|
|
|
// AddClientIDToOIDCProvider appends `clientID` to the provider's allowed
|
|
// audience list. Adding an existing client ID is a no-op (AWS-compat).
|
|
func (m *IAMManager) AddClientIDToOIDCProvider(ctx context.Context, arn, clientID string) error {
|
|
if m.oidcProviderStore == nil {
|
|
return fmt.Errorf("OIDC provider store not configured")
|
|
}
|
|
if clientID == "" {
|
|
return fmt.Errorf("ClientID cannot be empty")
|
|
}
|
|
rec, err := m.oidcProviderStore.GetProviderByARN(ctx, m.getFilerAddress(), arn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, existing := range rec.ClientIDs {
|
|
if existing == clientID {
|
|
return nil // idempotent
|
|
}
|
|
}
|
|
if len(rec.ClientIDs) >= 100 {
|
|
return fmt.Errorf("ClientIDList must contain at most 100 entries")
|
|
}
|
|
rec.ClientIDs = append(rec.ClientIDs, clientID)
|
|
rec.UpdatedAt = time.Now().UTC()
|
|
if err := m.oidcProviderStore.StoreProvider(ctx, m.getFilerAddress(), rec); err != nil {
|
|
return err
|
|
}
|
|
m.refreshOIDCProvidersBestEffort(ctx, "AddClientIDToOIDCProvider", arn)
|
|
m.emitOIDCAudit(ctx, OIDCAuditEventClientIDAdded, rec.ARN, rec.URL, map[string]string{"clientId": clientID})
|
|
return nil
|
|
}
|
|
|
|
// RemoveClientIDFromOIDCProvider drops `clientID` from the allowed audience
|
|
// list. Removing a missing client ID is a no-op.
|
|
func (m *IAMManager) RemoveClientIDFromOIDCProvider(ctx context.Context, arn, clientID string) error {
|
|
if m.oidcProviderStore == nil {
|
|
return fmt.Errorf("OIDC provider store not configured")
|
|
}
|
|
rec, err := m.oidcProviderStore.GetProviderByARN(ctx, m.getFilerAddress(), arn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pruned := make([]string, 0, len(rec.ClientIDs))
|
|
for _, existing := range rec.ClientIDs {
|
|
if existing != clientID {
|
|
pruned = append(pruned, existing)
|
|
}
|
|
}
|
|
if len(pruned) == len(rec.ClientIDs) {
|
|
return nil // not present; no-op
|
|
}
|
|
rec.ClientIDs = pruned
|
|
rec.UpdatedAt = time.Now().UTC()
|
|
if err := m.oidcProviderStore.StoreProvider(ctx, m.getFilerAddress(), rec); err != nil {
|
|
return err
|
|
}
|
|
m.refreshOIDCProvidersBestEffort(ctx, "RemoveClientIDFromOIDCProvider", arn)
|
|
m.emitOIDCAudit(ctx, OIDCAuditEventClientIDRemoved, rec.ARN, rec.URL, map[string]string{"clientId": clientID})
|
|
return nil
|
|
}
|
|
|
|
// UpdateOIDCProviderThumbprints replaces the entire thumbprint list. AWS
|
|
// constrains the list to 1..5 entries when non-empty; we mirror that bound.
|
|
func (m *IAMManager) UpdateOIDCProviderThumbprints(ctx context.Context, arn string, thumbprints []string) error {
|
|
if m.oidcProviderStore == nil {
|
|
return fmt.Errorf("OIDC provider store not configured")
|
|
}
|
|
if len(thumbprints) > 5 {
|
|
return fmt.Errorf("ThumbprintList must contain at most 5 entries, got %d", len(thumbprints))
|
|
}
|
|
for _, tp := range thumbprints {
|
|
if !isValidSHA1Thumbprint(tp) {
|
|
return fmt.Errorf("invalid thumbprint %q: must be 40-character SHA-1 hex", tp)
|
|
}
|
|
}
|
|
rec, err := m.oidcProviderStore.GetProviderByARN(ctx, m.getFilerAddress(), arn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rec.Thumbprints = append([]string(nil), thumbprints...)
|
|
rec.UpdatedAt = time.Now().UTC()
|
|
if err := m.oidcProviderStore.StoreProvider(ctx, m.getFilerAddress(), rec); err != nil {
|
|
return err
|
|
}
|
|
m.refreshOIDCProvidersBestEffort(ctx, "UpdateOIDCProviderThumbprints", arn)
|
|
m.emitOIDCAudit(ctx, OIDCAuditEventThumbprintsSet, rec.ARN, rec.URL, map[string]string{"count": fmt.Sprintf("%d", len(thumbprints))})
|
|
return nil
|
|
}
|
|
|
|
// TagOIDCProvider merges the supplied tags into the provider's tag set.
|
|
func (m *IAMManager) TagOIDCProvider(ctx context.Context, arn string, tags map[string]string) error {
|
|
if m.oidcProviderStore == nil {
|
|
return fmt.Errorf("OIDC provider store not configured")
|
|
}
|
|
rec, err := m.oidcProviderStore.GetProviderByARN(ctx, m.getFilerAddress(), arn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rec.Tags == nil {
|
|
rec.Tags = make(map[string]string, len(tags))
|
|
}
|
|
for k, v := range tags {
|
|
rec.Tags[k] = v
|
|
}
|
|
rec.UpdatedAt = time.Now().UTC()
|
|
if err := m.oidcProviderStore.StoreProvider(ctx, m.getFilerAddress(), rec); err != nil {
|
|
return err
|
|
}
|
|
m.emitOIDCAudit(ctx, OIDCAuditEventTagsAdded, rec.ARN, rec.URL, map[string]string{"count": fmt.Sprintf("%d", len(tags))})
|
|
return nil
|
|
}
|
|
|
|
// UntagOIDCProvider removes the named tags from the provider's tag set.
|
|
func (m *IAMManager) UntagOIDCProvider(ctx context.Context, arn string, keys []string) error {
|
|
if m.oidcProviderStore == nil {
|
|
return fmt.Errorf("OIDC provider store not configured")
|
|
}
|
|
rec, err := m.oidcProviderStore.GetProviderByARN(ctx, m.getFilerAddress(), arn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, k := range keys {
|
|
delete(rec.Tags, k)
|
|
}
|
|
rec.UpdatedAt = time.Now().UTC()
|
|
if err := m.oidcProviderStore.StoreProvider(ctx, m.getFilerAddress(), rec); err != nil {
|
|
return err
|
|
}
|
|
m.emitOIDCAudit(ctx, OIDCAuditEventTagsRemoved, rec.ARN, rec.URL, map[string]string{"count": fmt.Sprintf("%d", len(keys))})
|
|
return nil
|
|
}
|
|
|
|
// validateOIDCProviderRecord enforces the invariants AWS imposes on the
|
|
// underlying CreateOpenIDConnectProvider call.
|
|
func validateOIDCProviderRecord(rec *OIDCProviderRecord) error {
|
|
if rec.URL == "" {
|
|
return fmt.Errorf("Url is required")
|
|
}
|
|
if rec.ARN == "" {
|
|
return fmt.Errorf("ARN is required")
|
|
}
|
|
if len(rec.ClientIDs) == 0 {
|
|
return fmt.Errorf("ClientIDList must contain at least one entry")
|
|
}
|
|
if len(rec.ClientIDs) > 100 {
|
|
return fmt.Errorf("ClientIDList must contain at most 100 entries")
|
|
}
|
|
if len(rec.Thumbprints) > 5 {
|
|
return fmt.Errorf("ThumbprintList must contain at most 5 entries")
|
|
}
|
|
for _, tp := range rec.Thumbprints {
|
|
if !isValidSHA1Thumbprint(tp) {
|
|
return fmt.Errorf("invalid thumbprint %q: must be 40-character SHA-1 hex", tp)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// isValidSHA1Thumbprint returns true iff `s` is exactly 40 hex characters,
|
|
// matching the SHA-1 digest format AWS expects.
|
|
func isValidSHA1Thumbprint(s string) bool {
|
|
if len(s) != 40 {
|
|
return false
|
|
}
|
|
for _, r := range s {
|
|
switch {
|
|
case r >= '0' && r <= '9':
|
|
case r >= 'a' && r <= 'f':
|
|
case r >= 'A' && r <= 'F':
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// IAMConfig holds configuration for all IAM components
|
|
type IAMConfig struct {
|
|
// STS service configuration
|
|
STS *sts.STSConfig `json:"sts"`
|
|
|
|
// Policy engine configuration
|
|
Policy *policy.PolicyEngineConfig `json:"policy"`
|
|
|
|
// Role store configuration
|
|
Roles *RoleStoreConfig `json:"roleStore"`
|
|
|
|
// OIDCProviders configures the IAM-managed OIDC provider store. Optional;
|
|
// if absent the manager defaults to an in-memory store hydrated from
|
|
// STS.Providers at boot.
|
|
OIDCProviders *OIDCProviderStoreConfig `json:"oidcProviderStore,omitempty"`
|
|
}
|
|
|
|
// OIDCProviderStoreConfig holds OIDC provider store configuration.
|
|
type OIDCProviderStoreConfig struct {
|
|
StoreType string `json:"storeType"` // memory, filer
|
|
StoreConfig map[string]interface{} `json:"storeConfig,omitempty"`
|
|
}
|
|
|
|
// RoleStoreConfig holds role store configuration
|
|
type RoleStoreConfig struct {
|
|
// StoreType specifies the role store backend (memory, filer, etc.)
|
|
StoreType string `json:"storeType"`
|
|
|
|
// StoreConfig contains store-specific configuration
|
|
StoreConfig map[string]interface{} `json:"storeConfig,omitempty"`
|
|
}
|
|
|
|
// UserStore defines the interface for retrieving IAM user policy attachments.
|
|
type UserStore interface {
|
|
GetUser(ctx context.Context, username string) (*iam_pb.Identity, error)
|
|
}
|
|
|
|
// RoleDefinition defines a role with its trust policy and attached policies
|
|
type RoleDefinition struct {
|
|
// RoleName is the name of the role
|
|
RoleName string `json:"roleName"`
|
|
|
|
// RoleArn is the full ARN of the role
|
|
RoleArn string `json:"roleArn"`
|
|
|
|
// TrustPolicy defines who can assume this role
|
|
TrustPolicy *policy.PolicyDocument `json:"trustPolicy"`
|
|
|
|
// AttachedPolicies lists the policy names attached to this role
|
|
AttachedPolicies []string `json:"attachedPolicies"`
|
|
|
|
// Description is an optional description of the role
|
|
Description string `json:"description,omitempty"`
|
|
|
|
// MaxSessionDuration is the upper bound (in seconds) on session length when
|
|
// callers assume this role. Zero means "use the global STS default". When
|
|
// set it must satisfy AWS bounds: 3600 ≤ MaxSessionDuration ≤ 43200.
|
|
// Honoured by AssumeRole, AssumeRoleWithWebIdentity, AssumeRoleWithCredentials.
|
|
MaxSessionDuration int64 `json:"maxSessionDuration,omitempty"`
|
|
}
|
|
|
|
// ActionRequest represents a request to perform an action
|
|
type ActionRequest struct {
|
|
// Principal is the entity performing the action
|
|
Principal string `json:"principal"`
|
|
|
|
// Action is the action being requested
|
|
Action string `json:"action"`
|
|
|
|
// Resource is the resource being accessed
|
|
Resource string `json:"resource"`
|
|
|
|
// SessionToken for temporary credential validation
|
|
SessionToken string `json:"sessionToken"`
|
|
|
|
// RequestContext contains additional request information
|
|
RequestContext map[string]interface{} `json:"requestContext,omitempty"`
|
|
|
|
// PolicyNames to evaluate (overrides role-based policies if present)
|
|
PolicyNames []string `json:"policyNames,omitempty"`
|
|
}
|
|
|
|
// NewIAMManager creates a new IAM manager
|
|
func NewIAMManager() *IAMManager {
|
|
return &IAMManager{}
|
|
}
|
|
|
|
// SetUserStore assigns the user store used to resolve IAM user policy attachments.
|
|
func (m *IAMManager) SetUserStore(store UserStore) {
|
|
m.userStore = store
|
|
}
|
|
|
|
// SyncRuntimePolicies keeps zero-config runtime policies available to the
|
|
// in-memory policy engine used by the advanced IAM authorizer.
|
|
func (m *IAMManager) SyncRuntimePolicies(ctx context.Context, policies []*iam_pb.Policy) error {
|
|
if !m.initialized || m.policyEngine == nil {
|
|
return nil
|
|
}
|
|
if m.policyEngine.StoreType() != sts.StoreTypeMemory {
|
|
return nil
|
|
}
|
|
|
|
desiredPolicies := make(map[string]*policy.PolicyDocument, len(policies))
|
|
for _, runtimePolicy := range policies {
|
|
if runtimePolicy == nil || runtimePolicy.Name == "" {
|
|
continue
|
|
}
|
|
|
|
var document policy.PolicyDocument
|
|
if err := json.Unmarshal([]byte(runtimePolicy.Content), &document); err != nil {
|
|
return fmt.Errorf("failed to parse runtime policy %q: %w", runtimePolicy.Name, err)
|
|
}
|
|
|
|
desiredPolicies[runtimePolicy.Name] = &document
|
|
}
|
|
|
|
m.runtimePolicyMu.Lock()
|
|
defer m.runtimePolicyMu.Unlock()
|
|
|
|
filerAddress := m.getFilerAddress()
|
|
for policyName := range m.runtimePolicyNames {
|
|
if _, keep := desiredPolicies[policyName]; keep {
|
|
continue
|
|
}
|
|
if err := m.policyEngine.DeletePolicy(ctx, filerAddress, policyName); err != nil {
|
|
return fmt.Errorf("failed to delete runtime policy %q: %w", policyName, err)
|
|
}
|
|
}
|
|
|
|
for policyName, document := range desiredPolicies {
|
|
if err := m.policyEngine.AddPolicy(filerAddress, policyName, document); err != nil {
|
|
return fmt.Errorf("failed to sync runtime policy %q: %w", policyName, err)
|
|
}
|
|
}
|
|
|
|
m.runtimePolicyNames = make(map[string]struct{}, len(desiredPolicies))
|
|
for policyName := range desiredPolicies {
|
|
m.runtimePolicyNames[policyName] = struct{}{}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Initialize initializes the IAM manager with all components
|
|
func (m *IAMManager) Initialize(config *IAMConfig, filerAddressProvider func() string) error {
|
|
if config == nil {
|
|
return fmt.Errorf("config cannot be nil")
|
|
}
|
|
|
|
// Store the filer address provider function
|
|
m.filerAddressProvider = filerAddressProvider
|
|
|
|
// Initialize STS service
|
|
m.stsService = sts.NewSTSService()
|
|
if err := m.stsService.Initialize(config.STS); err != nil {
|
|
return fmt.Errorf("failed to initialize STS service: %w", err)
|
|
}
|
|
|
|
// CRITICAL SECURITY: Set trust policy validator to ensure proper role assumption validation
|
|
m.stsService.SetTrustPolicyValidator(m)
|
|
|
|
// Initialize policy engine
|
|
m.policyEngine = policy.NewPolicyEngine()
|
|
if err := m.policyEngine.InitializeWithProvider(config.Policy, m.filerAddressProvider); err != nil {
|
|
return fmt.Errorf("failed to initialize policy engine: %w", err)
|
|
}
|
|
|
|
// Initialize role store
|
|
roleStore, err := m.createRoleStoreWithProvider(config.Roles, m.filerAddressProvider)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize role store: %w", err)
|
|
}
|
|
m.roleStore = roleStore
|
|
|
|
// Initialize OIDC provider store and hydrate from static configuration so
|
|
// the read-only IAM API can return the same providers the STS service
|
|
// already accepts. Mutations will land in Phase 2b.
|
|
if err := m.initOIDCProviderStore(config); err != nil {
|
|
return fmt.Errorf("failed to initialize OIDC provider store: %w", err)
|
|
}
|
|
|
|
m.initialized = true
|
|
return nil
|
|
}
|
|
|
|
// initOIDCProviderStore creates the OIDC provider store and seeds it from the
|
|
// static STS provider configuration. The static path remains the bootstrap
|
|
// source: each enabled OIDC entry under STS.Providers is mirrored as an
|
|
// OIDCProviderRecord so the IAM API surfaces the same set the STS service
|
|
// validates against.
|
|
func (m *IAMManager) initOIDCProviderStore(config *IAMConfig) error {
|
|
store, err := m.createOIDCProviderStore(config.OIDCProviders)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.oidcProviderStore = store
|
|
|
|
if config.STS == nil {
|
|
return nil
|
|
}
|
|
for _, pc := range config.STS.Providers {
|
|
if pc == nil || !pc.Enabled || pc.Type != sts.ProviderTypeOIDC {
|
|
continue
|
|
}
|
|
issuer, _ := pc.Config["issuer"].(string)
|
|
if issuer == "" {
|
|
glog.Warningf("OIDC provider %s in static config has empty issuer; skipping mirror to store", pc.Name)
|
|
continue
|
|
}
|
|
accountID := ""
|
|
if config.STS != nil {
|
|
accountID = config.STS.AccountId
|
|
}
|
|
arn, err := DeriveOIDCProviderARN(accountID, issuer)
|
|
if err != nil {
|
|
glog.Warningf("derive ARN for static OIDC provider %s: %v", pc.Name, err)
|
|
continue
|
|
}
|
|
clientIDs := extractClientIDs(pc.Config)
|
|
ctx := context.Background()
|
|
// Preserve CreatedAt across reboots when a persistent store already
|
|
// has this provider — IAM's GetOpenIDConnectProvider response
|
|
// shouldn't shift its CreateDate every time the server restarts.
|
|
now := time.Now().UTC()
|
|
createdAt := now
|
|
if existing, err := store.GetProviderByARN(ctx, m.getFilerAddress(), arn); err == nil && existing != nil && !existing.CreatedAt.IsZero() {
|
|
createdAt = existing.CreatedAt
|
|
}
|
|
rec := &OIDCProviderRecord{
|
|
AccountID: accountID,
|
|
ARN: arn,
|
|
URL: issuer,
|
|
ClientIDs: clientIDs,
|
|
Thumbprints: extractStringList(pc.Config, "thumbprints"),
|
|
AllowedPrincipalTagKeys: extractStringList(pc.Config, "allowedPrincipalTagKeys"),
|
|
PolicyClaim: extractString(pc.Config, "policyClaim"),
|
|
CreatedAt: createdAt,
|
|
UpdatedAt: now,
|
|
}
|
|
if err := store.StoreProvider(ctx, m.getFilerAddress(), rec); err != nil {
|
|
glog.Warningf("mirror static OIDC provider %s into store: %v", pc.Name, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// refreshOIDCProvidersBestEffort calls RefreshOIDCProvidersFromStore and
|
|
// logs a warning on failure. The IAM API call has already succeeded by the
|
|
// time we get here, so a refresh failure must not surface to the caller —
|
|
// the worst case is that the local instance keeps the stale runtime view
|
|
// until a peer's metadata-subscribe event triggers another refresh.
|
|
func (m *IAMManager) refreshOIDCProvidersBestEffort(ctx context.Context, op, arn string) {
|
|
if err := m.RefreshOIDCProvidersFromStore(ctx); err != nil {
|
|
glog.Warningf("refresh OIDC providers after %s on %s: %v", op, arn, err)
|
|
}
|
|
}
|
|
|
|
// RefreshOIDCProvidersFromStore reloads every OIDCProviderRecord from the
|
|
// configured store and pushes the resulting runtime providers into the STS
|
|
// service so AssumeRoleWithWebIdentity sees the latest set without a
|
|
// restart. Safe to call when no store is configured (returns nil) and when
|
|
// the store is empty (clears the IAM-managed map). Records with empty URLs
|
|
// or invalid configuration are logged and skipped so a single bad entry
|
|
// does not stop the rest from refreshing.
|
|
func (m *IAMManager) RefreshOIDCProvidersFromStore(ctx context.Context) error {
|
|
if m.oidcProviderStore == nil || m.stsService == nil {
|
|
return nil
|
|
}
|
|
records, err := m.oidcProviderStore.ListProviders(ctx, m.getFilerAddress())
|
|
if err != nil {
|
|
return fmt.Errorf("list OIDC providers: %w", err)
|
|
}
|
|
byIssuer := make(map[string][]sts.ScopedOIDCProvider, len(records))
|
|
for _, rec := range records {
|
|
if rec == nil || rec.URL == "" {
|
|
continue
|
|
}
|
|
provider, err := buildOIDCProviderFromRecord(rec)
|
|
if err != nil {
|
|
glog.Warningf("skip refreshing OIDC provider %s: %v", rec.ARN, err)
|
|
continue
|
|
}
|
|
// Multiple records may share an issuer when each is scoped to a
|
|
// different account; STS picks the right one at validation time
|
|
// based on the role being assumed. See lookupOIDCProviderForAccount.
|
|
byIssuer[rec.URL] = append(byIssuer[rec.URL], sts.ScopedOIDCProvider{
|
|
AccountID: rec.AccountID,
|
|
Provider: provider,
|
|
})
|
|
}
|
|
m.stsService.SetIAMManagedOIDCProviders(byIssuer)
|
|
return nil
|
|
}
|
|
|
|
// buildOIDCProviderFromRecord turns a stored record into a runtime
|
|
// OIDCProvider. The provider name is the ARN so re-registration is
|
|
// idempotent and never collides with static-config entries.
|
|
func buildOIDCProviderFromRecord(rec *OIDCProviderRecord) (*oidc.OIDCProvider, error) {
|
|
if rec == nil {
|
|
return nil, fmt.Errorf("record cannot be nil")
|
|
}
|
|
cfg := &oidc.OIDCConfig{
|
|
Issuer: rec.URL,
|
|
ClientIDs: append([]string(nil), rec.ClientIDs...),
|
|
Thumbprints: append([]string(nil), rec.Thumbprints...),
|
|
}
|
|
provider := oidc.NewOIDCProvider(rec.ARN)
|
|
if err := provider.Initialize(cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
return provider, nil
|
|
}
|
|
|
|
// createOIDCProviderStore selects an OIDCProviderStore implementation. Defaults
|
|
// to memory; "filer" requires a filerAddressProvider to be configured.
|
|
func (m *IAMManager) createOIDCProviderStore(cfg *OIDCProviderStoreConfig) (OIDCProviderStore, error) {
|
|
if cfg == nil || cfg.StoreType == "" || cfg.StoreType == "memory" {
|
|
return NewMemoryOIDCProviderStore(), nil
|
|
}
|
|
if cfg.StoreType == "filer" {
|
|
return NewFilerOIDCProviderStore(cfg.StoreConfig, m.filerAddressProvider), nil
|
|
}
|
|
return nil, fmt.Errorf("unsupported OIDC provider store type: %s", cfg.StoreType)
|
|
}
|
|
|
|
// extractClientIDs reads a single clientId or a clientIds list from the
|
|
// provider's static config map. Mirrors the OIDCConfig schema.
|
|
func extractClientIDs(cfg map[string]interface{}) []string {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
if list, ok := cfg["clientIds"].([]interface{}); ok {
|
|
out := make([]string, 0, len(list))
|
|
for _, v := range list {
|
|
if s, ok := v.(string); ok && s != "" {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
if len(out) > 0 {
|
|
return out
|
|
}
|
|
}
|
|
if id, ok := cfg["clientId"].(string); ok && id != "" {
|
|
return []string{id}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// extractStringList reads a JSON string array out of the provider's static
|
|
// config map and returns the non-empty entries. Returns nil when the key is
|
|
// missing, the value is the wrong shape, or every entry is empty.
|
|
func extractStringList(cfg map[string]interface{}, key string) []string {
|
|
if cfg == nil {
|
|
return nil
|
|
}
|
|
list, ok := cfg[key].([]interface{})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
out := make([]string, 0, len(list))
|
|
for _, v := range list {
|
|
if s, ok := v.(string); ok && s != "" {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return nil
|
|
}
|
|
return out
|
|
}
|
|
|
|
// extractString reads a single string field from the provider's static
|
|
// config map; missing or non-string values produce "".
|
|
func extractString(cfg map[string]interface{}, key string) string {
|
|
if cfg == nil {
|
|
return ""
|
|
}
|
|
s, _ := cfg[key].(string)
|
|
return s
|
|
}
|
|
|
|
// getFilerAddress returns the current filer address using the provider function
|
|
func (m *IAMManager) getFilerAddress() string {
|
|
if m.filerAddressProvider != nil {
|
|
return m.filerAddressProvider()
|
|
}
|
|
return "" // Fallback to empty string if no provider is set
|
|
}
|
|
|
|
// createRoleStoreWithProvider creates a role store with a filer address provider function
|
|
func (m *IAMManager) createRoleStoreWithProvider(config *RoleStoreConfig, filerAddressProvider func() string) (RoleStore, error) {
|
|
if config == nil {
|
|
// Default to generic cached filer role store when no config provided
|
|
return NewGenericCachedRoleStore(nil, filerAddressProvider)
|
|
}
|
|
|
|
switch config.StoreType {
|
|
case "", "filer":
|
|
// Check if caching is explicitly disabled
|
|
if config.StoreConfig != nil {
|
|
if noCache, ok := config.StoreConfig["noCache"].(bool); ok && noCache {
|
|
return NewFilerRoleStore(config.StoreConfig, filerAddressProvider)
|
|
}
|
|
}
|
|
// Default to generic cached filer store for better performance
|
|
return NewGenericCachedRoleStore(config.StoreConfig, filerAddressProvider)
|
|
case "cached-filer", "generic-cached":
|
|
return NewGenericCachedRoleStore(config.StoreConfig, filerAddressProvider)
|
|
case "memory":
|
|
return NewMemoryRoleStore(), nil
|
|
default:
|
|
return nil, fmt.Errorf("unsupported role store type: %s", config.StoreType)
|
|
}
|
|
}
|
|
|
|
// RegisterIdentityProvider registers an identity provider
|
|
func (m *IAMManager) RegisterIdentityProvider(provider providers.IdentityProvider) error {
|
|
if !m.initialized {
|
|
return fmt.Errorf("IAM manager not initialized")
|
|
}
|
|
|
|
return m.stsService.RegisterProvider(provider)
|
|
}
|
|
|
|
// CreatePolicy creates a new policy
|
|
func (m *IAMManager) CreatePolicy(ctx context.Context, filerAddress string, name string, policyDoc *policy.PolicyDocument) error {
|
|
if !m.initialized {
|
|
return fmt.Errorf("IAM manager not initialized")
|
|
}
|
|
|
|
return m.policyEngine.AddPolicy(filerAddress, name, policyDoc)
|
|
}
|
|
|
|
// CreateRole creates a new role with trust policy and attached policies
|
|
func (m *IAMManager) CreateRole(ctx context.Context, filerAddress string, roleName string, roleDef *RoleDefinition) error {
|
|
if !m.initialized {
|
|
return fmt.Errorf("IAM manager not initialized")
|
|
}
|
|
|
|
if roleName == "" {
|
|
return fmt.Errorf("role name cannot be empty")
|
|
}
|
|
|
|
if roleDef == nil {
|
|
return fmt.Errorf("role definition cannot be nil")
|
|
}
|
|
|
|
// Set role ARN if not provided
|
|
if roleDef.RoleArn == "" {
|
|
roleDef.RoleArn = fmt.Sprintf("arn:aws:iam::role/%s", roleName)
|
|
}
|
|
|
|
// Validate trust policy
|
|
if roleDef.TrustPolicy != nil {
|
|
if err := policy.ValidateTrustPolicyDocument(roleDef.TrustPolicy); err != nil {
|
|
return fmt.Errorf("invalid trust policy: %w", err)
|
|
}
|
|
}
|
|
|
|
// Validate per-role MaxSessionDuration if specified. AWS bounds: 1h..12h.
|
|
if roleDef.MaxSessionDuration != 0 {
|
|
if roleDef.MaxSessionDuration < 3600 || roleDef.MaxSessionDuration > 43200 {
|
|
return fmt.Errorf("MaxSessionDuration must be between 3600 and 43200 seconds, got %d", roleDef.MaxSessionDuration)
|
|
}
|
|
}
|
|
|
|
// Store role definition
|
|
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 {
|
|
return fmt.Errorf("IAM manager not initialized")
|
|
}
|
|
|
|
if bucketName == "" {
|
|
return fmt.Errorf("bucket name cannot be empty")
|
|
}
|
|
|
|
// Parse the policy document handled by the IAM policy engine
|
|
var policyDoc policy.PolicyDocument
|
|
if err := json.Unmarshal(policyJSON, &policyDoc); err != nil {
|
|
return fmt.Errorf("invalid policy JSON: %w", err)
|
|
}
|
|
|
|
// Store the policy with a special prefix to distinguish from IAM policies
|
|
policyName := "bucket-policy:" + bucketName
|
|
return m.policyEngine.AddPolicy(m.getFilerAddress(), policyName, &policyDoc)
|
|
}
|
|
|
|
// AssumeRoleWithWebIdentity assumes a role using web identity (OIDC)
|
|
func (m *IAMManager) AssumeRoleWithWebIdentity(ctx context.Context, request *sts.AssumeRoleWithWebIdentityRequest) (*sts.AssumeRoleResponse, error) {
|
|
if !m.initialized {
|
|
return nil, fmt.Errorf("IAM manager not initialized")
|
|
}
|
|
|
|
// Claim-based mode bypasses role lookup and trust policy entirely; the
|
|
// STS service handles the policy resolution from the JWT itself. Account
|
|
// scoping still applies but at the provider-resolution layer in Phase 3c
|
|
// (we'll plug that in once a multi-account assume path lands).
|
|
if sts.IsClaimBasedPolicyRoleArn(request.RoleArn) {
|
|
return m.stsService.AssumeRoleWithWebIdentity(ctx, request)
|
|
}
|
|
|
|
// Extract role name from ARN
|
|
roleName := utils.ExtractRoleNameFromArn(request.RoleArn)
|
|
|
|
// Get role definition
|
|
roleDef, err := m.roleStore.GetRole(ctx, m.getFilerAddress(), roleName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("role not found: %s", roleName)
|
|
}
|
|
|
|
// Account scoping: when the role lives in account A, the OIDC provider
|
|
// validating the token must be either global (AccountID="") or also live
|
|
// in account A. Skip when we can't resolve account context; fall through
|
|
// to the existing trust-policy enforcement.
|
|
if err := m.enforceProviderAccountScope(ctx, request); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Validate trust policy before allowing STS to assume the role
|
|
if err := m.validateTrustPolicyForWebIdentity(ctx, roleDef, request.WebIdentityToken, request.DurationSeconds); err != nil {
|
|
return nil, fmt.Errorf("trust policy validation failed: %w", err)
|
|
}
|
|
|
|
// Apply role-level MaxSessionDuration cap. The STS service still applies
|
|
// the global MaxSessionLength and the source-token-expiry cap on top of
|
|
// this; per-role takes precedence whenever it is the tightest bound.
|
|
request.DurationSeconds = capDurationByRole(request.DurationSeconds, roleDef.MaxSessionDuration)
|
|
|
|
// Use STS service to assume the role
|
|
return m.stsService.AssumeRoleWithWebIdentity(ctx, request)
|
|
}
|
|
|
|
// enforceProviderAccountScope checks that the OIDC provider matching the
|
|
// token's issuer is registered in either the role's account or as a global
|
|
// (account-less) provider. Returns nil when no provider store is configured
|
|
// (preserves the static-config-only path) or when the issuer is not known to
|
|
// the store (the existing STS-layer issuer→provider map handles that case
|
|
// during validation).
|
|
func (m *IAMManager) enforceProviderAccountScope(ctx context.Context, request *sts.AssumeRoleWithWebIdentityRequest) error {
|
|
if m.oidcProviderStore == nil {
|
|
return nil
|
|
}
|
|
roleAccount := utils.ParseRoleARN(request.RoleArn).AccountID
|
|
if roleAccount == "" {
|
|
// Legacy ARN form (no account): nothing to enforce.
|
|
return nil
|
|
}
|
|
issuer, err := extractIssuerFromJWT(request.WebIdentityToken)
|
|
if err != nil {
|
|
return nil // signature validation will reject, no need to fail twice
|
|
}
|
|
// Look for a provider that is either global or scoped to the role's
|
|
// account. Multiple providers may share an issuer (e.g. one global plus
|
|
// one per tenant), and GetProviderByIssuer returns the first match
|
|
// arbitrarily — using it here would falsely reject a valid request
|
|
// whenever the wrong record happens to come back first.
|
|
if _, err := m.oidcProviderStore.GetProviderByIssuerAndAccount(ctx, m.getFilerAddress(), issuer, roleAccount); err == nil {
|
|
return nil
|
|
}
|
|
// No allowed match. Distinguish "issuer entirely unknown" (let the STS
|
|
// layer reject with the existing not-registered error) from "issuer is
|
|
// registered but only in a different account" (surface a precise error
|
|
// so the operator knows the call was cross-account).
|
|
if other, err := m.oidcProviderStore.GetProviderByIssuer(ctx, m.getFilerAddress(), issuer); err == nil {
|
|
return fmt.Errorf("OIDC provider for issuer %s is registered in account %s; cannot be used to assume a role in account %s", issuer, other.AccountID, roleAccount)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// extractIssuerFromJWT returns the iss claim of a JWT without verifying its
|
|
// signature. Safe here because the caller still goes through full signature
|
|
// + issuer validation in the STS service; this is purely for routing.
|
|
func extractIssuerFromJWT(token string) (string, error) {
|
|
parser := new(jwt.Parser)
|
|
parsed, _, err := parser.ParseUnverified(token, jwt.MapClaims{})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
claims, ok := parsed.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
return "", fmt.Errorf("invalid claims")
|
|
}
|
|
iss, _ := claims["iss"].(string)
|
|
if iss == "" {
|
|
return "", fmt.Errorf("token has no iss claim")
|
|
}
|
|
return iss, nil
|
|
}
|
|
|
|
// capDurationByRole returns the requested duration clamped to the role's
|
|
// MaxSessionDuration. A nil requested duration is left nil so the STS
|
|
// service's calculateSessionDuration applies the global default (typically
|
|
// 1 hour) — substituting the role's max here would silently mint a 12h
|
|
// session for any caller who omitted DurationSeconds, which AWS does not
|
|
// do. The role-max upper bound still applies in the downstream cap chain
|
|
// once the request has a concrete duration.
|
|
func capDurationByRole(requested *int64, roleMax int64) *int64 {
|
|
if roleMax <= 0 || requested == nil {
|
|
return requested
|
|
}
|
|
if *requested > roleMax {
|
|
v := roleMax
|
|
return &v
|
|
}
|
|
return requested
|
|
}
|
|
|
|
// AssumeRoleWithCredentials assumes a role using credentials (LDAP)
|
|
func (m *IAMManager) AssumeRoleWithCredentials(ctx context.Context, request *sts.AssumeRoleWithCredentialsRequest) (*sts.AssumeRoleResponse, error) {
|
|
if !m.initialized {
|
|
return nil, fmt.Errorf("IAM manager not initialized")
|
|
}
|
|
|
|
// Extract role name from ARN
|
|
roleName := utils.ExtractRoleNameFromArn(request.RoleArn)
|
|
|
|
// Get role definition
|
|
roleDef, err := m.roleStore.GetRole(ctx, m.getFilerAddress(), roleName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("role not found: %s", roleName)
|
|
}
|
|
|
|
// Validate trust policy
|
|
if err := m.validateTrustPolicyForCredentials(ctx, roleDef, request); err != nil {
|
|
return nil, fmt.Errorf("trust policy validation failed: %w", err)
|
|
}
|
|
|
|
// Apply role-level MaxSessionDuration cap.
|
|
request.DurationSeconds = capDurationByRole(request.DurationSeconds, roleDef.MaxSessionDuration)
|
|
|
|
// Use STS service to assume the role
|
|
return m.stsService.AssumeRoleWithCredentials(ctx, request)
|
|
}
|
|
|
|
// IsActionAllowed checks if a principal is allowed to perform an action on a resource
|
|
func (m *IAMManager) IsActionAllowed(ctx context.Context, request *ActionRequest) (bool, error) {
|
|
if !m.initialized {
|
|
return false, fmt.Errorf("IAM manager not initialized")
|
|
}
|
|
|
|
// Validate session token if present
|
|
// We always try to validate with the internal STS service first if it's a SeaweedFS token.
|
|
// This ensures that session policies embedded in the token are correctly extracted and enforced.
|
|
var sessionInfo *sts.SessionInfo
|
|
if request.SessionToken != "" {
|
|
// Parse unverified to check issuer
|
|
parsed, _, err := new(jwt.Parser).ParseUnverified(request.SessionToken, jwt.MapClaims{})
|
|
isInternal := false
|
|
if err == nil {
|
|
if claims, ok := parsed.Claims.(jwt.MapClaims); ok {
|
|
if issuer, ok := claims["iss"].(string); ok && m.stsService != nil && m.stsService.Config != nil {
|
|
if issuer == m.stsService.Config.Issuer {
|
|
isInternal = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if isInternal || !isOIDCToken(request.SessionToken) {
|
|
var err error
|
|
sessionInfo, err = m.stsService.ValidateSessionToken(ctx, request.SessionToken)
|
|
if err != nil {
|
|
return false, fmt.Errorf("invalid session: %w", err)
|
|
}
|
|
// Reject sessions whose JTI has been added to the revocation
|
|
// blocklist. SessionId == JTI for STS-issued tokens; this lookup
|
|
// is hot, so the store implementation must be O(1).
|
|
if sessionInfo != nil && sessionInfo.SessionId != "" {
|
|
revoked, rerr := m.IsSessionRevoked(ctx, sessionInfo.SessionId)
|
|
if rerr != nil {
|
|
return false, fmt.Errorf("revocation check failed: %w", rerr)
|
|
}
|
|
if revoked {
|
|
return false, fmt.Errorf("session has been revoked")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create evaluation context
|
|
evalCtx := &policy.EvaluationContext{
|
|
Principal: request.Principal,
|
|
Action: request.Action,
|
|
Resource: request.Resource,
|
|
RequestContext: request.RequestContext,
|
|
}
|
|
|
|
// Ensure RequestContext exists and populate with principal info
|
|
if evalCtx.RequestContext == nil {
|
|
evalCtx.RequestContext = make(map[string]interface{})
|
|
}
|
|
// Add principal to context for policy matching
|
|
// The PolicyEngine checks RequestContext["principal"] or RequestContext["aws:PrincipalArn"]
|
|
evalCtx.RequestContext["principal"] = request.Principal
|
|
evalCtx.RequestContext["aws:PrincipalArn"] = request.Principal // AWS standard key
|
|
|
|
// Check if this is an admin request - bypass policy evaluation if so
|
|
// This mirrors the logic in auth_signature_v4.go but applies it at authorization time
|
|
isAdmin := false
|
|
if request.RequestContext != nil {
|
|
if val, ok := request.RequestContext["is_admin"].(bool); ok && val {
|
|
isAdmin = true
|
|
}
|
|
// Print full request context for debugging
|
|
}
|
|
|
|
// Parse principal ARN to extract details for context variables (e.g. ${aws:username})
|
|
arnInfo := utils.ParsePrincipalARN(request.Principal)
|
|
if arnInfo.RoleName != "" {
|
|
// For assumed roles, AWS docs say aws:username IS the role name.
|
|
// However, for user isolation in these tests, we typically map the session name (the user who assumed the role) to aws:username.
|
|
// arn:aws:sts::account:assumed-role/RoleName/SessionName
|
|
awsUsername := arnInfo.RoleName
|
|
if idx := strings.LastIndex(request.Principal, "/"); idx != -1 && idx < len(request.Principal)-1 {
|
|
awsUsername = request.Principal[idx+1:]
|
|
}
|
|
|
|
evalCtx.RequestContext["aws:username"] = awsUsername
|
|
evalCtx.RequestContext["aws:userid"] = arnInfo.RoleName
|
|
} else if userName := utils.ExtractUserNameFromPrincipal(request.Principal); userName != "" {
|
|
evalCtx.RequestContext["aws:username"] = userName
|
|
evalCtx.RequestContext["aws:userid"] = userName
|
|
}
|
|
if arnInfo.AccountID != "" {
|
|
evalCtx.RequestContext["aws:PrincipalAccount"] = arnInfo.AccountID
|
|
}
|
|
|
|
// Determine if there is a bucket policy to evaluate
|
|
var bucketPolicyName string
|
|
if strings.HasPrefix(request.Resource, "arn:aws:s3:::") {
|
|
resourcePath := request.Resource[13:] // remove "arn:aws:s3:::"
|
|
parts := strings.SplitN(resourcePath, "/", 2)
|
|
if len(parts) > 0 && parts[0] != "" {
|
|
bucketPolicyName = "bucket-policy:" + parts[0]
|
|
}
|
|
}
|
|
|
|
var baseResult *policy.EvaluationResult
|
|
var err error
|
|
// hasManagedSubject is true once we've resolved the principal to a registered
|
|
// IAM user or role (or the caller has supplied PolicyNames directly). For a
|
|
// managed subject, "no matching statement" must deny — the DefaultEffect=Allow
|
|
// fallback is only meant for the unmanaged zero-config startup case.
|
|
hasManagedSubject := false
|
|
|
|
if isAdmin {
|
|
// Admin always has base access allowed
|
|
baseResult = &policy.EvaluationResult{Effect: policy.EffectAllow}
|
|
} else {
|
|
policies := request.PolicyNames
|
|
if len(policies) > 0 {
|
|
hasManagedSubject = true
|
|
}
|
|
if len(policies) == 0 {
|
|
// Extract role name from principal ARN
|
|
roleName := utils.ExtractRoleNameFromPrincipal(request.Principal)
|
|
if roleName == "" {
|
|
userName := utils.ExtractUserNameFromPrincipal(request.Principal)
|
|
if userName == "" {
|
|
return false, fmt.Errorf("could not extract role from principal: %s", request.Principal)
|
|
}
|
|
if m.userStore == nil {
|
|
return false, fmt.Errorf("user store unavailable for principal: %s", request.Principal)
|
|
}
|
|
user, err := m.userStore.GetUser(ctx, userName)
|
|
if err != nil || user == nil {
|
|
return false, fmt.Errorf("user not found for principal: %s (user=%s)", request.Principal, userName)
|
|
}
|
|
hasManagedSubject = true
|
|
policies = user.GetPolicyNames()
|
|
} else {
|
|
// Get role definition
|
|
roleDef, err := m.roleStore.GetRole(ctx, m.getFilerAddress(), roleName)
|
|
if err != nil {
|
|
return false, fmt.Errorf("role not found: %s", roleName)
|
|
}
|
|
|
|
hasManagedSubject = true
|
|
policies = roleDef.AttachedPolicies
|
|
}
|
|
}
|
|
if bucketPolicyName != "" {
|
|
// Enforce an upper bound on the number of policies to avoid excessive allocations
|
|
if len(policies) >= maxPoliciesForEvaluation {
|
|
return false, fmt.Errorf("too many policies for evaluation: %d >= %d", len(policies), maxPoliciesForEvaluation)
|
|
}
|
|
// Create a new slice to avoid modifying the original and append the bucket policy
|
|
copied := make([]string, len(policies))
|
|
copy(copied, policies)
|
|
policies = append(copied, bucketPolicyName)
|
|
}
|
|
|
|
baseResult, err = m.policyEngine.Evaluate(ctx, "", evalCtx, policies)
|
|
if err != nil {
|
|
return false, fmt.Errorf("policy evaluation failed: %w", err)
|
|
}
|
|
}
|
|
|
|
// Base policy must allow; if it doesn't, deny immediately (session policy can only further restrict)
|
|
if baseResult.Effect != policy.EffectAllow {
|
|
return false, nil
|
|
}
|
|
|
|
// Zero-config IAM uses DefaultEffect=Allow to preserve open-by-default behavior
|
|
// for requests without any subject policies. Once we resolve the principal to
|
|
// a registered IAM user or role (or the caller hands us policy names),
|
|
// "no matching statement" must fall back to deny — otherwise a freshly
|
|
// created user with zero policies would inherit full access.
|
|
if hasManagedSubject && len(baseResult.MatchingStatements) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
// If there's a session policy, it must also allow the action
|
|
if sessionInfo != nil && sessionInfo.SessionPolicy != "" {
|
|
var sessionPolicy policy.PolicyDocument
|
|
if err := json.Unmarshal([]byte(sessionInfo.SessionPolicy), &sessionPolicy); err != nil {
|
|
return false, fmt.Errorf("invalid session policy JSON: %w", err)
|
|
}
|
|
if err := policy.ValidatePolicyDocument(&sessionPolicy); err != nil {
|
|
return false, fmt.Errorf("invalid session policy document: %w", err)
|
|
}
|
|
sessionResult, err := m.policyEngine.EvaluatePolicyDocument(ctx, evalCtx, "session-policy", &sessionPolicy, policy.EffectDeny)
|
|
if err != nil {
|
|
return false, fmt.Errorf("session policy evaluation failed: %w", err)
|
|
}
|
|
if sessionResult.Effect != policy.EffectAllow {
|
|
// Session policy does not allow this action
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// IsPrincipalActionExplicitlyDenied reports whether the action on the resource is
|
|
// explicitly denied for the principal by either the named policies or, for a
|
|
// chained STS caller, the inline session policy carried by sessionToken. Unlike
|
|
// IsActionAllowed it does not require an allow — the absence of a matching
|
|
// statement is not a denial. Used to enforce AWS deny-always-wins when the allow
|
|
// is granted elsewhere (e.g. a role trust policy for sts:AssumeRole).
|
|
//
|
|
// A chained session that fails validation or has been revoked yields an error so
|
|
// callers fail closed. Raw OIDC tokens are skipped here — they are validated on
|
|
// the JWT path, not by the STS service.
|
|
func (m *IAMManager) IsPrincipalActionExplicitlyDenied(ctx context.Context, principal, action, resource string, policyNames []string, sessionToken string, requestContext map[string]interface{}) (bool, error) {
|
|
if !m.initialized {
|
|
return false, fmt.Errorf("IAM manager not initialized")
|
|
}
|
|
|
|
if requestContext == nil {
|
|
requestContext = make(map[string]interface{})
|
|
}
|
|
requestContext["principal"] = principal
|
|
requestContext["aws:PrincipalArn"] = principal
|
|
|
|
evalCtx := &policy.EvaluationContext{
|
|
Principal: principal,
|
|
Action: action,
|
|
Resource: resource,
|
|
RequestContext: requestContext,
|
|
}
|
|
|
|
// Base policies: the caller's attached identity policies, or for a chained
|
|
// caller the assumed role's attached policies.
|
|
if len(policyNames) > 0 {
|
|
result, err := m.policyEngine.Evaluate(ctx, "", evalCtx, policyNames)
|
|
if err != nil {
|
|
return false, fmt.Errorf("policy evaluation failed: %w", err)
|
|
}
|
|
if hasExplicitDeny(result.MatchingStatements) {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
// A chained STS caller's session restricts what it may do. Skip raw OIDC
|
|
// tokens (validated on the JWT path); for our own session tokens, reject a
|
|
// revoked session and honor an explicit Deny in the inline session policy.
|
|
if sessionToken != "" && m.stsService != nil && !isOIDCToken(sessionToken) {
|
|
sessionInfo, err := m.stsService.ValidateSessionToken(ctx, sessionToken)
|
|
if err != nil {
|
|
return false, fmt.Errorf("session validation failed: %w", err)
|
|
}
|
|
if sessionInfo != nil && sessionInfo.SessionId != "" {
|
|
revoked, rerr := m.IsSessionRevoked(ctx, sessionInfo.SessionId)
|
|
if rerr != nil {
|
|
return false, fmt.Errorf("revocation check failed: %w", rerr)
|
|
}
|
|
if revoked {
|
|
return false, fmt.Errorf("session has been revoked")
|
|
}
|
|
}
|
|
if sessionInfo != nil && sessionInfo.SessionPolicy != "" {
|
|
var sessionPolicy policy.PolicyDocument
|
|
if err := json.Unmarshal([]byte(sessionInfo.SessionPolicy), &sessionPolicy); err != nil {
|
|
return false, fmt.Errorf("invalid session policy JSON: %w", err)
|
|
}
|
|
result, err := m.policyEngine.EvaluatePolicyDocument(ctx, evalCtx, "session-policy", &sessionPolicy, policy.EffectDeny)
|
|
if err != nil {
|
|
return false, fmt.Errorf("session policy evaluation failed: %w", err)
|
|
}
|
|
if hasExplicitDeny(result.MatchingStatements) {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// hasExplicitDeny reports whether any matched statement is a Deny.
|
|
func hasExplicitDeny(matches []policy.StatementMatch) bool {
|
|
for _, stmt := range matches {
|
|
if stmt.Effect == policy.EffectDeny {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ValidateTrustPolicy validates if a principal can assume a role (for testing)
|
|
func (m *IAMManager) ValidateTrustPolicy(ctx context.Context, roleArn, provider, userID string) bool {
|
|
roleName := utils.ExtractRoleNameFromArn(roleArn)
|
|
roleDef, err := m.roleStore.GetRole(ctx, m.getFilerAddress(), roleName)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// Simple validation based on provider in trust policy
|
|
if roleDef.TrustPolicy != nil {
|
|
for _, statement := range roleDef.TrustPolicy.Statement {
|
|
if statement.Effect == "Allow" {
|
|
if principal, ok := statement.Principal.(map[string]interface{}); ok {
|
|
if federated, ok := principal["Federated"].(string); ok {
|
|
// For OIDC, check against issuer URL
|
|
if provider == "oidc" && federated == "test-oidc" {
|
|
return true
|
|
}
|
|
// For LDAP, check against test-ldap
|
|
if provider == "ldap" && federated == "test-ldap" {
|
|
return true
|
|
}
|
|
// Also check for wildcard
|
|
if federated == "*" {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// validateTrustPolicyForWebIdentity validates trust policy for OIDC assumption
|
|
func (m *IAMManager) validateTrustPolicyForWebIdentity(ctx context.Context, roleDef *RoleDefinition, webIdentityToken string, durationSeconds *int64) error {
|
|
if roleDef.TrustPolicy == nil {
|
|
return fmt.Errorf("role has no trust policy")
|
|
}
|
|
|
|
// Create evaluation context for trust policy validation
|
|
requestContext := make(map[string]interface{})
|
|
|
|
// Try to parse as JWT first, fallback to mock token handling
|
|
tokenClaims, err := parseJWTTokenForTrustPolicy(webIdentityToken)
|
|
if err != nil {
|
|
// If JWT parsing fails, this might be a mock token (like "valid-oidc-token")
|
|
// For mock tokens, we'll use default values that match the trust policy expectations
|
|
requestContext["aws:FederatedProvider"] = "test-oidc"
|
|
requestContext["oidc:iss"] = "test-oidc"
|
|
// This ensures aws:userid key is populated even for mock tokens if needed
|
|
requestContext["aws:userid"] = "mock-user"
|
|
requestContext["oidc:sub"] = "mock-user"
|
|
} else {
|
|
// Add standard context values from JWT claims that trust policies might check
|
|
// See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#condition-keys-web-identity-federation
|
|
|
|
// The issuer is the federated provider for OIDC
|
|
if iss, ok := tokenClaims["iss"].(string); ok {
|
|
// Default to issuer URL
|
|
requestContext["aws:FederatedProvider"] = iss
|
|
requestContext["oidc:iss"] = iss
|
|
|
|
// Try to resolve provider name from issuer for better policy matching
|
|
// This allows policies to reference the provider name (e.g. "keycloak") instead of the full issuer URL
|
|
if m.stsService != nil {
|
|
for name, provider := range m.stsService.GetProviders() {
|
|
if oidcProvider, ok := provider.(interface{ GetIssuer() string }); ok {
|
|
confIssuer := oidcProvider.GetIssuer()
|
|
|
|
if confIssuer == iss {
|
|
requestContext["aws:FederatedProvider"] = name
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if sub, ok := tokenClaims["sub"].(string); ok {
|
|
requestContext["oidc:sub"] = sub
|
|
// Map subject to aws:userid as well for compatibility
|
|
requestContext["aws:userid"] = sub
|
|
}
|
|
if aud, ok := tokenClaims["aud"].(string); ok {
|
|
requestContext["oidc:aud"] = aud
|
|
}
|
|
// Custom claims can be prefixed if needed, but for "be 100% compatible with AWS",
|
|
// we should rely on standard OIDC claims.
|
|
|
|
// Add all other claims with oidc: prefix to support custom claims in trust policies
|
|
// This enables checking claims like "oidc:roles", "oidc:groups", "oidc:email", etc.
|
|
for k, v := range tokenClaims {
|
|
// Skip claims we've already handled explicitly or shouldn't expose
|
|
if k == "iss" || k == "sub" || k == "aud" {
|
|
continue
|
|
}
|
|
|
|
// Add with oidc: prefix
|
|
requestContext["oidc:"+k] = v
|
|
}
|
|
}
|
|
|
|
// Add DurationSeconds to context if provided
|
|
if durationSeconds != nil {
|
|
requestContext["sts:DurationSeconds"] = *durationSeconds
|
|
}
|
|
|
|
// Create evaluation context for trust policy
|
|
evalCtx := &policy.EvaluationContext{
|
|
Principal: "web-identity-user", // Placeholder principal for trust policy evaluation
|
|
Action: "sts:AssumeRoleWithWebIdentity",
|
|
Resource: roleDef.RoleArn,
|
|
RequestContext: requestContext,
|
|
}
|
|
|
|
// Evaluate the trust policy directly
|
|
if !m.evaluateTrustPolicy(roleDef.TrustPolicy, evalCtx) {
|
|
return fmt.Errorf("trust policy denies web identity assumption")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateTrustPolicyForCredentials validates trust policy for credential assumption
|
|
func (m *IAMManager) validateTrustPolicyForCredentials(ctx context.Context, roleDef *RoleDefinition, request *sts.AssumeRoleWithCredentialsRequest) error {
|
|
if roleDef.TrustPolicy == nil {
|
|
return fmt.Errorf("role has no trust policy")
|
|
}
|
|
|
|
// Check if trust policy allows credential assumption for the specific provider
|
|
for _, statement := range roleDef.TrustPolicy.Statement {
|
|
if statement.Effect == "Allow" {
|
|
for _, action := range statement.Action {
|
|
if action == "sts:AssumeRoleWithCredentials" {
|
|
if principal, ok := statement.Principal.(map[string]interface{}); ok {
|
|
if federated, ok := principal["Federated"].(string); ok {
|
|
if federated == request.ProviderName {
|
|
return nil // Allow
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("trust policy does not allow credential assumption for provider: %s", request.ProviderName)
|
|
}
|
|
|
|
// Helper functions
|
|
|
|
// ExpireSessionForTesting manually expires a session for testing purposes
|
|
func (m *IAMManager) ExpireSessionForTesting(ctx context.Context, sessionToken string) error {
|
|
if !m.initialized {
|
|
return fmt.Errorf("IAM manager not initialized")
|
|
}
|
|
|
|
return m.stsService.ExpireSessionForTesting(ctx, sessionToken)
|
|
}
|
|
|
|
// GetPoliciesForUser returns the policy names attached to an IAM user.
|
|
// Returns an error if the user store is not configured or the lookup fails,
|
|
// so callers can fail closed on policy-resolution failures.
|
|
func (m *IAMManager) GetPoliciesForUser(ctx context.Context, username string) ([]string, error) {
|
|
if m.userStore == nil {
|
|
return nil, fmt.Errorf("user store not configured")
|
|
}
|
|
user, err := m.userStore.GetUser(ctx, username)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to look up user %q: %w", username, err)
|
|
}
|
|
if user == nil {
|
|
return nil, nil
|
|
}
|
|
return user.PolicyNames, nil
|
|
}
|
|
|
|
// GetSTSService returns the STS service instance
|
|
func (m *IAMManager) GetSTSService() *sts.STSService {
|
|
return m.stsService
|
|
}
|
|
|
|
// DefaultAllow returns whether the default effect is Allow
|
|
func (m *IAMManager) DefaultAllow() bool {
|
|
if !m.initialized || m.policyEngine == nil {
|
|
return true // Default to true if not initialized
|
|
}
|
|
return m.policyEngine.DefaultAllow()
|
|
}
|
|
|
|
// parseJWTTokenForTrustPolicy parses a JWT token to extract claims for trust policy evaluation
|
|
func parseJWTTokenForTrustPolicy(tokenString string) (map[string]interface{}, error) {
|
|
// Simple JWT parsing without verification (for trust policy context only)
|
|
// In production, this should use proper JWT parsing with signature verification
|
|
parts := strings.Split(tokenString, ".")
|
|
if len(parts) != 3 {
|
|
return nil, fmt.Errorf("invalid JWT format")
|
|
}
|
|
|
|
// Decode the payload (second part)
|
|
payload := parts[1]
|
|
// Add padding if needed
|
|
for len(payload)%4 != 0 {
|
|
payload += "="
|
|
}
|
|
|
|
decoded, err := base64.URLEncoding.DecodeString(payload)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode JWT payload: %w", err)
|
|
}
|
|
|
|
var claims map[string]interface{}
|
|
if err := json.Unmarshal(decoded, &claims); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal JWT claims: %w", err)
|
|
}
|
|
|
|
return claims, nil
|
|
}
|
|
|
|
// evaluateTrustPolicy evaluates a trust policy against the evaluation context
|
|
// Now delegates to PolicyEngine for unified policy evaluation
|
|
func (m *IAMManager) evaluateTrustPolicy(trustPolicy *policy.PolicyDocument, evalCtx *policy.EvaluationContext) bool {
|
|
if trustPolicy == nil {
|
|
return false
|
|
}
|
|
|
|
// Use the PolicyEngine to evaluate the trust policy
|
|
// The PolicyEngine now handles Principal, Action, Resource, and Condition matching
|
|
result, err := m.policyEngine.EvaluateTrustPolicy(context.Background(), trustPolicy, evalCtx)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return result.Effect == policy.EffectAllow
|
|
}
|
|
|
|
// evaluateTrustPolicyConditions and evaluatePrincipalValue have been removed
|
|
// Trust policy evaluation is now handled entirely by PolicyEngine.EvaluateTrustPolicy()
|
|
|
|
// isOIDCToken checks if a token is an OIDC JWT token (vs STS session token)
|
|
func isOIDCToken(token string) bool {
|
|
// JWT tokens have three parts separated by dots and start with base64-encoded JSON
|
|
parts := strings.Split(token, ".")
|
|
if len(parts) != 3 {
|
|
return false
|
|
}
|
|
|
|
// JWT tokens typically start with "eyJ" (base64 encoded JSON starting with "{")
|
|
if !strings.HasPrefix(token, "eyJ") {
|
|
return false
|
|
}
|
|
|
|
parsed, _, err := new(jwt.Parser).ParseUnverified(token, jwt.MapClaims{})
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
claims, ok := parsed.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
if typ, ok := claims["typ"].(string); ok && typ == sts.TokenTypeSession {
|
|
return false
|
|
}
|
|
if typ, ok := claims[sts.JWTClaimTokenType].(string); ok && typ == sts.TokenTypeSession {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// TrustPolicyValidator interface implementation
|
|
// These methods allow the IAMManager to serve as the trust policy validator for the STS service
|
|
|
|
// ValidateTrustPolicyForWebIdentity implements the TrustPolicyValidator interface
|
|
func (m *IAMManager) ValidateTrustPolicyForWebIdentity(ctx context.Context, roleArn string, webIdentityToken string, durationSeconds *int64) error {
|
|
if !m.initialized {
|
|
return fmt.Errorf("IAM manager not initialized")
|
|
}
|
|
|
|
// Extract role name from ARN
|
|
roleName := utils.ExtractRoleNameFromArn(roleArn)
|
|
|
|
// Get role definition
|
|
roleDef, err := m.roleStore.GetRole(ctx, m.getFilerAddress(), roleName)
|
|
if err != nil {
|
|
return fmt.Errorf("role not found: %s", roleName)
|
|
}
|
|
|
|
// Use existing trust policy validation logic
|
|
return m.validateTrustPolicyForWebIdentity(ctx, roleDef, webIdentityToken, durationSeconds)
|
|
}
|
|
|
|
// ValidateTrustPolicyForCredentials implements the TrustPolicyValidator interface
|
|
func (m *IAMManager) ValidateTrustPolicyForCredentials(ctx context.Context, roleArn string, identity *providers.ExternalIdentity) error {
|
|
if !m.initialized {
|
|
return fmt.Errorf("IAM manager not initialized")
|
|
}
|
|
|
|
// Extract role name from ARN
|
|
roleName := utils.ExtractRoleNameFromArn(roleArn)
|
|
|
|
// Get role definition
|
|
roleDef, err := m.roleStore.GetRole(ctx, m.getFilerAddress(), roleName)
|
|
if err != nil {
|
|
return fmt.Errorf("role not found: %s", roleName)
|
|
}
|
|
|
|
// For credentials, we need to create a mock request to reuse existing validation
|
|
// This is a bit of a hack, but it allows us to reuse the existing logic
|
|
mockRequest := &sts.AssumeRoleWithCredentialsRequest{
|
|
ProviderName: identity.Provider, // Use the provider name from the identity
|
|
}
|
|
|
|
// Use existing trust policy validation logic
|
|
return m.validateTrustPolicyForCredentials(ctx, roleDef, mockRequest)
|
|
}
|