Skip to content

Commit 541a9f3

Browse files
committed
fixup! refactor group sync
1 parent 7563073 commit 541a9f3

File tree

1 file changed

+108
-2
lines changed

1 file changed

+108
-2
lines changed

enterprise/coderd/idp-sync/idpsync.go

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ import (
44
"context"
55
"regexp"
66

7+
"github.com/google/uuid"
8+
"golang.org/x/xerrors"
9+
10+
"cdr.dev/slog"
711
"github.com/coder/coder/v2/coderd/database"
12+
"github.com/coder/coder/v2/coderd/database/db2sdk"
13+
"github.com/coder/coder/v2/coderd/database/dbauthz"
14+
"github.com/coder/coder/v2/coderd/util/slice"
815
"github.com/coder/coder/v2/codersdk"
916
)
1017

@@ -14,7 +21,12 @@ type IDPSync struct {
1421

1522
// SynchronizeGroupsParams
1623
type SynchronizeGroupsParams struct {
17-
IDPGroups []string
24+
UserID uuid.UUID
25+
// CoderGroups should be the coder groups to map a user into.
26+
// TODO: This function should really take the raw IDP groups, but that
27+
// is handled by the caller of `oauthLogin` atm. Need to plumb
28+
// that through to here.
29+
CoderGroups []string
1830
// TODO: These options will be moved outside of deployment into organization
1931
// scoped settings. So these parameters will be removed, and instead sourced
2032
// from some settings object that should have these values cached.
@@ -25,7 +37,101 @@ type SynchronizeGroupsParams struct {
2537

2638
// SynchronizeGroups takes a given user, and ensures their group memberships
2739
// within a given organization are correct.
28-
func SynchronizeGroups(ctx context.Context, tx database.Store, params *SynchronizeGroupsParams) error {
40+
func SynchronizeGroups(ctx context.Context, logger slog.Logger, tx database.Store, params *SynchronizeGroupsParams) error {
41+
//nolint:gocritic // group sync happens as a system operation.
42+
ctx = dbauthz.AsSystemRestricted(ctx)
43+
44+
wantGroups := params.CoderGroups
45+
46+
// Apply regex filter if applicable
47+
if params.GroupFilter != nil {
48+
wantGroups = make([]string, 0, len(params.CoderGroups))
49+
for _, group := range params.CoderGroups {
50+
if params.GroupFilter.MatchString(group) {
51+
wantGroups = append(wantGroups, group)
52+
}
53+
}
54+
}
55+
56+
// By default, group sync applies only to the default organization.
57+
defaultOrganization, err := tx.GetDefaultOrganization(ctx)
58+
if err != nil {
59+
// If there is no default org, then we can't assign groups.
60+
// By default, we assume all groups belong to the default org.
61+
return xerrors.Errorf("get default organization: %w", err)
62+
}
63+
64+
memberships, err := tx.OrganizationMembers(dbauthz.AsSystemRestricted(ctx), database.OrganizationMembersParams{
65+
UserID: params.UserID,
66+
OrganizationID: defaultOrganization.ID,
67+
})
68+
if err != nil {
69+
return xerrors.Errorf("get user memberships: %w", err)
70+
}
71+
72+
// If the user is not in the default organization, then we can't assign groups.
73+
// A user cannot be in groups to an org they are not a member of.
74+
if len(memberships) == 0 {
75+
return xerrors.Errorf("user %s is not a member of the default organization, cannot assign to groups in the org", params.UserID)
76+
}
77+
78+
userGroups, err := tx.GetGroups(ctx, database.GetGroupsParams{
79+
OrganizationID: defaultOrganization.ID,
80+
HasMemberID: params.UserID,
81+
})
82+
83+
userGroupNames := db2sdk.List(userGroups, func(g database.Group) string {
84+
return g.Name
85+
})
86+
87+
// Optimize for the case the user is in the correct groups, since
88+
// group membership is mostly unchanging.
89+
add, remove := slice.SymmetricDifference(userGroupNames, wantGroups)
90+
if len(add) == 0 && len(remove) == 0 {
91+
// Add done, the user is in all the correct groups! Do not waste any more db
92+
// calls.
93+
return nil
94+
}
95+
96+
// We could only insert the user to missing groups, and remove them from the excess.
97+
// But that is at minimum 1 db call, and it's only 2 if we delete them from all groups,
98+
// then re-add them to the correct groups. So we just do the latter.
99+
100+
// Just remove the user from all their groups, then add them back in.
101+
err = tx.RemoveUserFromAllGroups(ctx, database.RemoveUserFromAllGroupsParams{
102+
UserID: params.UserID,
103+
OrganizationID: defaultOrganization.ID,
104+
})
105+
if err != nil {
106+
return xerrors.Errorf("delete user groups: %w", err)
107+
}
108+
109+
if params.CreateMissingGroups {
110+
created, err := tx.InsertMissingGroups(ctx, database.InsertMissingGroupsParams{
111+
OrganizationID: defaultOrganization.ID,
112+
GroupNames: wantGroups,
113+
Source: database.GroupSourceOidc,
114+
})
115+
if err != nil {
116+
return xerrors.Errorf("insert missing groups: %w", err)
117+
}
118+
if len(created) > 0 {
119+
logger.Debug(ctx, "auto created missing groups",
120+
slog.F("org_id", defaultOrganization.ID.ID),
121+
slog.F("created", created),
122+
slog.F("num", len(created)),
123+
)
124+
}
125+
}
126+
127+
err = tx.InsertUserGroupsByName(ctx, database.InsertUserGroupsByNameParams{
128+
UserID: params.UserID,
129+
OrganizationID: defaultOrganization.ID,
130+
GroupNames: wantGroups,
131+
})
132+
if err != nil {
133+
return xerrors.Errorf("insert user groups: %w", err)
134+
}
29135

30136
return nil
31137
}

0 commit comments

Comments
 (0)