Skip to content

Commit 7139374

Browse files
authored
feat: implement organization role sync (#14649)
* chore: implement organization and site wide role sync in idpsync * chore: remove old role sync, insert new idpsync package
1 parent 5aa54be commit 7139374

File tree

16 files changed

+1159
-223
lines changed

16 files changed

+1159
-223
lines changed

cli/server.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,6 @@ func createOIDCConfig(ctx context.Context, logger slog.Logger, vals *codersdk.De
187187
EmailField: vals.OIDC.EmailField.String(),
188188
AuthURLParams: vals.OIDC.AuthURLParams.Value,
189189
IgnoreUserInfo: vals.OIDC.IgnoreUserInfo.Value(),
190-
UserRoleField: vals.OIDC.UserRoleField.String(),
191-
UserRoleMapping: vals.OIDC.UserRoleMapping.Value,
192-
UserRolesDefault: vals.OIDC.UserRolesDefault.GetSlice(),
193190
SignInText: vals.OIDC.SignInText.String(),
194191
SignupsDisabledText: vals.OIDC.SignupsDisabledText.String(),
195192
IconURL: vals.OIDC.IconURL.String(),

coderd/coderd.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,6 @@ type Options struct {
181181
NetworkTelemetryBatchFrequency time.Duration
182182
NetworkTelemetryBatchMaxSize int
183183
SwaggerEndpoint bool
184-
SetUserSiteRoles func(ctx context.Context, logger slog.Logger, tx database.Store, userID uuid.UUID, roles []string) error
185184
TemplateScheduleStore *atomic.Pointer[schedule.TemplateScheduleStore]
186185
UserQuietHoursScheduleStore *atomic.Pointer[schedule.UserQuietHoursScheduleStore]
187186
AccessControlStore *atomic.Pointer[dbauthz.AccessControlStore]
@@ -373,14 +372,6 @@ func New(options *Options) *API {
373372
if options.TracerProvider == nil {
374373
options.TracerProvider = trace.NewNoopTracerProvider()
375374
}
376-
if options.SetUserSiteRoles == nil {
377-
options.SetUserSiteRoles = func(ctx context.Context, logger slog.Logger, _ database.Store, userID uuid.UUID, roles []string) error {
378-
logger.Warn(ctx, "attempted to assign OIDC user roles without enterprise license",
379-
slog.F("user_id", userID), slog.F("roles", roles),
380-
)
381-
return nil
382-
}
383-
}
384375
if options.TemplateScheduleStore == nil {
385376
options.TemplateScheduleStore = &atomic.Pointer[schedule.TemplateScheduleStore]{}
386377
}

coderd/database/dbmem/dbmem.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8730,7 +8730,7 @@ func (q *FakeQuerier) UpdateUserRoles(_ context.Context, arg database.UpdateUser
87308730
}
87318731

87328732
// Set new roles
8733-
user.RBACRoles = arg.GrantedRoles
8733+
user.RBACRoles = slice.Unique(arg.GrantedRoles)
87348734
// Remove duplicates and sort
87358735
uniqueRoles := make([]string, 0, len(user.RBACRoles))
87368736
exist := make(map[string]struct{})

coderd/idpsync/group_test.go

Lines changed: 148 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func TestGroupSyncTable(t *testing.T) {
8585
testCases := []orgSetupDefinition{
8686
{
8787
Name: "SwitchGroups",
88-
Settings: &codersdk.GroupSyncSettings{
88+
GroupSettings: &codersdk.GroupSyncSettings{
8989
Field: "groups",
9090
Mapping: map[string][]uuid.UUID{
9191
"foo": {ids.ID("sg-foo"), ids.ID("sg-foo-2")},
@@ -102,16 +102,18 @@ func TestGroupSyncTable(t *testing.T) {
102102
ids.ID("sg-bar"): false,
103103
ids.ID("sg-baz"): false,
104104
},
105-
ExpectedGroups: []uuid.UUID{
106-
ids.ID("sg-foo"),
107-
ids.ID("sg-foo-2"),
108-
ids.ID("sg-bar"),
109-
ids.ID("sg-baz"),
105+
assertGroups: &orgGroupAssert{
106+
ExpectedGroups: []uuid.UUID{
107+
ids.ID("sg-foo"),
108+
ids.ID("sg-foo-2"),
109+
ids.ID("sg-bar"),
110+
ids.ID("sg-baz"),
111+
},
110112
},
111113
},
112114
{
113115
Name: "StayInGroup",
114-
Settings: &codersdk.GroupSyncSettings{
116+
GroupSettings: &codersdk.GroupSyncSettings{
115117
Field: "groups",
116118
// Only match foo, so bar does not map
117119
RegexFilter: regexp.MustCompile("^foo$"),
@@ -125,13 +127,15 @@ func TestGroupSyncTable(t *testing.T) {
125127
ids.ID("gg-foo"): true,
126128
ids.ID("gg-bar"): false,
127129
},
128-
ExpectedGroups: []uuid.UUID{
129-
ids.ID("gg-foo"),
130+
assertGroups: &orgGroupAssert{
131+
ExpectedGroups: []uuid.UUID{
132+
ids.ID("gg-foo"),
133+
},
130134
},
131135
},
132136
{
133137
Name: "UserJoinsGroups",
134-
Settings: &codersdk.GroupSyncSettings{
138+
GroupSettings: &codersdk.GroupSyncSettings{
135139
Field: "groups",
136140
Mapping: map[string][]uuid.UUID{
137141
"foo": {ids.ID("ng-foo"), uuid.New()},
@@ -145,29 +149,33 @@ func TestGroupSyncTable(t *testing.T) {
145149
ids.ID("ng-bar-2"): false,
146150
ids.ID("ng-baz"): false,
147151
},
148-
ExpectedGroups: []uuid.UUID{
149-
ids.ID("ng-foo"),
150-
ids.ID("ng-bar"),
151-
ids.ID("ng-bar-2"),
152-
ids.ID("ng-baz"),
152+
assertGroups: &orgGroupAssert{
153+
ExpectedGroups: []uuid.UUID{
154+
ids.ID("ng-foo"),
155+
ids.ID("ng-bar"),
156+
ids.ID("ng-bar-2"),
157+
ids.ID("ng-baz"),
158+
},
153159
},
154160
},
155161
{
156162
Name: "CreateGroups",
157-
Settings: &codersdk.GroupSyncSettings{
163+
GroupSettings: &codersdk.GroupSyncSettings{
158164
Field: "groups",
159165
RegexFilter: regexp.MustCompile("^create"),
160166
AutoCreateMissing: true,
161167
},
162168
Groups: map[uuid.UUID]bool{},
163-
ExpectedGroupNames: []string{
164-
"create-bar",
165-
"create-baz",
169+
assertGroups: &orgGroupAssert{
170+
ExpectedGroupNames: []string{
171+
"create-bar",
172+
"create-baz",
173+
},
166174
},
167175
},
168176
{
169177
Name: "GroupNamesNoMapping",
170-
Settings: &codersdk.GroupSyncSettings{
178+
GroupSettings: &codersdk.GroupSyncSettings{
171179
Field: "groups",
172180
RegexFilter: regexp.MustCompile(".*"),
173181
AutoCreateMissing: false,
@@ -177,14 +185,16 @@ func TestGroupSyncTable(t *testing.T) {
177185
"bar": false,
178186
"goob": true,
179187
},
180-
ExpectedGroupNames: []string{
181-
"foo",
182-
"bar",
188+
assertGroups: &orgGroupAssert{
189+
ExpectedGroupNames: []string{
190+
"foo",
191+
"bar",
192+
},
183193
},
184194
},
185195
{
186196
Name: "NoUser",
187-
Settings: &codersdk.GroupSyncSettings{
197+
GroupSettings: &codersdk.GroupSyncSettings{
188198
Field: "groups",
189199
Mapping: map[string][]uuid.UUID{
190200
// Extra ID that does not map to a group
@@ -200,13 +210,16 @@ func TestGroupSyncTable(t *testing.T) {
200210
},
201211
},
202212
{
203-
Name: "NoSettingsNoUser",
204-
Settings: nil,
205-
Groups: map[uuid.UUID]bool{},
213+
Name: "NoSettings",
214+
GroupSettings: nil,
215+
Groups: map[uuid.UUID]bool{},
216+
assertGroups: &orgGroupAssert{
217+
ExpectedGroups: []uuid.UUID{},
218+
},
206219
},
207220
{
208221
Name: "LegacyMapping",
209-
Settings: &codersdk.GroupSyncSettings{
222+
GroupSettings: &codersdk.GroupSyncSettings{
210223
Field: "groups",
211224
RegexFilter: regexp.MustCompile("^legacy"),
212225
LegacyNameMapping: map[string]string{
@@ -224,9 +237,11 @@ func TestGroupSyncTable(t *testing.T) {
224237
"extra": true,
225238
"legacy-bop": true,
226239
},
227-
ExpectedGroupNames: []string{
228-
"legacy-bar",
229-
"legacy-foo",
240+
assertGroups: &orgGroupAssert{
241+
ExpectedGroupNames: []string{
242+
"legacy-bar",
243+
"legacy-foo",
244+
},
230245
},
231246
},
232247
}
@@ -311,9 +326,10 @@ func TestGroupSyncTable(t *testing.T) {
311326
"random": true,
312327
},
313328
// No settings, because they come from the deployment values
314-
Settings: nil,
315-
ExpectedGroups: nil,
316-
ExpectedGroupNames: []string{"legacy-foo", "legacy-baz", "legacy-bar"},
329+
GroupSettings: nil,
330+
assertGroups: &orgGroupAssert{
331+
ExpectedGroupNames: []string{"legacy-foo", "legacy-baz", "legacy-bar"},
332+
},
317333
}
318334

319335
//nolint:gocritic // testing
@@ -385,16 +401,18 @@ func TestSyncDisabled(t *testing.T) {
385401
ids.ID("baz"): false,
386402
ids.ID("bop"): false,
387403
},
388-
Settings: &codersdk.GroupSyncSettings{
404+
GroupSettings: &codersdk.GroupSyncSettings{
389405
Field: "groups",
390406
Mapping: map[string][]uuid.UUID{
391407
"foo": {ids.ID("foo")},
392408
"baz": {ids.ID("baz")},
393409
},
394410
},
395-
ExpectedGroups: []uuid.UUID{
396-
ids.ID("foo"),
397-
ids.ID("bar"),
411+
assertGroups: &orgGroupAssert{
412+
ExpectedGroups: []uuid.UUID{
413+
ids.ID("foo"),
414+
ids.ID("bar"),
415+
},
398416
},
399417
}
400418

@@ -728,9 +746,14 @@ func SetupOrganization(t *testing.T, s *idpsync.AGPLIDPSync, db database.Store,
728746
}
729747

730748
manager := runtimeconfig.NewManager()
731-
if def.Settings != nil {
732-
orgResolver := manager.OrganizationResolver(db, org.ID)
733-
err = s.Group.SetRuntimeValue(context.Background(), orgResolver, (*idpsync.GroupSyncSettings)(def.Settings))
749+
orgResolver := manager.OrganizationResolver(db, org.ID)
750+
if def.GroupSettings != nil {
751+
err = s.Group.SetRuntimeValue(context.Background(), orgResolver, (*idpsync.GroupSyncSettings)(def.GroupSettings))
752+
require.NoError(t, err)
753+
}
754+
755+
if def.RoleSettings != nil {
756+
err = s.Role.SetRuntimeValue(context.Background(), orgResolver, def.RoleSettings)
734757
require.NoError(t, err)
735758
}
736759

@@ -740,6 +763,33 @@ func SetupOrganization(t *testing.T, s *idpsync.AGPLIDPSync, db database.Store,
740763
OrganizationID: org.ID,
741764
})
742765
}
766+
767+
if len(def.OrganizationRoles) > 0 {
768+
_, err := db.UpdateMemberRoles(context.Background(), database.UpdateMemberRolesParams{
769+
GrantedRoles: def.OrganizationRoles,
770+
UserID: user.ID,
771+
OrgID: org.ID,
772+
})
773+
require.NoError(t, err)
774+
}
775+
776+
if len(def.CustomRoles) > 0 {
777+
for _, cr := range def.CustomRoles {
778+
_, err := db.InsertCustomRole(context.Background(), database.InsertCustomRoleParams{
779+
Name: cr,
780+
DisplayName: cr,
781+
OrganizationID: uuid.NullUUID{
782+
UUID: org.ID,
783+
Valid: true,
784+
},
785+
SitePermissions: nil,
786+
OrgPermissions: nil,
787+
UserPermissions: nil,
788+
})
789+
require.NoError(t, err)
790+
}
791+
}
792+
743793
for groupID, in := range def.Groups {
744794
dbgen.Group(t, db, database.Group{
745795
ID: groupID,
@@ -769,11 +819,25 @@ func SetupOrganization(t *testing.T, s *idpsync.AGPLIDPSync, db database.Store,
769819
type orgSetupDefinition struct {
770820
Name string
771821
// True if the user is a member of the group
772-
Groups map[uuid.UUID]bool
773-
GroupNames map[string]bool
774-
NotMember bool
822+
Groups map[uuid.UUID]bool
823+
GroupNames map[string]bool
824+
OrganizationRoles []string
825+
CustomRoles []string
826+
// NotMember if true will ensure the user is not a member of the organization.
827+
NotMember bool
828+
829+
GroupSettings *codersdk.GroupSyncSettings
830+
RoleSettings *idpsync.RoleSyncSettings
831+
832+
assertGroups *orgGroupAssert
833+
assertRoles *orgRoleAssert
834+
}
835+
836+
type orgRoleAssert struct {
837+
ExpectedOrgRoles []string
838+
}
775839

776-
Settings *codersdk.GroupSyncSettings
840+
type orgGroupAssert struct {
777841
ExpectedGroups []uuid.UUID
778842
ExpectedGroupNames []string
779843
}
@@ -794,6 +858,25 @@ func (o orgSetupDefinition) Assert(t *testing.T, orgID uuid.UUID, db database.St
794858
require.Len(t, members, 1, "should be a member")
795859
}
796860

861+
if o.assertGroups != nil {
862+
o.assertGroups.Assert(t, orgID, db, user)
863+
}
864+
if o.assertRoles != nil {
865+
o.assertRoles.Assert(t, orgID, db, o.NotMember, user)
866+
}
867+
868+
// If the user is not a member, there is nothing to really assert in the org
869+
if o.assertGroups == nil && o.assertRoles == nil && !o.NotMember {
870+
t.Errorf("no group or role asserts present, must have at least one")
871+
t.FailNow()
872+
}
873+
}
874+
875+
func (o orgGroupAssert) Assert(t *testing.T, orgID uuid.UUID, db database.Store, user database.User) {
876+
t.Helper()
877+
878+
ctx := context.Background()
879+
797880
userGroups, err := db.GetGroups(ctx, database.GetGroupsParams{
798881
OrganizationID: orgID,
799882
HasMemberID: user.ID,
@@ -826,3 +909,23 @@ func (o orgSetupDefinition) Assert(t *testing.T, orgID uuid.UUID, db database.St
826909
require.Len(t, o.ExpectedGroupNames, 0, "ExpectedGroupNames should be empty")
827910
}
828911
}
912+
913+
//nolint:revive
914+
func (o orgRoleAssert) Assert(t *testing.T, orgID uuid.UUID, db database.Store, notMember bool, user database.User) {
915+
t.Helper()
916+
917+
ctx := context.Background()
918+
919+
members, err := db.OrganizationMembers(ctx, database.OrganizationMembersParams{
920+
OrganizationID: orgID,
921+
UserID: user.ID,
922+
})
923+
if notMember {
924+
require.ErrorIs(t, err, sql.ErrNoRows)
925+
return
926+
}
927+
require.NoError(t, err)
928+
require.Len(t, members, 1)
929+
member := members[0]
930+
require.ElementsMatch(t, member.OrganizationMember.Roles, o.ExpectedOrgRoles)
931+
}

0 commit comments

Comments
 (0)