Files
pulse/internal/api/auth_principal_identity_test.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

111 lines
3.6 KiB
Go

package api
import (
"strings"
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/pkg/auth"
)
type ssoIdentityRBACManager struct {
auth.Manager
assignments map[string]auth.UserRoleAssignment
updatedUser string
updatedRoles []string
}
func (m *ssoIdentityRBACManager) GetUserAssignment(username string) (auth.UserRoleAssignment, bool) {
if m.assignments == nil {
return auth.UserRoleAssignment{}, false
}
assignment, ok := m.assignments[username]
return assignment, ok
}
func (m *ssoIdentityRBACManager) UpdateUserRoles(username string, roleIDs []string) error {
m.updatedUser = username
m.updatedRoles = append([]string{}, roleIDs...)
if m.assignments == nil {
m.assignments = make(map[string]auth.UserRoleAssignment)
}
m.assignments[username] = auth.UserRoleAssignment{Username: username, RoleIDs: append([]string{}, roleIDs...)}
return nil
}
func TestStableSSOPrincipalUsesProviderScopedOpaqueSubject(t *testing.T) {
principal, err := stableSSOPrincipal(config.SSOProviderTypeOIDC, "okta", "alice@example.com")
if err != nil {
t.Fatalf("stableSSOPrincipal error: %v", err)
}
again, err := stableSSOPrincipal(config.SSOProviderTypeOIDC, "okta", "alice@example.com")
if err != nil {
t.Fatalf("stableSSOPrincipal repeat error: %v", err)
}
otherProvider, err := stableSSOPrincipal(config.SSOProviderTypeOIDC, "entra", "alice@example.com")
if err != nil {
t.Fatalf("stableSSOPrincipal other provider error: %v", err)
}
if principal != again {
t.Fatalf("stable principal is not deterministic: %q != %q", principal, again)
}
if principal == otherProvider {
t.Fatal("stable principal must be scoped by provider")
}
if !strings.HasPrefix(principal, "sso:oidc:okta:") {
t.Fatalf("principal %q missing expected provider scope", principal)
}
if strings.Contains(principal, "alice") || strings.Contains(principal, "example.com") {
t.Fatalf("principal %q must not expose the raw email subject", principal)
}
}
func TestStableSSOPrincipalRequiresSubject(t *testing.T) {
if _, err := stableSSOPrincipal(config.SSOProviderTypeSAML, "okta", ""); err == nil {
t.Fatal("expected missing subject to fail")
}
}
func TestApplySSORoleAssignmentsMigratesLegacyRoles(t *testing.T) {
manager := &ssoIdentityRBACManager{
assignments: map[string]auth.UserRoleAssignment{
"alice@example.com": {Username: "alice@example.com", RoleIDs: []string{"admin", "viewer"}},
},
}
principal := "sso:oidc:okta:stable"
err := applySSORoleAssignments(manager, principal, ssoLegacyPrincipalCandidates("Alice@Example.com"), nil, false, true)
if err != nil {
t.Fatalf("applySSORoleAssignments error: %v", err)
}
if manager.updatedUser != principal {
t.Fatalf("updated user = %q, want %q", manager.updatedUser, principal)
}
if got := strings.Join(manager.updatedRoles, ","); got != "admin,viewer" {
t.Fatalf("updated roles = %q, want admin,viewer", got)
}
}
func TestApplySSORoleAssignmentsUsesAuthoritativeMapping(t *testing.T) {
manager := &ssoIdentityRBACManager{
assignments: map[string]auth.UserRoleAssignment{
"alice@example.com": {Username: "alice@example.com", RoleIDs: []string{"admin"}},
},
}
principal := "sso:saml:okta:stable"
err := applySSORoleAssignments(manager, principal, []string{"alice@example.com"}, []string{"viewer"}, true, true)
if err != nil {
t.Fatalf("applySSORoleAssignments error: %v", err)
}
if manager.updatedUser != principal {
t.Fatalf("updated user = %q, want %q", manager.updatedUser, principal)
}
if got := strings.Join(manager.updatedRoles, ","); got != "viewer" {
t.Fatalf("updated roles = %q, want viewer", got)
}
}