aboutsummaryrefslogtreecommitdiff
path: root/weed/iam/providers/provider.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/iam/providers/provider.go')
-rw-r--r--weed/iam/providers/provider.go227
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
+}