You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							224 lines
						
					
					
						
							5.6 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							224 lines
						
					
					
						
							5.6 KiB
						
					
					
				| package providers | |
| 
 | |
| import ( | |
| 	"context" | |
| 	"fmt" | |
| 	"net/mail" | |
| 	"strings" | |
| 	"time" | |
| ) | |
| 
 | |
| // 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 == "" { | |
| 		return false | |
| 	} | |
| 
 | |
| 	claimValue, exists := claims.GetClaimString(r.Claim) | |
| 	if !exists { | |
| 		// Try as string slice | |
| 		if claimSlice, sliceExists := claims.GetClaimStringSlice(r.Claim); sliceExists { | |
| 			for _, val := range claimSlice { | |
| 				if r.matchValue(val) { | |
| 					return true | |
| 				} | |
| 			} | |
| 		} | |
| 		return false | |
| 	} | |
| 
 | |
| 	return r.matchValue(claimValue) | |
| } | |
| 
 | |
| // matchValue checks if a value matches the rule value (with wildcard support) | |
| func (r *MappingRule) matchValue(value string) bool { | |
| 	// Simple wildcard matching | |
| 	if strings.Contains(r.Value, "*") { | |
| 		// Convert wildcard to regex-like matching | |
| 		pattern := strings.ReplaceAll(r.Value, "*", "") | |
| 		if pattern == "" { | |
| 			return true // "*" matches everything | |
| 		} | |
| 		return strings.Contains(value, pattern) | |
| 	} | |
| 
 | |
| 	return value == r.Value | |
| }
 |