Files
coder/coderd/idpsync/role_test.go
T
Spike Curtis bddb808b25 chore: arrange imports in a standard way (#21452)
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.
2026-01-08 15:24:11 +04:00

367 lines
9.7 KiB
Go

package idpsync_test
import (
"context"
"encoding/json"
"slices"
"testing"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"cdr.dev/slog/v3/sloggers/slogtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/database/dbauthz"
"github.com/coder/coder/v2/coderd/database/dbgen"
"github.com/coder/coder/v2/coderd/database/dbmock"
"github.com/coder/coder/v2/coderd/database/dbtestutil"
"github.com/coder/coder/v2/coderd/idpsync"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/runtimeconfig"
"github.com/coder/coder/v2/testutil"
)
//nolint:paralleltest, tparallel
func TestRoleSyncTable(t *testing.T) {
t.Parallel()
userClaims := jwt.MapClaims{
"roles": []string{
"foo", "bar", "baz",
"create-bar", "create-baz",
"legacy-bar", rbac.RoleOrgAuditor(),
},
// bad-claim is a number, and will fail any role sync
"bad-claim": 100,
"empty": []string{},
}
testCases := []orgSetupDefinition{
{
Name: "NoSync",
OrganizationRoles: []string{},
assertRoles: &orgRoleAssert{
ExpectedOrgRoles: []string{},
},
},
{
Name: "SyncDisabled",
OrganizationRoles: []string{
rbac.RoleOrgAdmin(),
},
RoleSettings: &idpsync.RoleSyncSettings{},
assertRoles: &orgRoleAssert{
ExpectedOrgRoles: []string{
rbac.RoleOrgAdmin(),
},
},
},
{
// Audit role from claim
Name: "RawAudit",
OrganizationRoles: []string{
rbac.RoleOrgAdmin(),
},
RoleSettings: &idpsync.RoleSyncSettings{
Field: "roles",
Mapping: map[string][]string{},
},
assertRoles: &orgRoleAssert{
ExpectedOrgRoles: []string{
rbac.RoleOrgAuditor(),
},
},
},
{
Name: "CustomRole",
OrganizationRoles: []string{
rbac.RoleOrgAdmin(),
},
CustomRoles: []string{"foo"},
RoleSettings: &idpsync.RoleSyncSettings{
Field: "roles",
Mapping: map[string][]string{},
},
assertRoles: &orgRoleAssert{
ExpectedOrgRoles: []string{
rbac.RoleOrgAuditor(),
"foo",
},
},
},
{
Name: "RoleMapping",
OrganizationRoles: []string{
rbac.RoleOrgAdmin(),
"invalid", // Throw in an extra invalid role that will be removed
},
CustomRoles: []string{"custom"},
RoleSettings: &idpsync.RoleSyncSettings{
Field: "roles",
Mapping: map[string][]string{
"foo": {"custom", rbac.RoleOrgTemplateAdmin()},
},
},
assertRoles: &orgRoleAssert{
ExpectedOrgRoles: []string{
rbac.RoleOrgAuditor(),
rbac.RoleOrgTemplateAdmin(),
"custom",
},
},
},
{
// InvalidClaims will log an error, but do not block authentication.
// This is to prevent a misconfigured organization from blocking
// a user from authenticating.
Name: "InvalidClaim",
OrganizationRoles: []string{rbac.RoleOrgAdmin()},
RoleSettings: &idpsync.RoleSyncSettings{
Field: "bad-claim",
},
assertRoles: &orgRoleAssert{
ExpectedOrgRoles: []string{
rbac.RoleOrgAdmin(),
},
},
},
{
Name: "NoChange",
OrganizationRoles: []string{rbac.RoleOrgAdmin(), rbac.RoleOrgTemplateAdmin(), rbac.RoleOrgAuditor()},
RoleSettings: &idpsync.RoleSyncSettings{
Field: "roles",
Mapping: map[string][]string{
"foo": {rbac.RoleOrgAuditor(), rbac.RoleOrgTemplateAdmin()},
"bar": {rbac.RoleOrgAdmin()},
},
},
assertRoles: &orgRoleAssert{
ExpectedOrgRoles: []string{
rbac.RoleOrgAdmin(), rbac.RoleOrgAuditor(), rbac.RoleOrgTemplateAdmin(),
},
},
},
{
// InvalidOriginalRole starts the user with an invalid role.
// In practice, this should not happen, as it means a role was
// inserted into the database that does not exist.
// For the purposes of syncing, it does not matter, and the sync
// should succeed.
Name: "InvalidOriginalRole",
OrganizationRoles: []string{"something-bad"},
RoleSettings: &idpsync.RoleSyncSettings{
Field: "roles",
Mapping: map[string][]string{},
},
assertRoles: &orgRoleAssert{
ExpectedOrgRoles: []string{
rbac.RoleOrgAuditor(),
},
},
},
{
Name: "NonExistentClaim",
OrganizationRoles: []string{rbac.RoleOrgAuditor()},
RoleSettings: &idpsync.RoleSyncSettings{
Field: "not-exists",
Mapping: map[string][]string{},
},
assertRoles: &orgRoleAssert{
ExpectedOrgRoles: []string{},
},
},
{
Name: "EmptyClaim",
OrganizationRoles: []string{rbac.RoleOrgAuditor()},
RoleSettings: &idpsync.RoleSyncSettings{
Field: "empty",
Mapping: map[string][]string{},
},
assertRoles: &orgRoleAssert{
ExpectedOrgRoles: []string{},
},
},
}
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{
IgnoreErrors: true,
}),
manager,
idpsync.DeploymentSyncSettings{
SiteRoleField: "roles",
},
)
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 role sync!
err := s.SyncRoles(ctx, db, user, idpsync.RoleParams{
SyncEntitled: true,
SyncSiteWide: false,
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{
IgnoreErrors: true,
}),
manager,
// Also sync some site wide roles
idpsync.DeploymentSyncSettings{
GroupField: "groups",
SiteRoleField: "roles",
// Site sync settings do not matter,
// as we are not testing the site parse here.
// Only the sync, assuming the parse is correct.
},
)
ctx := testutil.Context(t, testutil.WaitSuperLong)
user := dbgen.User(t, db, database.User{})
var asserts []func(t *testing.T)
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)
})
})
}
err := s.SyncRoles(ctx, db, user, idpsync.RoleParams{
SyncEntitled: true,
SyncSiteWide: true,
SiteWideRoles: []string{
rbac.RoleTemplateAdmin().Name, // Duplicate this value to test deduplication
rbac.RoleTemplateAdmin().Name, rbac.RoleAuditor().Name,
},
MergedClaims: userClaims,
})
require.NoError(t, err)
for _, assert := range asserts {
assert(t)
}
// Also assert site wide roles
allRoles, err := db.GetAuthorizationUserRoles(dbauthz.AsSystemRestricted(ctx), user.ID)
require.NoError(t, err)
allRoleIDs, err := allRoles.RoleNames()
require.NoError(t, err)
// Remove the org roles
siteRoles := slices.DeleteFunc(allRoleIDs, func(r rbac.RoleIdentifier) bool {
return r.IsOrgRole()
})
require.ElementsMatch(t, []rbac.RoleIdentifier{
rbac.RoleTemplateAdmin(), rbac.RoleAuditor(), rbac.RoleMember(),
}, siteRoles)
})
}
// TestNoopNoDiff verifies if no role change occurs, no database call is taken
// per organization. This limits the number of db calls to O(1) if there
// are no changes. Which is the usual case, as user's roles do not change often.
func TestNoopNoDiff(t *testing.T) {
t.Parallel()
ctx := context.Background()
ctrl := gomock.NewController(t)
mDB := dbmock.NewMockStore(ctrl)
mgr := runtimeconfig.NewManager()
s := idpsync.NewAGPLSync(slogtest.Make(t, &slogtest.Options{}), mgr, idpsync.DeploymentSyncSettings{
SiteRoleField: "",
SiteRoleMapping: nil,
SiteDefaultRoles: nil,
})
userID := uuid.New()
orgID := uuid.New()
siteRoles := []string{rbac.RoleTemplateAdmin().Name, rbac.RoleAuditor().Name}
orgRoles := []string{rbac.RoleOrgAuditor(), rbac.RoleOrgAdmin()}
// The DB mock expects.
// If this test fails, feel free to add more expectations.
// The primary expectations to avoid is 'UpdateUserRoles'
// and 'UpdateMemberRoles'.
mDB.EXPECT().InTx(
gomock.Any(), gomock.Any(),
).DoAndReturn(func(f func(database.Store) error, _ *database.TxOptions) error {
err := f(mDB)
return err
})
mDB.EXPECT().OrganizationMembers(gomock.Any(), database.OrganizationMembersParams{
UserID: userID,
}).Return([]database.OrganizationMembersRow{
{
OrganizationMember: database.OrganizationMember{
UserID: userID,
OrganizationID: orgID,
Roles: orgRoles,
},
},
}, nil)
mDB.EXPECT().GetRuntimeConfig(gomock.Any(), gomock.Any()).Return(
string(must(json.Marshal(idpsync.RoleSyncSettings{
Field: "roles",
Mapping: nil,
}))), nil)
err := s.SyncRoles(ctx, mDB, database.User{
ID: userID,
Email: "alice@email.com",
Username: "alice",
Status: database.UserStatusActive,
RBACRoles: siteRoles,
LoginType: database.LoginTypePassword,
}, idpsync.RoleParams{
SyncEntitled: true,
SyncSiteWide: true,
SiteWideRoles: siteRoles,
MergedClaims: jwt.MapClaims{
"roles": orgRoles,
},
})
require.NoError(t, err)
}
func must[T any](value T, err error) T {
if err != nil {
panic(err)
}
return value
}