OIDC groups implementation

- Add Groups field to User struct with JSON storage
- Include GetGroups() and SetGroups() helper methods
- Extract groups from OIDC claims in FromClaim()
- Add database migration 202509161200 for groups column
- Update config-example.yaml with groups scope
- Add comprehensive documentation and testing
This commit is contained in:
Ryan Malloy 2026-05-21 17:55:31 -06:00
parent 30d12dafed
commit 5abc3c87b2
29 changed files with 5088 additions and 3 deletions

View file

@ -68,6 +68,11 @@ type User struct {
Provider string
ProfilePicURL string
// Groups stores the OIDC groups/roles that the user belongs to.
// This is populated from the 'groups' claim in OIDC tokens and
// is used for role-based access control in Headplane.
Groups string `gorm:"type:text"`
}
func (u *User) StringID() string {
@ -104,6 +109,39 @@ func (u *User) profilePicURL() string {
return u.ProfilePicURL
}
// GetGroups returns the user's groups as a slice of strings.
// Groups are stored as JSON in the database.
func (u *User) GetGroups() []string {
if u.Groups == "" {
return []string{}
}
var groups []string
if err := json.Unmarshal([]byte(u.Groups), &groups); err != nil {
log.Error().Err(err).Msg("Failed to unmarshal user groups")
return []string{}
}
return groups
}
// SetGroups stores the user's groups as JSON in the database.
func (u *User) SetGroups(groups []string) {
if len(groups) == 0 {
u.Groups = ""
return
}
data, err := json.Marshal(groups)
if err != nil {
log.Error().Err(err).Msg("Failed to marshal user groups")
u.Groups = ""
return
}
u.Groups = string(data)
}
func (u *User) TailscaleUser() *tailcfg.User {
user := tailcfg.User{
ID: tailcfg.UserID(u.ID),
@ -341,4 +379,7 @@ func (u *User) FromClaim(claims *OIDCClaims) {
u.DisplayName = claims.Name
u.ProfilePicURL = claims.ProfilePictureURL
u.Provider = util.RegisterMethodOIDC
// Store OIDC groups for role-based access control
u.SetGroups(claims.Groups)
}