Let each team name its own OIDC group, not a global mapping
Team membership from single sign-on used to come from one env var,
TERDUT_OIDC_GROUP_MAPPINGS, matched against a team by name and creating
the team if none existed. That put the decision in the server's
environment rather than the team's own hands, needed a restart to
change, and let a typo in a team name silently create a stray team.
Each team now carries its own oidc_member_group and oidc_owner_group,
set by its owner (or an administrator) from the Members tab, or PUT
/api/teams/{teamID}/oidc-groups. The "highest role wins" rule
TERDUT_OIDC_GROUP_MAPPINGS used to apply across mappings now applies
across one team's own two fields: being in both makes somebody an
owner. The sync no longer creates a team by name; a group only ever
grants into a team that already exists.
This is a breaking change for anyone already using
TERDUT_OIDC_GROUP_MAPPINGS, deliberately not auto-migrated: an
OIDC-sourced membership is dropped at a user's next sign-in until its
team's owner re-sets the group. The README's OIDC section spells out
the migration and the risk of a visible access gap during it.
TERDUT_OIDC_ADMIN_GROUP and TERDUT_OIDC_ALLOWED_GROUPS are untouched --
only team membership moved. terdut-tui needs no change: it only reads
GET /api/teams and GET /api/teams/{id}/members, and neither response
shape moved.
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
@@ -112,25 +111,10 @@ type OIDC struct {
|
||||
// AdminGroup grants the system administrator flag while the user is in it.
|
||||
AdminGroup string
|
||||
|
||||
// GroupMappings grants team roles. A user in Group gets Role in Team.
|
||||
GroupMappings []GroupMapping
|
||||
|
||||
// SessionMaxAge is the hard ceiling on a session made by an SSO login. The
|
||||
// login is the only moment groups are re-read, so this is how long a change
|
||||
// in the provider may take to reach terdut.
|
||||
SessionMaxAge time.Duration
|
||||
|
||||
// parseErr is a malformed TERDUT_OIDC_GROUP_MAPPINGS, reported by Validate:
|
||||
// Load cannot fail, and a mapping that was silently dropped would grant
|
||||
// less access than the operator wrote down.
|
||||
parseErr error
|
||||
}
|
||||
|
||||
// GroupMapping grants Role in Team to members of Group.
|
||||
type GroupMapping struct {
|
||||
Group string `json:"group"`
|
||||
Team string `json:"team"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
// Enabled reports whether single sign-on is configured.
|
||||
@@ -185,11 +169,6 @@ func loadOIDC() OIDC {
|
||||
AdminGroup: os.Getenv("TERDUT_OIDC_ADMIN_GROUP"),
|
||||
SessionMaxAge: duration("TERDUT_OIDC_SESSION_MAX_AGE", 12*time.Hour),
|
||||
}
|
||||
if raw := strings.TrimSpace(os.Getenv("TERDUT_OIDC_GROUP_MAPPINGS")); raw != "" {
|
||||
if err := json.Unmarshal([]byte(raw), &o.GroupMappings); err != nil {
|
||||
o.parseErr = fmt.Errorf("TERDUT_OIDC_GROUP_MAPPINGS: %w", err)
|
||||
}
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
@@ -199,14 +178,11 @@ func loadOIDC() OIDC {
|
||||
// than not starting.
|
||||
func (c Config) Validate() error {
|
||||
o := c.OIDC
|
||||
if o.parseErr != nil {
|
||||
return o.parseErr
|
||||
}
|
||||
if !o.Enabled() {
|
||||
if c.DisablePasswordLogin {
|
||||
return errors.New("TERDUT_PASSWORD_LOGIN=false without TERDUT_OIDC_ISSUER leaves no way to sign in")
|
||||
}
|
||||
if len(o.GroupMappings) > 0 || o.AdminGroup != "" || len(o.AllowedGroups) > 0 {
|
||||
if o.AdminGroup != "" || len(o.AllowedGroups) > 0 {
|
||||
return errors.New("TERDUT_OIDC_* group settings are set but TERDUT_OIDC_ISSUER is not")
|
||||
}
|
||||
return nil
|
||||
@@ -223,16 +199,12 @@ func (c Config) Validate() error {
|
||||
if o.SessionMaxAge <= 0 {
|
||||
return errors.New("TERDUT_OIDC_SESSION_MAX_AGE must be positive")
|
||||
}
|
||||
for i, m := range o.GroupMappings {
|
||||
if m.Group == "" || m.Team == "" {
|
||||
return fmt.Errorf("TERDUT_OIDC_GROUP_MAPPINGS[%d]: group and team are required", i)
|
||||
}
|
||||
if m.Role != "owner" && m.Role != "member" {
|
||||
return fmt.Errorf("TERDUT_OIDC_GROUP_MAPPINGS[%d]: role must be owner or member, got %q", i, m.Role)
|
||||
}
|
||||
}
|
||||
if c.DisablePasswordLogin && len(o.GroupMappings) == 0 && o.AdminGroup == "" {
|
||||
return errors.New("TERDUT_PASSWORD_LOGIN=false with no OIDC group grants leaves nobody able to do anything")
|
||||
// Team grants are no longer visible here: they live on each team's own
|
||||
// oidc_member_group/oidc_owner_group columns, set by that team's owner, not
|
||||
// in config Validate can see at startup. The one thing left to guard against
|
||||
// is an install nobody can administer at all.
|
||||
if c.DisablePasswordLogin && o.AdminGroup == "" {
|
||||
return errors.New("TERDUT_PASSWORD_LOGIN=false with no TERDUT_OIDC_ADMIN_GROUP leaves nobody able to administer the install")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user