mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-20 02:42:11 +00:00
2c96242487
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.
111 lines
3.6 KiB
Go
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)
|
|
}
|
|
}
|