mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-04-01 13:37:30 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a101f215dc | ||
|
|
5398246a61 | ||
|
|
b43c52f5b5 | ||
|
|
b7e133fbaf | ||
|
|
c3b6d93941 | ||
|
|
c13fd68aa9 |
@@ -79,7 +79,7 @@ func NewOIDC(a *Authentication) *OIDC {
|
|||||||
ClientID: clientID,
|
ClientID: clientID,
|
||||||
ClientSecret: clientSecret,
|
ClientSecret: clientSecret,
|
||||||
Endpoint: provider.Endpoint(),
|
Endpoint: provider.Endpoint(),
|
||||||
Scopes: []string{oidc.ScopeOpenID, "profile", "roles"},
|
Scopes: []string{oidc.ScopeOpenID, "profile"},
|
||||||
}
|
}
|
||||||
|
|
||||||
oa := &OIDC{provider: provider, client: client, clientID: clientID, authentication: a}
|
oa := &OIDC{provider: provider, client: client, clientID: clientID, authentication: a}
|
||||||
@@ -164,65 +164,34 @@ func (oa *OIDC) OAuth2Callback(rw http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
projects := make([]string, 0)
|
projects := make([]string, 0)
|
||||||
|
|
||||||
// Extract profile claims from userinfo (username, name)
|
// Extract custom claims from userinfo
|
||||||
var userInfoClaims struct {
|
var claims struct {
|
||||||
Username string `json:"preferred_username"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
if err := userInfo.Claims(&userInfoClaims); err != nil {
|
|
||||||
cclog.Errorf("failed to extract userinfo claims: %s", err.Error())
|
|
||||||
http.Error(rw, "Failed to extract user claims", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract role claims from the ID token.
|
|
||||||
// Keycloak includes realm_access and resource_access in the ID token (JWT),
|
|
||||||
// but NOT in the UserInfo endpoint response by default.
|
|
||||||
var idTokenClaims struct {
|
|
||||||
Username string `json:"preferred_username"`
|
Username string `json:"preferred_username"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
// Keycloak realm-level roles
|
// Keycloak realm-level roles
|
||||||
RealmAccess struct {
|
RealmAccess struct {
|
||||||
Roles []string `json:"roles"`
|
Roles []string `json:"roles"`
|
||||||
} `json:"realm_access"`
|
} `json:"realm_access"`
|
||||||
// Keycloak client-level roles: map from client-id to role list
|
// Keycloak client-level roles
|
||||||
ResourceAccess map[string]struct {
|
ResourceAccess struct {
|
||||||
|
Client struct {
|
||||||
Roles []string `json:"roles"`
|
Roles []string `json:"roles"`
|
||||||
|
} `json:"clustercockpit"`
|
||||||
} `json:"resource_access"`
|
} `json:"resource_access"`
|
||||||
}
|
}
|
||||||
if err := idToken.Claims(&idTokenClaims); err != nil {
|
if err := userInfo.Claims(&claims); err != nil {
|
||||||
cclog.Errorf("failed to extract ID token claims: %s", err.Error())
|
cclog.Errorf("failed to extract claims: %s", err.Error())
|
||||||
http.Error(rw, "Failed to extract ID token claims", http.StatusInternalServerError)
|
http.Error(rw, "Failed to extract user claims", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cclog.Debugf("OIDC userinfo claims: username=%q name=%q", userInfoClaims.Username, userInfoClaims.Name)
|
if claims.Username == "" {
|
||||||
cclog.Debugf("OIDC ID token realm_access roles: %v", idTokenClaims.RealmAccess.Roles)
|
|
||||||
cclog.Debugf("OIDC ID token resource_access: %v", idTokenClaims.ResourceAccess)
|
|
||||||
|
|
||||||
// Prefer username from userInfo; fall back to ID token claim
|
|
||||||
username := userInfoClaims.Username
|
|
||||||
if username == "" {
|
|
||||||
username = idTokenClaims.Username
|
|
||||||
}
|
|
||||||
name := userInfoClaims.Name
|
|
||||||
if name == "" {
|
|
||||||
name = idTokenClaims.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
if username == "" {
|
|
||||||
http.Error(rw, "Username claim missing from OIDC provider", http.StatusBadRequest)
|
http.Error(rw, "Username claim missing from OIDC provider", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect roles from realm_access (realm roles) in the ID token
|
// Merge roles from both client-level and realm-level access
|
||||||
oidcRoles := append([]string{}, idTokenClaims.RealmAccess.Roles...)
|
oidcRoles := append(claims.ResourceAccess.Client.Roles, claims.RealmAccess.Roles...)
|
||||||
|
|
||||||
// Also collect roles from resource_access (client roles) for all clients
|
|
||||||
for clientID, access := range idTokenClaims.ResourceAccess {
|
|
||||||
cclog.Debugf("OIDC ID token resource_access[%q] roles: %v", clientID, access.Roles)
|
|
||||||
oidcRoles = append(oidcRoles, access.Roles...)
|
|
||||||
}
|
|
||||||
|
|
||||||
roleSet := make(map[string]bool)
|
roleSet := make(map[string]bool)
|
||||||
for _, r := range oidcRoles {
|
for _, r := range oidcRoles {
|
||||||
@@ -248,8 +217,8 @@ func (oa *OIDC) OAuth2Callback(rw http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
user := &schema.User{
|
user := &schema.User{
|
||||||
Username: username,
|
Username: claims.Username,
|
||||||
Name: name,
|
Name: claims.Name,
|
||||||
Roles: roles,
|
Roles: roles,
|
||||||
Projects: projects,
|
Projects: projects,
|
||||||
AuthSource: schema.AuthViaOIDC,
|
AuthSource: schema.AuthViaOIDC,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -211,12 +210,6 @@ func (r *UserRepository) AddUserIfNotExists(user *schema.User) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func sortedRoles(roles []string) []string {
|
|
||||||
cp := append([]string{}, roles...)
|
|
||||||
sort.Strings(cp)
|
|
||||||
return cp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *UserRepository) UpdateUser(dbUser *schema.User, user *schema.User) error {
|
func (r *UserRepository) UpdateUser(dbUser *schema.User, user *schema.User) error {
|
||||||
// user contains updated info -> Apply to dbUser
|
// user contains updated info -> Apply to dbUser
|
||||||
// --- Simple Name Update ---
|
// --- Simple Name Update ---
|
||||||
@@ -286,15 +279,6 @@ func (r *UserRepository) UpdateUser(dbUser *schema.User, user *schema.User) erro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Fallback: sync any remaining role differences not covered above ---
|
|
||||||
// This handles admin role assignment/removal and any other combinations that
|
|
||||||
// the specific branches above do not cover (e.g. user→admin, admin→user).
|
|
||||||
if !reflect.DeepEqual(sortedRoles(dbUser.Roles), sortedRoles(user.Roles)) {
|
|
||||||
if err := updateRoles(user.Roles); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user