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:
@@ -11,11 +11,6 @@ func testCfg() config.OIDC {
|
||||
return config.OIDC{
|
||||
AllowedGroups: []string{"terdut-users"},
|
||||
AdminGroup: "terdut-admins",
|
||||
GroupMappings: []config.GroupMapping{
|
||||
{Group: "sre", Team: "SRE", Role: "member"},
|
||||
{Group: "sre-leads", Team: "SRE", Role: "owner"},
|
||||
{Group: "platform", Team: "Platform", Role: "member"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,27 +23,17 @@ func TestComputeGrants(t *testing.T) {
|
||||
{
|
||||
name: "not in an allowed group is refused",
|
||||
groups: []string{"sre", "terdut-admins"},
|
||||
want: Grants{Admitted: false, Teams: map[string]string{}},
|
||||
want: Grants{Admitted: false},
|
||||
},
|
||||
{
|
||||
name: "allowed but no grants",
|
||||
groups: []string{"terdut-users"},
|
||||
want: Grants{Admitted: true, Teams: map[string]string{}},
|
||||
want: Grants{Admitted: true},
|
||||
},
|
||||
{
|
||||
name: "admin group grants admin",
|
||||
groups: []string{"terdut-users", "terdut-admins"},
|
||||
want: Grants{Admitted: true, Admin: true, Teams: map[string]string{}},
|
||||
},
|
||||
{
|
||||
name: "team roles from several groups",
|
||||
groups: []string{"terdut-users", "sre", "platform"},
|
||||
want: Grants{Admitted: true, Teams: map[string]string{"SRE": "member", "Platform": "member"}},
|
||||
},
|
||||
{
|
||||
name: "highest role wins whatever the order",
|
||||
groups: []string{"sre-leads", "terdut-users", "sre"},
|
||||
want: Grants{Admitted: true, Teams: map[string]string{"SRE": "owner"}},
|
||||
want: Grants{Admitted: true, Admin: true},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -69,6 +54,79 @@ func TestComputeGrants_NoAllowedGroupsAdmitsEveryone(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// testTeamGroups is one SRE team keyed off two groups (a member group and a
|
||||
// higher owner group) and one Platform team keyed off a member group only —
|
||||
// the same shape the old global TERDUT_OIDC_GROUP_MAPPINGS example used.
|
||||
func testTeamGroups() []TeamGroup {
|
||||
return []TeamGroup{
|
||||
{TeamID: 1, MemberGroup: "sre", OwnerGroup: "sre-leads"},
|
||||
{TeamID: 2, MemberGroup: "platform"},
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeTeamGrants(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
teamGroups []TeamGroup
|
||||
groups []string
|
||||
want map[int64]string
|
||||
}{
|
||||
{
|
||||
name: "no matching group grants nothing",
|
||||
teamGroups: testTeamGroups(),
|
||||
groups: []string{"terdut-users"},
|
||||
want: map[int64]string{},
|
||||
},
|
||||
{
|
||||
name: "member group grants member",
|
||||
teamGroups: testTeamGroups(),
|
||||
groups: []string{"sre"},
|
||||
want: map[int64]string{1: roleMember},
|
||||
},
|
||||
{
|
||||
name: "owner group grants owner",
|
||||
teamGroups: testTeamGroups(),
|
||||
groups: []string{"sre-leads"},
|
||||
want: map[int64]string{1: roleOwner},
|
||||
},
|
||||
{
|
||||
name: "in both of a team's groups, owner wins",
|
||||
teamGroups: testTeamGroups(),
|
||||
groups: []string{"sre", "sre-leads"},
|
||||
want: map[int64]string{1: roleOwner},
|
||||
},
|
||||
{
|
||||
name: "several teams from several groups",
|
||||
teamGroups: testTeamGroups(),
|
||||
groups: []string{"sre", "platform"},
|
||||
want: map[int64]string{1: roleMember, 2: roleMember},
|
||||
},
|
||||
{
|
||||
name: "two teams may share a group",
|
||||
teamGroups: []TeamGroup{
|
||||
{TeamID: 1, MemberGroup: "sre"},
|
||||
{TeamID: 2, MemberGroup: "sre"},
|
||||
},
|
||||
groups: []string{"sre"},
|
||||
want: map[int64]string{1: roleMember, 2: roleMember},
|
||||
},
|
||||
{
|
||||
name: "a team with neither field set is never granted",
|
||||
teamGroups: []TeamGroup{{TeamID: 1}},
|
||||
groups: []string{"sre", "sre-leads", "platform"},
|
||||
want: map[int64]string{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := ComputeTeamGrants(tt.teamGroups, tt.groups)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("got %+v, want %+v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStringList(t *testing.T) {
|
||||
if got := stringList([]any{"a", "", 3, "b"}); !reflect.DeepEqual(got, []string{"a", "b"}) {
|
||||
t.Errorf("list: %v", got)
|
||||
|
||||
Reference in New Issue
Block a user