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:
+40
-14
@@ -18,7 +18,9 @@ const (
|
||||
roleMember = "member"
|
||||
)
|
||||
|
||||
// Grants is the access a set of groups confers.
|
||||
// Grants is the account-wide access a set of groups confers. Team access is a
|
||||
// separate question — see TeamGroup and ComputeTeamGrants — because it is
|
||||
// configured per team in the database, not in this package's cfg.
|
||||
type Grants struct {
|
||||
// Admitted is false when AllowedGroups is set and the user is in none of
|
||||
// them. Nothing else in the struct means anything then.
|
||||
@@ -26,21 +28,16 @@ type Grants struct {
|
||||
|
||||
// Admin is whether the user is in the admin group.
|
||||
Admin bool
|
||||
|
||||
// Teams maps team name to role. Where several groups grant the same team the
|
||||
// highest role wins, so belonging to both a members group and an owners
|
||||
// group makes somebody an owner rather than whichever mapping came last.
|
||||
Teams map[string]string
|
||||
}
|
||||
|
||||
// ComputeGrants evaluates the configured mappings against groups.
|
||||
// ComputeGrants evaluates the account-wide configuration against groups.
|
||||
func ComputeGrants(cfg config.OIDC, groups []string) Grants {
|
||||
in := make(map[string]bool, len(groups))
|
||||
for _, g := range groups {
|
||||
in[g] = true
|
||||
}
|
||||
|
||||
g := Grants{Teams: map[string]string{}}
|
||||
var g Grants
|
||||
|
||||
g.Admitted = len(cfg.AllowedGroups) == 0
|
||||
for _, allowed := range cfg.AllowedGroups {
|
||||
@@ -54,16 +51,45 @@ func ComputeGrants(cfg config.OIDC, groups []string) Grants {
|
||||
}
|
||||
|
||||
g.Admin = cfg.AdminGroup != "" && in[cfg.AdminGroup]
|
||||
return g
|
||||
}
|
||||
|
||||
for _, m := range cfg.GroupMappings {
|
||||
if !in[m.Group] {
|
||||
continue
|
||||
// TeamGroup is one team's own OIDC binding: which group, if any, grants
|
||||
// member access to it and which grants owner access, as read from
|
||||
// teams.oidc_member_group / teams.oidc_owner_group.
|
||||
type TeamGroup struct {
|
||||
TeamID int64
|
||||
MemberGroup string // "" means no group grants member access here.
|
||||
OwnerGroup string // "" means no group grants owner access here.
|
||||
}
|
||||
|
||||
// ComputeTeamGrants evaluates every team's own group binding against groups,
|
||||
// and returns the role each team grants, keyed by team ID. A team absent from
|
||||
// the result is not granted at all. Where a team's member and owner groups
|
||||
// both match, the owner group wins — the same "highest role wins" rule that
|
||||
// applied across the old global mapping list applies here across one team's
|
||||
// two fields, so belonging to both groups makes somebody an owner rather than
|
||||
// whichever field happened to be checked last.
|
||||
func ComputeTeamGrants(teamGroups []TeamGroup, groups []string) map[int64]string {
|
||||
in := make(map[string]bool, len(groups))
|
||||
for _, g := range groups {
|
||||
in[g] = true
|
||||
}
|
||||
|
||||
out := map[int64]string{}
|
||||
for _, tg := range teamGroups {
|
||||
role := ""
|
||||
if tg.MemberGroup != "" && in[tg.MemberGroup] {
|
||||
role = roleMember
|
||||
}
|
||||
if rank(m.Role) > rank(g.Teams[m.Team]) {
|
||||
g.Teams[m.Team] = m.Role
|
||||
if tg.OwnerGroup != "" && in[tg.OwnerGroup] && rank(roleOwner) > rank(role) {
|
||||
role = roleOwner
|
||||
}
|
||||
if role != "" {
|
||||
out[tg.TeamID] = role
|
||||
}
|
||||
}
|
||||
return g
|
||||
return out
|
||||
}
|
||||
|
||||
// rank orders roles; an unknown or absent role ranks lowest.
|
||||
|
||||
Reference in New Issue
Block a user