5b4683febf
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.
141 lines
3.6 KiB
Go
141 lines
3.6 KiB
Go
package oidc
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"git.ryuvia.com/niklas/terdut-server/internal/config"
|
|
)
|
|
|
|
func testCfg() config.OIDC {
|
|
return config.OIDC{
|
|
AllowedGroups: []string{"terdut-users"},
|
|
AdminGroup: "terdut-admins",
|
|
}
|
|
}
|
|
|
|
func TestComputeGrants(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
groups []string
|
|
want Grants
|
|
}{
|
|
{
|
|
name: "not in an allowed group is refused",
|
|
groups: []string{"sre", "terdut-admins"},
|
|
want: Grants{Admitted: false},
|
|
},
|
|
{
|
|
name: "allowed but no grants",
|
|
groups: []string{"terdut-users"},
|
|
want: Grants{Admitted: true},
|
|
},
|
|
{
|
|
name: "admin group grants admin",
|
|
groups: []string{"terdut-users", "terdut-admins"},
|
|
want: Grants{Admitted: true, Admin: true},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := ComputeGrants(testCfg(), tt.groups)
|
|
if !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("got %+v, want %+v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestComputeGrants_NoAllowedGroupsAdmitsEveryone(t *testing.T) {
|
|
cfg := testCfg()
|
|
cfg.AllowedGroups = nil
|
|
if g := ComputeGrants(cfg, nil); !g.Admitted {
|
|
t.Error("with no allowed groups configured, everybody the provider authenticates is admitted")
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
if got := stringList("solo"); !reflect.DeepEqual(got, []string{"solo"}) {
|
|
t.Errorf("single string: %v", got)
|
|
}
|
|
if got := stringList(nil); got != nil {
|
|
t.Errorf("nil: %v", got)
|
|
}
|
|
}
|