Files
pulse/internal/api/auth_principal_identity.go
T
rcourtman 2c96242487 Use stable SSO principals
Derive OIDC and SAML browser-session principals from provider-scoped subjects instead of mutable username or email claims.

Preserve compatibility by migrating legacy username/email RBAC assignments to the stable SSO principal when no authoritative group mapping is present, and pin the invariant in API/security contracts.
2026-05-04 00:16:31 +01:00

86 lines
2.3 KiB
Go

package api
import (
"crypto/sha256"
"encoding/base64"
"fmt"
"strings"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
internalauth "github.com/rcourtman/pulse-go-rewrite/pkg/auth"
)
func stableSSOPrincipal(providerType config.SSOProviderType, providerID string, subject string) (string, error) {
typ := strings.TrimSpace(strings.ToLower(string(providerType)))
id := strings.TrimSpace(providerID)
sub := strings.TrimSpace(subject)
if typ == "" {
return "", fmt.Errorf("sso provider type is required")
}
if id == "" {
return "", fmt.Errorf("sso provider id is required")
}
if sub == "" {
return "", fmt.Errorf("sso subject is required")
}
sum := sha256.Sum256([]byte(typ + "\x00" + id + "\x00" + sub))
return "sso:" + typ + ":" + id + ":" + base64.RawURLEncoding.EncodeToString(sum[:]), nil
}
func ssoLegacyPrincipalCandidates(values ...string) []string {
candidates := make([]string, 0, len(values)+2)
seen := make(map[string]struct{}, len(values)+2)
for _, value := range values {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
continue
}
if _, ok := seen[trimmed]; !ok {
candidates = append(candidates, trimmed)
seen[trimmed] = struct{}{}
}
if strings.Contains(trimmed, "@") {
lower := strings.ToLower(trimmed)
if _, ok := seen[lower]; !ok {
candidates = append(candidates, lower)
seen[lower] = struct{}{}
}
}
}
return candidates
}
func applySSORoleAssignments(manager internalauth.Manager, principal string, legacyCandidates []string, mappedRoles []string, mappingAuthoritative bool, ensureListed bool) error {
principal = strings.TrimSpace(principal)
if manager == nil || principal == "" {
return nil
}
if mappingAuthoritative || len(mappedRoles) > 0 {
return manager.UpdateUserRoles(principal, mappedRoles)
}
if assignment, ok := manager.GetUserAssignment(principal); ok && len(assignment.RoleIDs) > 0 {
return nil
}
for _, candidate := range legacyCandidates {
candidate = strings.TrimSpace(candidate)
if candidate == "" || candidate == principal {
continue
}
assignment, ok := manager.GetUserAssignment(candidate)
if !ok || len(assignment.RoleIDs) == 0 {
continue
}
return manager.UpdateUserRoles(principal, assignment.RoleIDs)
}
if ensureListed {
return manager.UpdateUserRoles(principal, nil)
}
return nil
}