@@ -4,7 +4,14 @@ import (
4
4
"context"
5
5
"regexp"
6
6
7
+ "github.com/google/uuid"
8
+ "golang.org/x/xerrors"
9
+
10
+ "cdr.dev/slog"
7
11
"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"
8
15
"github.com/coder/coder/v2/codersdk"
9
16
)
10
17
@@ -14,7 +21,12 @@ type IDPSync struct {
14
21
15
22
// SynchronizeGroupsParams
16
23
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
18
30
// TODO: These options will be moved outside of deployment into organization
19
31
// scoped settings. So these parameters will be removed, and instead sourced
20
32
// from some settings object that should have these values cached.
@@ -25,7 +37,101 @@ type SynchronizeGroupsParams struct {
25
37
26
38
// SynchronizeGroups takes a given user, and ensures their group memberships
27
39
// 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
+ }
29
135
30
136
return nil
31
137
}
0 commit comments