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:
parent
30d12dafed
commit
5abc3c87b2
29 changed files with 5088 additions and 3 deletions
|
|
@ -932,6 +932,28 @@ AND auth_key_id NOT IN (
|
|||
},
|
||||
Rollback: func(db *gorm.DB) error { return nil },
|
||||
},
|
||||
// Add Groups column to users table for OIDC role-based access control
|
||||
{
|
||||
ID: "202509161200",
|
||||
Migrate: func(tx *gorm.DB) error {
|
||||
// Add Groups column to store OIDC group memberships as JSON
|
||||
if !tx.Migrator().HasColumn(&types.User{}, "groups") {
|
||||
err := tx.Migrator().AddColumn(&types.User{}, "groups")
|
||||
if err != nil {
|
||||
return fmt.Errorf("adding groups column to users table: %w", err)
|
||||
}
|
||||
log.Info().Msg("Added Groups column to users table for OIDC role mapping")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Rollback: func(db *gorm.DB) error {
|
||||
// Remove Groups column on rollback
|
||||
if tx.Migrator().HasColumn(&types.User{}, "groups") {
|
||||
return tx.Migrator().DropColumn(&types.User{}, "groups")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
// From this point, the following rules must be followed:
|
||||
// - NEVER use gorm.AutoMigrate, write the exact migration steps needed
|
||||
// - AutoMigrate depends on the struct staying exactly the same, which it won't over time.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue