mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
bddb808b25
Fixes all our Go file imports to match the preferred spec that we've _mostly_ been using. For example: ``` import ( "context" "time" "github.com/prometheus/client_golang/prometheus" "golang.org/x/xerrors" "gopkg.in/natefinch/lumberjack.v2" "cdr.dev/slog/v3" "github.com/coder/coder/v2/codersdk/agentsdk" "github.com/coder/serpent" ) ``` 3 groups: standard library, 3rd partly libs, Coder libs. This PR makes the change across the codebase. The PR in the stack above modifies our formatting to maintain this state of affairs, and is a separate PR so it's possible to review that one in detail.
947 lines
23 KiB
Go
947 lines
23 KiB
Go
package idpsync_test
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"regexp"
|
|
"slices"
|
|
"testing"
|
|
|
|
"github.com/golang-jwt/jwt/v4"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/xerrors"
|
|
|
|
"cdr.dev/slog/v3/sloggers/slogtest"
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/db2sdk"
|
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
|
"github.com/coder/coder/v2/coderd/idpsync"
|
|
"github.com/coder/coder/v2/coderd/runtimeconfig"
|
|
"github.com/coder/coder/v2/coderd/util/ptr"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func TestParseGroupClaims(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("EmptyConfig", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := idpsync.NewAGPLSync(slogtest.Make(t, &slogtest.Options{}),
|
|
runtimeconfig.NewManager(),
|
|
idpsync.DeploymentSyncSettings{})
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
|
|
params, err := s.ParseGroupClaims(ctx, jwt.MapClaims{})
|
|
require.Nil(t, err)
|
|
|
|
require.False(t, params.SyncEntitled)
|
|
})
|
|
|
|
t.Run("NotInAllowList", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := idpsync.NewAGPLSync(slogtest.Make(t, &slogtest.Options{}),
|
|
runtimeconfig.NewManager(),
|
|
idpsync.DeploymentSyncSettings{
|
|
GroupField: "groups",
|
|
GroupAllowList: map[string]struct{}{
|
|
"foo": {},
|
|
},
|
|
})
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
|
|
// Invalid group
|
|
_, err := s.ParseGroupClaims(ctx, jwt.MapClaims{
|
|
"groups": []string{"bar"},
|
|
})
|
|
require.NotNil(t, err)
|
|
require.Equal(t, 403, err.Code)
|
|
|
|
// No groups
|
|
_, err = s.ParseGroupClaims(ctx, jwt.MapClaims{})
|
|
require.NotNil(t, err)
|
|
require.Equal(t, 403, err.Code)
|
|
})
|
|
|
|
t.Run("InAllowList", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := idpsync.NewAGPLSync(slogtest.Make(t, &slogtest.Options{}),
|
|
runtimeconfig.NewManager(),
|
|
idpsync.DeploymentSyncSettings{
|
|
GroupField: "groups",
|
|
GroupAllowList: map[string]struct{}{
|
|
"foo": {},
|
|
},
|
|
})
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
|
|
claims := jwt.MapClaims{
|
|
"groups": []string{"foo", "bar"},
|
|
}
|
|
params, err := s.ParseGroupClaims(ctx, claims)
|
|
require.Nil(t, err)
|
|
require.Equal(t, claims, params.MergedClaims)
|
|
})
|
|
}
|
|
|
|
//nolint:paralleltest, tparallel
|
|
func TestGroupSyncTable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
userClaims := jwt.MapClaims{
|
|
"groups": []string{
|
|
"foo", "bar", "baz",
|
|
"create-bar", "create-baz",
|
|
"legacy-bar",
|
|
},
|
|
}
|
|
|
|
ids := coderdtest.NewDeterministicUUIDGenerator()
|
|
testCases := []orgSetupDefinition{
|
|
{
|
|
Name: "SwitchGroups",
|
|
GroupSettings: &codersdk.GroupSyncSettings{
|
|
Field: "groups",
|
|
Mapping: map[string][]uuid.UUID{
|
|
"foo": {ids.ID("sg-foo"), ids.ID("sg-foo-2")},
|
|
"bar": {ids.ID("sg-bar")},
|
|
"baz": {ids.ID("sg-baz")},
|
|
},
|
|
},
|
|
Groups: map[uuid.UUID]bool{
|
|
uuid.New(): true,
|
|
uuid.New(): true,
|
|
// Extra groups
|
|
ids.ID("sg-foo"): false,
|
|
ids.ID("sg-foo-2"): false,
|
|
ids.ID("sg-bar"): false,
|
|
ids.ID("sg-baz"): false,
|
|
},
|
|
assertGroups: &orgGroupAssert{
|
|
ExpectedGroups: []uuid.UUID{
|
|
ids.ID("sg-foo"),
|
|
ids.ID("sg-foo-2"),
|
|
ids.ID("sg-bar"),
|
|
ids.ID("sg-baz"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "StayInGroup",
|
|
GroupSettings: &codersdk.GroupSyncSettings{
|
|
Field: "groups",
|
|
// Only match foo, so bar does not map
|
|
RegexFilter: regexp.MustCompile("^foo$"),
|
|
Mapping: map[string][]uuid.UUID{
|
|
"foo": {ids.ID("gg-foo"), uuid.New()},
|
|
"bar": {ids.ID("gg-bar")},
|
|
"baz": {ids.ID("gg-baz")},
|
|
},
|
|
},
|
|
Groups: map[uuid.UUID]bool{
|
|
ids.ID("gg-foo"): true,
|
|
ids.ID("gg-bar"): false,
|
|
},
|
|
assertGroups: &orgGroupAssert{
|
|
ExpectedGroups: []uuid.UUID{
|
|
ids.ID("gg-foo"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "UserJoinsGroups",
|
|
GroupSettings: &codersdk.GroupSyncSettings{
|
|
Field: "groups",
|
|
Mapping: map[string][]uuid.UUID{
|
|
"foo": {ids.ID("ng-foo"), uuid.New()},
|
|
"bar": {ids.ID("ng-bar"), ids.ID("ng-bar-2")},
|
|
"baz": {ids.ID("ng-baz")},
|
|
},
|
|
},
|
|
Groups: map[uuid.UUID]bool{
|
|
ids.ID("ng-foo"): false,
|
|
ids.ID("ng-bar"): false,
|
|
ids.ID("ng-bar-2"): false,
|
|
ids.ID("ng-baz"): false,
|
|
},
|
|
assertGroups: &orgGroupAssert{
|
|
ExpectedGroups: []uuid.UUID{
|
|
ids.ID("ng-foo"),
|
|
ids.ID("ng-bar"),
|
|
ids.ID("ng-bar-2"),
|
|
ids.ID("ng-baz"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "CreateGroups",
|
|
GroupSettings: &codersdk.GroupSyncSettings{
|
|
Field: "groups",
|
|
RegexFilter: regexp.MustCompile("^create"),
|
|
AutoCreateMissing: true,
|
|
},
|
|
Groups: map[uuid.UUID]bool{},
|
|
assertGroups: &orgGroupAssert{
|
|
ExpectedGroupNames: []string{
|
|
"create-bar",
|
|
"create-baz",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "GroupNamesNoMapping",
|
|
GroupSettings: &codersdk.GroupSyncSettings{
|
|
Field: "groups",
|
|
RegexFilter: regexp.MustCompile(".*"),
|
|
AutoCreateMissing: false,
|
|
},
|
|
GroupNames: map[string]bool{
|
|
"foo": false,
|
|
"bar": false,
|
|
"goob": true,
|
|
},
|
|
assertGroups: &orgGroupAssert{
|
|
ExpectedGroupNames: []string{
|
|
"foo",
|
|
"bar",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Name: "NoUser",
|
|
GroupSettings: &codersdk.GroupSyncSettings{
|
|
Field: "groups",
|
|
Mapping: map[string][]uuid.UUID{
|
|
// Extra ID that does not map to a group
|
|
"foo": {ids.ID("ow-foo"), uuid.New()},
|
|
},
|
|
RegexFilter: nil,
|
|
AutoCreateMissing: false,
|
|
},
|
|
NotMember: true,
|
|
Groups: map[uuid.UUID]bool{
|
|
ids.ID("ow-foo"): false,
|
|
ids.ID("ow-bar"): false,
|
|
},
|
|
},
|
|
{
|
|
Name: "NoSettings",
|
|
GroupSettings: nil,
|
|
Groups: map[uuid.UUID]bool{},
|
|
assertGroups: &orgGroupAssert{
|
|
ExpectedGroups: []uuid.UUID{},
|
|
},
|
|
},
|
|
{
|
|
Name: "LegacyMapping",
|
|
GroupSettings: &codersdk.GroupSyncSettings{
|
|
Field: "groups",
|
|
RegexFilter: regexp.MustCompile("^legacy"),
|
|
LegacyNameMapping: map[string]string{
|
|
"create-bar": "legacy-bar",
|
|
"foo": "legacy-foo",
|
|
"bop": "legacy-bop",
|
|
},
|
|
AutoCreateMissing: true,
|
|
},
|
|
Groups: map[uuid.UUID]bool{
|
|
ids.ID("lg-foo"): true,
|
|
},
|
|
GroupNames: map[string]bool{
|
|
"legacy-foo": false,
|
|
"extra": true,
|
|
"legacy-bop": true,
|
|
},
|
|
assertGroups: &orgGroupAssert{
|
|
ExpectedGroupNames: []string{
|
|
"legacy-bar",
|
|
"legacy-foo",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
// The final test, "AllTogether", cannot run in parallel.
|
|
// These tests are nearly instant using the memory db, so
|
|
// this is still fast without being in parallel.
|
|
//nolint:paralleltest, tparallel
|
|
t.Run(tc.Name, func(t *testing.T) {
|
|
db, _ := dbtestutil.NewDB(t)
|
|
manager := runtimeconfig.NewManager()
|
|
s := idpsync.NewAGPLSync(slogtest.Make(t, &slogtest.Options{}),
|
|
manager,
|
|
idpsync.DeploymentSyncSettings{
|
|
GroupField: "groups",
|
|
Legacy: idpsync.DefaultOrgLegacySettings{
|
|
GroupField: "groups",
|
|
GroupMapping: map[string]string{
|
|
"foo": "legacy-foo",
|
|
"baz": "legacy-baz",
|
|
},
|
|
GroupFilter: regexp.MustCompile("^legacy"),
|
|
CreateMissingGroups: true,
|
|
},
|
|
},
|
|
)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
|
user := dbgen.User(t, db, database.User{})
|
|
orgID := uuid.New()
|
|
SetupOrganization(t, s, db, user, orgID, tc)
|
|
|
|
// Do the group sync!
|
|
err := s.SyncGroups(ctx, db, user, idpsync.GroupParams{
|
|
SyncEntitled: true,
|
|
MergedClaims: userClaims,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
tc.Assert(t, orgID, db, user)
|
|
})
|
|
}
|
|
|
|
// AllTogether runs the entire tabled test as a singular user and
|
|
// deployment. This tests all organizations being synced together.
|
|
// The reason we do them individually, is that it is much easier to
|
|
// debug a single test case.
|
|
//nolint:paralleltest, tparallel // This should run after all the individual tests
|
|
t.Run("AllTogether", func(t *testing.T) {
|
|
db, _ := dbtestutil.NewDB(t)
|
|
manager := runtimeconfig.NewManager()
|
|
s := idpsync.NewAGPLSync(slogtest.Make(t, &slogtest.Options{}),
|
|
manager,
|
|
// Also sync the default org!
|
|
idpsync.DeploymentSyncSettings{
|
|
GroupField: "groups",
|
|
// This legacy field will fail any tests if the legacy override code
|
|
// has any bugs.
|
|
Legacy: idpsync.DefaultOrgLegacySettings{
|
|
GroupField: "groups",
|
|
GroupMapping: map[string]string{
|
|
"foo": "legacy-foo",
|
|
"baz": "legacy-baz",
|
|
},
|
|
GroupFilter: regexp.MustCompile("^legacy"),
|
|
CreateMissingGroups: true,
|
|
},
|
|
},
|
|
)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
|
user := dbgen.User(t, db, database.User{})
|
|
|
|
var asserts []func(t *testing.T)
|
|
// The default org is also going to do something
|
|
def := orgSetupDefinition{
|
|
Name: "DefaultOrg",
|
|
GroupNames: map[string]bool{
|
|
"legacy-foo": false,
|
|
"legacy-baz": true,
|
|
"random": true,
|
|
},
|
|
// No settings, because they come from the deployment values
|
|
GroupSettings: nil,
|
|
assertGroups: &orgGroupAssert{
|
|
ExpectedGroupNames: []string{"legacy-foo", "legacy-baz", "legacy-bar"},
|
|
},
|
|
}
|
|
|
|
defOrg, err := db.GetDefaultOrganization(dbauthz.AsSystemRestricted(ctx))
|
|
require.NoError(t, err)
|
|
SetupOrganization(t, s, db, user, defOrg.ID, def)
|
|
asserts = append(asserts, func(t *testing.T) {
|
|
t.Run(def.Name, func(t *testing.T) {
|
|
t.Parallel()
|
|
def.Assert(t, defOrg.ID, db, user)
|
|
})
|
|
})
|
|
|
|
for _, tc := range testCases {
|
|
orgID := uuid.New()
|
|
SetupOrganization(t, s, db, user, orgID, tc)
|
|
asserts = append(asserts, func(t *testing.T) {
|
|
t.Run(tc.Name, func(t *testing.T) {
|
|
t.Parallel()
|
|
tc.Assert(t, orgID, db, user)
|
|
})
|
|
})
|
|
}
|
|
|
|
asserts = append(asserts, func(t *testing.T) {
|
|
t.Helper()
|
|
def.Assert(t, defOrg.ID, db, user)
|
|
})
|
|
|
|
// Do the group sync!
|
|
err = s.SyncGroups(ctx, db, user, idpsync.GroupParams{
|
|
SyncEntitled: true,
|
|
MergedClaims: userClaims,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
for _, assert := range asserts {
|
|
assert(t)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSyncDisabled(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
db, _ := dbtestutil.NewDB(t)
|
|
manager := runtimeconfig.NewManager()
|
|
s := idpsync.NewAGPLSync(slogtest.Make(t, &slogtest.Options{}),
|
|
manager,
|
|
idpsync.DeploymentSyncSettings{},
|
|
)
|
|
|
|
ids := coderdtest.NewDeterministicUUIDGenerator()
|
|
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
|
user := dbgen.User(t, db, database.User{})
|
|
orgID := uuid.New()
|
|
|
|
def := orgSetupDefinition{
|
|
Name: "SyncDisabled",
|
|
Groups: map[uuid.UUID]bool{
|
|
ids.ID("foo"): true,
|
|
ids.ID("bar"): true,
|
|
ids.ID("baz"): false,
|
|
ids.ID("bop"): false,
|
|
},
|
|
GroupSettings: &codersdk.GroupSyncSettings{
|
|
Field: "groups",
|
|
Mapping: map[string][]uuid.UUID{
|
|
"foo": {ids.ID("foo")},
|
|
"baz": {ids.ID("baz")},
|
|
},
|
|
},
|
|
assertGroups: &orgGroupAssert{
|
|
ExpectedGroups: []uuid.UUID{
|
|
ids.ID("foo"),
|
|
ids.ID("bar"),
|
|
},
|
|
},
|
|
}
|
|
|
|
SetupOrganization(t, s, db, user, orgID, def)
|
|
|
|
// Do the group sync!
|
|
err := s.SyncGroups(ctx, db, user, idpsync.GroupParams{
|
|
SyncEntitled: false,
|
|
MergedClaims: jwt.MapClaims{
|
|
"groups": []string{"baz", "bop"},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
def.Assert(t, orgID, db, user)
|
|
}
|
|
|
|
// TestApplyGroupDifference is mainly testing the database functions
|
|
func TestApplyGroupDifference(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ids := coderdtest.NewDeterministicUUIDGenerator()
|
|
testCase := []struct {
|
|
Name string
|
|
Before map[uuid.UUID]bool
|
|
Add []uuid.UUID
|
|
Remove []uuid.UUID
|
|
Expect []uuid.UUID
|
|
}{
|
|
{
|
|
Name: "Empty",
|
|
},
|
|
{
|
|
Name: "AddFromNone",
|
|
Before: map[uuid.UUID]bool{
|
|
ids.ID("g1"): false,
|
|
},
|
|
Add: []uuid.UUID{
|
|
ids.ID("g1"),
|
|
},
|
|
Expect: []uuid.UUID{
|
|
ids.ID("g1"),
|
|
},
|
|
},
|
|
{
|
|
Name: "AddSome",
|
|
Before: map[uuid.UUID]bool{
|
|
ids.ID("g1"): true,
|
|
ids.ID("g2"): false,
|
|
ids.ID("g3"): false,
|
|
uuid.New(): false,
|
|
},
|
|
Add: []uuid.UUID{
|
|
ids.ID("g2"),
|
|
ids.ID("g3"),
|
|
},
|
|
Expect: []uuid.UUID{
|
|
ids.ID("g1"),
|
|
ids.ID("g2"),
|
|
ids.ID("g3"),
|
|
},
|
|
},
|
|
{
|
|
Name: "RemoveAll",
|
|
Before: map[uuid.UUID]bool{
|
|
uuid.New(): false,
|
|
ids.ID("g2"): true,
|
|
ids.ID("g3"): true,
|
|
},
|
|
Remove: []uuid.UUID{
|
|
ids.ID("g2"),
|
|
ids.ID("g3"),
|
|
},
|
|
Expect: []uuid.UUID{},
|
|
},
|
|
{
|
|
Name: "Mixed",
|
|
Before: map[uuid.UUID]bool{
|
|
// adds
|
|
ids.ID("a1"): true,
|
|
ids.ID("a2"): true,
|
|
ids.ID("a3"): false,
|
|
ids.ID("a4"): false,
|
|
// removes
|
|
ids.ID("r1"): true,
|
|
ids.ID("r2"): true,
|
|
ids.ID("r3"): false,
|
|
ids.ID("r4"): false,
|
|
// stable
|
|
ids.ID("s1"): true,
|
|
ids.ID("s2"): true,
|
|
// noise
|
|
uuid.New(): false,
|
|
uuid.New(): false,
|
|
},
|
|
Add: []uuid.UUID{
|
|
ids.ID("a1"), ids.ID("a2"),
|
|
ids.ID("a3"), ids.ID("a4"),
|
|
// Double up to try and confuse
|
|
ids.ID("a1"),
|
|
ids.ID("a4"),
|
|
},
|
|
Remove: []uuid.UUID{
|
|
ids.ID("r1"), ids.ID("r2"),
|
|
ids.ID("r3"), ids.ID("r4"),
|
|
// Double up to try and confuse
|
|
ids.ID("r1"),
|
|
ids.ID("r4"),
|
|
},
|
|
Expect: []uuid.UUID{
|
|
ids.ID("a1"), ids.ID("a2"), ids.ID("a3"), ids.ID("a4"),
|
|
ids.ID("s1"), ids.ID("s2"),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCase {
|
|
t.Run(tc.Name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
mgr := runtimeconfig.NewManager()
|
|
db, _ := dbtestutil.NewDB(t)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
ctx = dbauthz.AsSystemRestricted(ctx)
|
|
|
|
org := dbgen.Organization(t, db, database.Organization{})
|
|
_, err := db.InsertAllUsersGroup(ctx, org.ID)
|
|
require.NoError(t, err)
|
|
|
|
user := dbgen.User(t, db, database.User{})
|
|
_ = dbgen.OrganizationMember(t, db, database.OrganizationMember{
|
|
UserID: user.ID,
|
|
OrganizationID: org.ID,
|
|
})
|
|
|
|
for gid, in := range tc.Before {
|
|
group := dbgen.Group(t, db, database.Group{
|
|
ID: gid,
|
|
OrganizationID: org.ID,
|
|
})
|
|
if in {
|
|
_ = dbgen.GroupMember(t, db, database.GroupMemberTable{
|
|
UserID: user.ID,
|
|
GroupID: group.ID,
|
|
})
|
|
}
|
|
}
|
|
|
|
s := idpsync.NewAGPLSync(slogtest.Make(t, &slogtest.Options{}), mgr, idpsync.FromDeploymentValues(coderdtest.DeploymentValues(t)))
|
|
err = s.ApplyGroupDifference(context.Background(), db, user, tc.Add, tc.Remove)
|
|
require.NoError(t, err)
|
|
|
|
userGroups, err := db.GetGroups(ctx, database.GetGroupsParams{
|
|
HasMemberID: user.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// assert
|
|
found := db2sdk.List(userGroups, func(g database.GetGroupsRow) uuid.UUID {
|
|
return g.Group.ID
|
|
})
|
|
|
|
// Add everyone group
|
|
require.ElementsMatch(t, append(tc.Expect, org.ID), found)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExpectedGroupEqual(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ids := coderdtest.NewDeterministicUUIDGenerator()
|
|
testCases := []struct {
|
|
Name string
|
|
A idpsync.ExpectedGroup
|
|
B idpsync.ExpectedGroup
|
|
Equal bool
|
|
}{
|
|
{
|
|
Name: "Empty",
|
|
A: idpsync.ExpectedGroup{},
|
|
B: idpsync.ExpectedGroup{},
|
|
Equal: true,
|
|
},
|
|
{
|
|
Name: "DifferentOrgs",
|
|
A: idpsync.ExpectedGroup{
|
|
OrganizationID: uuid.New(),
|
|
GroupID: ptr.Ref(ids.ID("g1")),
|
|
GroupName: nil,
|
|
},
|
|
B: idpsync.ExpectedGroup{
|
|
OrganizationID: uuid.New(),
|
|
GroupID: ptr.Ref(ids.ID("g1")),
|
|
GroupName: nil,
|
|
},
|
|
Equal: false,
|
|
},
|
|
{
|
|
Name: "SameID",
|
|
A: idpsync.ExpectedGroup{
|
|
OrganizationID: ids.ID("org"),
|
|
GroupID: ptr.Ref(ids.ID("g1")),
|
|
GroupName: nil,
|
|
},
|
|
B: idpsync.ExpectedGroup{
|
|
OrganizationID: ids.ID("org"),
|
|
GroupID: ptr.Ref(ids.ID("g1")),
|
|
GroupName: nil,
|
|
},
|
|
Equal: true,
|
|
},
|
|
{
|
|
Name: "DifferentIDs",
|
|
A: idpsync.ExpectedGroup{
|
|
OrganizationID: ids.ID("org"),
|
|
GroupID: ptr.Ref(uuid.New()),
|
|
GroupName: nil,
|
|
},
|
|
B: idpsync.ExpectedGroup{
|
|
OrganizationID: ids.ID("org"),
|
|
GroupID: ptr.Ref(uuid.New()),
|
|
GroupName: nil,
|
|
},
|
|
Equal: false,
|
|
},
|
|
{
|
|
Name: "SameName",
|
|
A: idpsync.ExpectedGroup{
|
|
OrganizationID: ids.ID("org"),
|
|
GroupID: nil,
|
|
GroupName: ptr.Ref("foo"),
|
|
},
|
|
B: idpsync.ExpectedGroup{
|
|
OrganizationID: ids.ID("org"),
|
|
GroupID: nil,
|
|
GroupName: ptr.Ref("foo"),
|
|
},
|
|
Equal: true,
|
|
},
|
|
{
|
|
Name: "DifferentName",
|
|
A: idpsync.ExpectedGroup{
|
|
OrganizationID: ids.ID("org"),
|
|
GroupID: nil,
|
|
GroupName: ptr.Ref("foo"),
|
|
},
|
|
B: idpsync.ExpectedGroup{
|
|
OrganizationID: ids.ID("org"),
|
|
GroupID: nil,
|
|
GroupName: ptr.Ref("bar"),
|
|
},
|
|
Equal: false,
|
|
},
|
|
// Edge cases
|
|
{
|
|
// A bit strange, but valid as ID takes priority.
|
|
// We assume 2 groups with the same ID are equal, even if
|
|
// their names are different. Names are mutable, IDs are not,
|
|
// so there is 0% chance they are different groups.
|
|
Name: "DifferentIDSameName",
|
|
A: idpsync.ExpectedGroup{
|
|
OrganizationID: ids.ID("org"),
|
|
GroupID: ptr.Ref(ids.ID("g1")),
|
|
GroupName: ptr.Ref("foo"),
|
|
},
|
|
B: idpsync.ExpectedGroup{
|
|
OrganizationID: ids.ID("org"),
|
|
GroupID: ptr.Ref(ids.ID("g1")),
|
|
GroupName: ptr.Ref("bar"),
|
|
},
|
|
Equal: true,
|
|
},
|
|
{
|
|
Name: "MixedNils",
|
|
A: idpsync.ExpectedGroup{
|
|
OrganizationID: ids.ID("org"),
|
|
GroupID: ptr.Ref(ids.ID("g1")),
|
|
GroupName: nil,
|
|
},
|
|
B: idpsync.ExpectedGroup{
|
|
OrganizationID: ids.ID("org"),
|
|
GroupID: nil,
|
|
GroupName: ptr.Ref("bar"),
|
|
},
|
|
Equal: false,
|
|
},
|
|
{
|
|
Name: "NoComparable",
|
|
A: idpsync.ExpectedGroup{
|
|
OrganizationID: ids.ID("org"),
|
|
GroupID: ptr.Ref(ids.ID("g1")),
|
|
GroupName: nil,
|
|
},
|
|
B: idpsync.ExpectedGroup{
|
|
OrganizationID: ids.ID("org"),
|
|
GroupID: nil,
|
|
GroupName: nil,
|
|
},
|
|
Equal: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.Name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
require.Equal(t, tc.Equal, tc.A.Equal(tc.B))
|
|
})
|
|
}
|
|
}
|
|
|
|
func SetupOrganization(t *testing.T, s *idpsync.AGPLIDPSync, db database.Store, user database.User, orgID uuid.UUID, def orgSetupDefinition) {
|
|
t.Helper()
|
|
|
|
// Account that the org might be the default organization
|
|
org, err := db.GetOrganizationByID(context.Background(), orgID)
|
|
if xerrors.Is(err, sql.ErrNoRows) {
|
|
org = dbgen.Organization(t, db, database.Organization{
|
|
ID: orgID,
|
|
})
|
|
}
|
|
|
|
_, err = db.InsertAllUsersGroup(context.Background(), org.ID)
|
|
if !database.IsUniqueViolation(err) {
|
|
require.NoError(t, err, "Everyone group for an org")
|
|
}
|
|
|
|
manager := runtimeconfig.NewManager()
|
|
orgResolver := manager.OrganizationResolver(db, org.ID)
|
|
if def.GroupSettings != nil {
|
|
err = s.Group.SetRuntimeValue(context.Background(), orgResolver, (*idpsync.GroupSyncSettings)(def.GroupSettings))
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
if def.RoleSettings != nil {
|
|
err = s.Role.SetRuntimeValue(context.Background(), orgResolver, def.RoleSettings)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
if !def.NotMember {
|
|
dbgen.OrganizationMember(t, db, database.OrganizationMember{
|
|
UserID: user.ID,
|
|
OrganizationID: org.ID,
|
|
})
|
|
}
|
|
|
|
if len(def.OrganizationRoles) > 0 {
|
|
_, err := db.UpdateMemberRoles(context.Background(), database.UpdateMemberRolesParams{
|
|
GrantedRoles: def.OrganizationRoles,
|
|
UserID: user.ID,
|
|
OrgID: org.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
if len(def.CustomRoles) > 0 {
|
|
for _, cr := range def.CustomRoles {
|
|
_, err := db.InsertCustomRole(context.Background(), database.InsertCustomRoleParams{
|
|
Name: cr,
|
|
DisplayName: cr,
|
|
OrganizationID: uuid.NullUUID{
|
|
UUID: org.ID,
|
|
Valid: true,
|
|
},
|
|
SitePermissions: nil,
|
|
OrgPermissions: nil,
|
|
UserPermissions: nil,
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
for groupID, in := range def.Groups {
|
|
dbgen.Group(t, db, database.Group{
|
|
ID: groupID,
|
|
OrganizationID: org.ID,
|
|
})
|
|
if in {
|
|
dbgen.GroupMember(t, db, database.GroupMemberTable{
|
|
UserID: user.ID,
|
|
GroupID: groupID,
|
|
})
|
|
}
|
|
}
|
|
for groupName, in := range def.GroupNames {
|
|
group := dbgen.Group(t, db, database.Group{
|
|
Name: groupName,
|
|
OrganizationID: org.ID,
|
|
})
|
|
if in {
|
|
dbgen.GroupMember(t, db, database.GroupMemberTable{
|
|
UserID: user.ID,
|
|
GroupID: group.ID,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
type orgSetupDefinition struct {
|
|
Name string
|
|
// True if the user is a member of the group
|
|
Groups map[uuid.UUID]bool
|
|
GroupNames map[string]bool
|
|
OrganizationRoles []string
|
|
CustomRoles []string
|
|
// NotMember if true will ensure the user is not a member of the organization.
|
|
NotMember bool
|
|
|
|
GroupSettings *codersdk.GroupSyncSettings
|
|
RoleSettings *idpsync.RoleSyncSettings
|
|
|
|
assertGroups *orgGroupAssert
|
|
assertRoles *orgRoleAssert
|
|
}
|
|
|
|
type orgRoleAssert struct {
|
|
ExpectedOrgRoles []string
|
|
}
|
|
|
|
type orgGroupAssert struct {
|
|
ExpectedGroups []uuid.UUID
|
|
ExpectedGroupNames []string
|
|
}
|
|
|
|
func (o orgSetupDefinition) Assert(t *testing.T, orgID uuid.UUID, db database.Store, user database.User) {
|
|
t.Helper()
|
|
|
|
ctx := context.Background()
|
|
|
|
members, err := db.OrganizationMembers(ctx, database.OrganizationMembersParams{
|
|
OrganizationID: orgID,
|
|
UserID: user.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
if o.NotMember {
|
|
require.Len(t, members, 0, "should not be a member")
|
|
} else {
|
|
require.Len(t, members, 1, "should be a member")
|
|
}
|
|
|
|
if o.assertGroups != nil {
|
|
o.assertGroups.Assert(t, orgID, db, user)
|
|
}
|
|
if o.assertRoles != nil {
|
|
o.assertRoles.Assert(t, orgID, db, o.NotMember, user)
|
|
}
|
|
|
|
// If the user is not a member, there is nothing to really assert in the org
|
|
if o.assertGroups == nil && o.assertRoles == nil && !o.NotMember {
|
|
t.Errorf("no group or role asserts present, must have at least one")
|
|
t.FailNow()
|
|
}
|
|
}
|
|
|
|
func (o *orgGroupAssert) Assert(t *testing.T, orgID uuid.UUID, db database.Store, user database.User) {
|
|
t.Helper()
|
|
|
|
ctx := context.Background()
|
|
|
|
userGroups, err := db.GetGroups(ctx, database.GetGroupsParams{
|
|
OrganizationID: orgID,
|
|
HasMemberID: user.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
if o.ExpectedGroups == nil {
|
|
o.ExpectedGroups = make([]uuid.UUID, 0)
|
|
}
|
|
if len(o.ExpectedGroupNames) > 0 && len(o.ExpectedGroups) > 0 {
|
|
t.Fatal("ExpectedGroups and ExpectedGroupNames are mutually exclusive")
|
|
}
|
|
|
|
// Everyone groups mess up our asserts
|
|
userGroups = slices.DeleteFunc(userGroups, func(row database.GetGroupsRow) bool {
|
|
return row.Group.ID == row.Group.OrganizationID
|
|
})
|
|
|
|
if len(o.ExpectedGroupNames) > 0 {
|
|
found := db2sdk.List(userGroups, func(g database.GetGroupsRow) string {
|
|
return g.Group.Name
|
|
})
|
|
require.ElementsMatch(t, o.ExpectedGroupNames, found, "user groups by name")
|
|
require.Len(t, o.ExpectedGroups, 0, "ExpectedGroups should be empty")
|
|
} else {
|
|
// Check by ID, recommended
|
|
found := db2sdk.List(userGroups, func(g database.GetGroupsRow) uuid.UUID {
|
|
return g.Group.ID
|
|
})
|
|
require.ElementsMatch(t, o.ExpectedGroups, found, "user groups")
|
|
require.Len(t, o.ExpectedGroupNames, 0, "ExpectedGroupNames should be empty")
|
|
}
|
|
}
|
|
|
|
//nolint:revive
|
|
func (o orgRoleAssert) Assert(t *testing.T, orgID uuid.UUID, db database.Store, notMember bool, user database.User) {
|
|
t.Helper()
|
|
|
|
ctx := context.Background()
|
|
|
|
members, err := db.OrganizationMembers(ctx, database.OrganizationMembersParams{
|
|
OrganizationID: orgID,
|
|
UserID: user.ID,
|
|
})
|
|
if notMember {
|
|
require.ErrorIs(t, err, sql.ErrNoRows)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
require.Len(t, members, 1)
|
|
member := members[0]
|
|
require.ElementsMatch(t, member.OrganizationMember.Roles, o.ExpectedOrgRoles)
|
|
}
|