6 Commits
hotfix ... main

Author SHA1 Message Date
Jan Eitzinger
a101f215dc Merge pull request #542 from ClusterCockpit/hotfix
Hotfix
2026-03-31 07:23:14 +02:00
Jan Eitzinger
5398246a61 Merge pull request #540 from ClusterCockpit/hotfix
Hotfix
2026-03-27 10:00:32 +01:00
Jan Eitzinger
b43c52f5b5 Merge pull request #538 from ClusterCockpit/hotfix
Hotfix
2026-03-26 08:01:31 +01:00
Jan Eitzinger
b7e133fbaf Merge pull request #536 from ClusterCockpit/hotfix
Hotfix
2026-03-25 07:07:49 +01:00
Jan Eitzinger
c3b6d93941 Merge pull request #535 from ClusterCockpit/hotfix
Hotfix
2026-03-24 19:01:49 +01:00
Jan Eitzinger
c13fd68aa9 Merge pull request #534 from ClusterCockpit/hotfix
feat: Add command line switch to trigger manual metricstore checkpoin…
2026-03-23 19:28:06 +01:00
2 changed files with 16 additions and 63 deletions

View File

@@ -79,7 +79,7 @@ func NewOIDC(a *Authentication) *OIDC {
ClientID: clientID,
ClientSecret: clientSecret,
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "roles"},
Scopes: []string{oidc.ScopeOpenID, "profile"},
}
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)
// Extract profile claims from userinfo (username, name)
var userInfoClaims 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 {
// Extract custom claims from userinfo
var claims struct {
Username string `json:"preferred_username"`
Name string `json:"name"`
// Keycloak realm-level roles
RealmAccess struct {
Roles []string `json:"roles"`
} `json:"realm_access"`
// Keycloak client-level roles: map from client-id to role list
ResourceAccess map[string]struct {
Roles []string `json:"roles"`
// Keycloak client-level roles
ResourceAccess struct {
Client struct {
Roles []string `json:"roles"`
} `json:"clustercockpit"`
} `json:"resource_access"`
}
if err := idToken.Claims(&idTokenClaims); err != nil {
cclog.Errorf("failed to extract ID token claims: %s", err.Error())
http.Error(rw, "Failed to extract ID token claims", http.StatusInternalServerError)
if err := userInfo.Claims(&claims); err != nil {
cclog.Errorf("failed to extract claims: %s", err.Error())
http.Error(rw, "Failed to extract user claims", http.StatusInternalServerError)
return
}
cclog.Debugf("OIDC userinfo claims: username=%q name=%q", userInfoClaims.Username, userInfoClaims.Name)
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 == "" {
if claims.Username == "" {
http.Error(rw, "Username claim missing from OIDC provider", http.StatusBadRequest)
return
}
// Collect roles from realm_access (realm roles) in the ID token
oidcRoles := append([]string{}, idTokenClaims.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...)
}
// Merge roles from both client-level and realm-level access
oidcRoles := append(claims.ResourceAccess.Client.Roles, claims.RealmAccess.Roles...)
roleSet := make(map[string]bool)
for _, r := range oidcRoles {
@@ -248,8 +217,8 @@ func (oa *OIDC) OAuth2Callback(rw http.ResponseWriter, r *http.Request) {
}
user := &schema.User{
Username: username,
Name: name,
Username: claims.Username,
Name: claims.Name,
Roles: roles,
Projects: projects,
AuthSource: schema.AuthViaOIDC,

View File

@@ -14,7 +14,6 @@ import (
"path/filepath"
"reflect"
"runtime"
"sort"
"strings"
"sync"
@@ -211,12 +210,6 @@ func (r *UserRepository) AddUserIfNotExists(user *schema.User) error {
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 {
// user contains updated info -> Apply to dbUser
// --- 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
}