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.
227 lines
6.2 KiB
227 lines
6.2 KiB
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
|
|
}
|