diff options
Diffstat (limited to 'weed/iam/providers/provider.go')
| -rw-r--r-- | weed/iam/providers/provider.go | 227 |
1 files changed, 227 insertions, 0 deletions
diff --git a/weed/iam/providers/provider.go b/weed/iam/providers/provider.go new file mode 100644 index 000000000..5c1deb03d --- /dev/null +++ b/weed/iam/providers/provider.go @@ -0,0 +1,227 @@ +package providers + +import ( + "context" + "fmt" + "net/mail" + "time" + + "github.com/seaweedfs/seaweedfs/weed/glog" + "github.com/seaweedfs/seaweedfs/weed/iam/policy" +) + +// IdentityProvider defines the interface for external identity providers +type IdentityProvider interface { + // Name returns the unique name of the provider + Name() string + + // Initialize initializes the provider with configuration + Initialize(config interface{}) error + + // Authenticate authenticates a user with a token and returns external identity + Authenticate(ctx context.Context, token string) (*ExternalIdentity, error) + + // GetUserInfo retrieves user information by user ID + GetUserInfo(ctx context.Context, userID string) (*ExternalIdentity, error) + + // ValidateToken validates a token and returns claims + ValidateToken(ctx context.Context, token string) (*TokenClaims, error) +} + +// ExternalIdentity represents an identity from an external provider +type ExternalIdentity struct { + // UserID is the unique identifier from the external provider + UserID string `json:"userId"` + + // Email is the user's email address + Email string `json:"email"` + + // DisplayName is the user's display name + DisplayName string `json:"displayName"` + + // Groups are the groups the user belongs to + Groups []string `json:"groups,omitempty"` + + // Attributes are additional user attributes + Attributes map[string]string `json:"attributes,omitempty"` + + // Provider is the name of the identity provider + Provider string `json:"provider"` +} + +// Validate validates the external identity structure +func (e *ExternalIdentity) Validate() error { + if e.UserID == "" { + return fmt.Errorf("user ID is required") + } + + if e.Provider == "" { + return fmt.Errorf("provider is required") + } + + if e.Email != "" { + if _, err := mail.ParseAddress(e.Email); err != nil { + return fmt.Errorf("invalid email format: %w", err) + } + } + + return nil +} + +// TokenClaims represents claims from a validated token +type TokenClaims struct { + // Subject (sub) - user identifier + Subject string `json:"sub"` + + // Issuer (iss) - token issuer + Issuer string `json:"iss"` + + // Audience (aud) - intended audience + Audience string `json:"aud"` + + // ExpiresAt (exp) - expiration time + ExpiresAt time.Time `json:"exp"` + + // IssuedAt (iat) - issued at time + IssuedAt time.Time `json:"iat"` + + // NotBefore (nbf) - not valid before time + NotBefore time.Time `json:"nbf,omitempty"` + + // Claims are additional claims from the token + Claims map[string]interface{} `json:"claims,omitempty"` +} + +// IsValid checks if the token claims are valid (not expired, etc.) +func (c *TokenClaims) IsValid() bool { + now := time.Now() + + // Check expiration + if !c.ExpiresAt.IsZero() && now.After(c.ExpiresAt) { + return false + } + + // Check not before + if !c.NotBefore.IsZero() && now.Before(c.NotBefore) { + return false + } + + // Check issued at (shouldn't be in the future) + if !c.IssuedAt.IsZero() && now.Before(c.IssuedAt) { + return false + } + + return true +} + +// GetClaimString returns a string claim value +func (c *TokenClaims) GetClaimString(key string) (string, bool) { + if value, exists := c.Claims[key]; exists { + if str, ok := value.(string); ok { + return str, true + } + } + return "", false +} + +// GetClaimStringSlice returns a string slice claim value +func (c *TokenClaims) GetClaimStringSlice(key string) ([]string, bool) { + if value, exists := c.Claims[key]; exists { + switch v := value.(type) { + case []string: + return v, true + case []interface{}: + var result []string + for _, item := range v { + if str, ok := item.(string); ok { + result = append(result, str) + } + } + return result, len(result) > 0 + case string: + // Single string can be treated as slice + return []string{v}, true + } + } + return nil, false +} + +// ProviderConfig represents configuration for identity providers +type ProviderConfig struct { + // Type of provider (oidc, ldap, saml) + Type string `json:"type"` + + // Name of the provider instance + Name string `json:"name"` + + // Enabled indicates if the provider is active + Enabled bool `json:"enabled"` + + // Config is provider-specific configuration + Config map[string]interface{} `json:"config"` + + // RoleMapping defines how to map external identities to roles + RoleMapping *RoleMapping `json:"roleMapping,omitempty"` +} + +// RoleMapping defines rules for mapping external identities to roles +type RoleMapping struct { + // Rules are the mapping rules + Rules []MappingRule `json:"rules"` + + // DefaultRole is assigned if no rules match + DefaultRole string `json:"defaultRole,omitempty"` +} + +// MappingRule defines a single mapping rule +type MappingRule struct { + // Claim is the claim key to check + Claim string `json:"claim"` + + // Value is the expected claim value (supports wildcards) + Value string `json:"value"` + + // Role is the role ARN to assign + Role string `json:"role"` + + // Condition is additional condition logic (optional) + Condition string `json:"condition,omitempty"` +} + +// Matches checks if a rule matches the given claims +func (r *MappingRule) Matches(claims *TokenClaims) bool { + if r.Claim == "" || r.Value == "" { + glog.V(3).Infof("Rule invalid: claim=%s, value=%s", r.Claim, r.Value) + return false + } + + claimValue, exists := claims.GetClaimString(r.Claim) + if !exists { + glog.V(3).Infof("Claim '%s' not found as string, trying as string slice", r.Claim) + // Try as string slice + if claimSlice, sliceExists := claims.GetClaimStringSlice(r.Claim); sliceExists { + glog.V(3).Infof("Claim '%s' found as string slice: %v", r.Claim, claimSlice) + for _, val := range claimSlice { + glog.V(3).Infof("Checking if '%s' matches rule value '%s'", val, r.Value) + if r.matchValue(val) { + glog.V(3).Infof("Match found: '%s' matches '%s'", val, r.Value) + return true + } + } + } else { + glog.V(3).Infof("Claim '%s' not found in any format", r.Claim) + } + return false + } + + glog.V(3).Infof("Claim '%s' found as string: '%s'", r.Claim, claimValue) + return r.matchValue(claimValue) +} + +// matchValue checks if a value matches the rule value (with wildcard support) +// Uses AWS IAM-compliant case-insensitive wildcard matching for consistency with policy engine +func (r *MappingRule) matchValue(value string) bool { + matched := policy.AwsWildcardMatch(r.Value, value) + glog.V(3).Infof("AWS IAM pattern match result: '%s' matches '%s' = %t", value, r.Value, matched) + return matched +} |
