-
Notifications
You must be signed in to change notification settings - Fork 773
/
service.go
460 lines (411 loc) · 15.2 KB
/
service.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"reflect"
admin "google.golang.org/api/admin/directory/v1"
"google.golang.org/api/googleapi"
groupssettings "google.golang.org/api/groupssettings/v1"
"google.golang.org/api/option"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
)
const (
OwnerRole = "OWNER"
ManagerRole = "MANAGER"
MemberRole = "MEMBER"
)
// AdminService provides functionality to perform high level
// tasks using a AdminServiceClient.
type AdminService interface {
AddOrUpdateGroupMembers(group GoogleGroup, role string, members []string) error
CreateOrUpdateGroupIfNescessary(group GoogleGroup) error
DeleteGroupsIfNecessary() error
RemoveOwnerOrManagersFromGroup(group GoogleGroup, members []string) error
RemoveMembersFromGroup(group GoogleGroup, members []string) error
// ListGroup here is a proxy to the ListGroups method of the underlying
// AdminServiceClient being used.
ListGroups() (*admin.Groups, error)
// ListMembers here is a proxy to the ListMembers method of the underlying
// AdminServiceClient being used.
ListMembers(groupKey string) (*admin.Members, error)
}
// GroupService provides functionality to perform high level
// tasks using a GroupServiceClient.
type GroupService interface {
UpdateGroupSettings(group GoogleGroup) error
// Get here is a proxy to the Get method of the
// underlying GroupServiceClient
Get(groupUniqueID string) (*groupssettings.Groups, error)
}
func NewAdminService(ctx context.Context, clientOption option.ClientOption) (AdminService, error) {
client, err := NewAdminServiceClient(ctx, clientOption)
if err != nil {
return nil, err
}
return &adminService{client: client}, nil
}
func NewGroupService(ctx context.Context, clientOption option.ClientOption) (GroupService, error) {
client, err := NewGroupServiceClient(ctx, clientOption)
if err != nil {
return nil, err
}
return &groupService{client: client}, nil
}
type adminService struct {
client AdminServiceClient
}
// AddOrUpdateGroupMembers first lists all members that are part of group. Based on this list and the
// members, it will update the member in the group (if needed) or if the member is not found in the
// list, it will create the member.
func (as *adminService) AddOrUpdateGroupMembers(group GoogleGroup, role string, members []string) error {
if *verbose {
log.Printf("adminService.AddOrUpdateGroupMembers %s %s %v", group.EmailId, role, members)
}
l, err := as.client.ListMembers(group.EmailId)
if err != nil {
if apierr, ok := err.(*googleapi.Error); ok && apierr.Code == http.StatusNotFound {
log.Printf("skipping adding members to group %q as it has not yet been created\n", group.EmailId)
return nil
}
return fmt.Errorf("unable to retrieve members in group %q: %w", group.EmailId, err)
}
// aggregate the errors that occured and return them together in the end.
var errs []error
for _, memberEmailId := range members {
var member *admin.Member
for _, m := range l.Members {
if m.Email == memberEmailId {
member = m
break
}
}
if member != nil {
// update if necessary
if member.Role != role {
member.Role = role
if config.ConfirmChanges {
_, err := as.client.UpdateMember(group.EmailId, member.Email, member)
if err != nil {
errs = append(errs, fmt.Errorf("unable to update %s in %q as %s : %w", memberEmailId, group.EmailId, role, err))
continue
}
log.Printf("Updated %s to %q as a %s\n", memberEmailId, group.EmailId, role)
} else {
log.Printf("dry-run: would update %s to %q as %s\n", memberEmailId, group.EmailId, role)
}
}
continue
}
member = &admin.Member{
Email: memberEmailId,
Role: role,
}
// We did not find the person in the google group, so we add them
if config.ConfirmChanges {
_, err := as.client.InsertMember(group.EmailId, member)
if err != nil {
errs = append(errs, fmt.Errorf("unable to add %s to %q as %s : %w", memberEmailId, group.EmailId, role, err))
continue
}
log.Printf("Added %s to %q as a %s\n", memberEmailId, group.EmailId, role)
} else {
log.Printf("dry-run: would add %s to %q as %s\n", memberEmailId, group.EmailId, role)
}
}
return utilerrors.NewAggregate(errs)
}
// CreateOrUpdateGroupIfNescessary will create a group if the provided group's email ID
// does not already exist. If it exists, it will update the group if needed to match the
// provided group.
func (as *adminService) CreateOrUpdateGroupIfNescessary(group GoogleGroup) error {
if *verbose {
log.Printf("adminService.CreateOrUpdateGroupIfNecessary %s", group.EmailId)
}
grp, err := as.client.GetGroup(group.EmailId)
if err != nil {
if apierr, ok := err.(*googleapi.Error); ok && apierr.Code == http.StatusNotFound {
if !config.ConfirmChanges {
log.Printf("dry-run: would create group %q\n", group.EmailId)
} else {
log.Printf("Trying to create group: %q\n", group.EmailId)
g := admin.Group{
Email: group.EmailId,
}
if group.Name != "" {
g.Name = group.Name
}
if group.Description != "" {
g.Description = group.Description
}
g4, err := as.client.InsertGroup(&g)
if err != nil {
return fmt.Errorf("unable to add new group %q: %w", group.EmailId, err)
}
log.Printf("> Successfully created group %s\n", g4.Email)
}
} else {
return fmt.Errorf("unable to fetch group %q: %w", group.EmailId, err)
}
} else {
if group.Name != "" && grp.Name != group.Name ||
group.Description != "" && grp.Description != group.Description {
if !config.ConfirmChanges {
log.Printf("dry-run: would update group name/description %q\n", group.EmailId)
} else {
log.Printf("Trying to update group: %q\n", group.EmailId)
g := admin.Group{
Email: group.EmailId,
}
if group.Name != "" {
g.Name = group.Name
}
if group.Description != "" {
g.Description = group.Description
}
g4, err := as.client.UpdateGroup(group.EmailId, &g)
if err != nil {
return fmt.Errorf("unable to update group %q: %w", group.EmailId, err)
}
log.Printf("> Successfully updated group %s\n", g4.Email)
}
}
}
return nil
}
// DeleteGroupsIfNecessary checks against the groups config provided by the user. It
// first lists all existing groups, if a group in this list does not appear in the
// provided group config, it will delete this group to match the desired state.
func (as *adminService) DeleteGroupsIfNecessary() error {
g, err := as.client.ListGroups()
if err != nil {
return fmt.Errorf("unable to retrieve users in domain: %w", err)
}
// aggregate the errors that occured and return them together in the end.
var errs []error
for _, g := range g.Groups {
found := false
for _, g2 := range groupsConfig.Groups {
if g2.EmailId == g.Email {
found = true
break
}
}
if found {
continue
}
// We did not find the group in our groups.xml, so delete the group
if config.ConfirmChanges {
if *verbose {
log.Printf("deleting group %s", g.Email)
}
err := as.client.DeleteGroup(g.Email)
if err != nil {
errs = append(errs, fmt.Errorf("unable to remove group %s : %w", g.Email, err))
continue
}
log.Printf("Removing group %s\n", g.Email)
} else {
log.Printf("dry-run: would remove group %s\n", g.Email)
}
}
return utilerrors.NewAggregate(errs)
}
// RemoveOwnerOrManagersFromGroup lists members of the group and checks against the list of members
// passed. If a member from the retrieved list of members does not exist in the passed list of members,
// this member is removed - provided this member had a OWNER/MANAGER role.
func (as *adminService) RemoveOwnerOrManagersFromGroup(group GoogleGroup, members []string) error {
if *verbose {
log.Printf("adminService.RemoveOwnerOrManagersGroup %s %v", group.EmailId, members)
}
l, err := as.client.ListMembers(group.EmailId)
if err != nil {
if apierr, ok := err.(*googleapi.Error); ok && apierr.Code == http.StatusNotFound {
log.Printf("skipping removing members group %q as group has not yet been created\n", group.EmailId)
return nil
}
return fmt.Errorf("unable to retrieve members in group %q: %w", group.EmailId, err)
}
// aggregate the errors that occured and return them together in the end.
var errs []error
for _, m := range l.Members {
found := false
for _, m2 := range members {
if m2 == m.Email {
found = true
break
}
}
// If a member m exists in our desired list of members, do nothing.
// However, if this member m does not exist in our desired list of
// members but is in the role of a MEMBER (non OWNER/MANAGER), still
// do nothing since we care only about OWNER/MANAGER roles here.
if found || m.Role == MemberRole {
continue
}
// a person was deleted from a group, let's remove them
if config.ConfirmChanges {
err := as.client.DeleteMember(group.EmailId, m.Id)
if err != nil {
errs = append(errs, fmt.Errorf("unable to remove %s from %q as OWNER or MANAGER : %w", m.Email, group.EmailId, err))
continue
}
log.Printf("Removing %s from %q as OWNER or MANAGER\n", m.Email, group.EmailId)
} else {
log.Printf("dry-run: would remove %s from %q as OWNER or MANAGER\n", m.Email, group.EmailId)
}
}
return utilerrors.NewAggregate(errs)
}
// RemoveMembersFromGroup lists members of the group and checks against the list of members passed.
// If a member from the retrieved list of members does not exist in the passed list of members, this
// member is removed. Unlike RemoveOwnerOrManagersFromGroup, RemoveMembersFromGroup will remove the
// member regardless of the role that the member held.
func (as *adminService) RemoveMembersFromGroup(group GoogleGroup, members []string) error {
if *verbose {
log.Printf("adminService.RemoveMembersFromGroup %s %v", group.EmailId, members)
}
l, err := as.client.ListMembers(group.EmailId)
if err != nil {
if apierr, ok := err.(*googleapi.Error); ok && apierr.Code == http.StatusNotFound {
log.Printf("skipping removing members group %q as group has not yet been created\n", group.EmailId)
return nil
}
return fmt.Errorf("unable to retrieve members in group %q: %w", group.EmailId, err)
}
// aggregate the errors that occured and return them together in the end.
var errs []error
for _, m := range l.Members {
found := false
for _, m2 := range members {
if m2 == m.Email {
found = true
break
}
}
if found {
continue
}
// a person was deleted from a group, let's remove them
if config.ConfirmChanges {
err := as.client.DeleteMember(group.EmailId, m.Id)
if err != nil {
errs = append(errs, fmt.Errorf("unable to remove %s from %q as a %s : %w", m.Email, group.EmailId, m.Role, err))
continue
}
log.Printf("Removing %s from %q as a %s\n", m.Email, group.EmailId, m.Role)
} else {
log.Printf("dry-run: would remove %s from %q as a %s\n", m.Email, group.EmailId, m.Role)
}
}
return utilerrors.NewAggregate(errs)
}
// ListGroups lists all the groups available.
func (as *adminService) ListGroups() (*admin.Groups, error) {
return as.client.ListGroups()
}
// ListMembers lists all the members of a group with a particular groupKey.
func (as *adminService) ListMembers(groupKey string) (*admin.Members, error) {
return as.client.ListMembers(groupKey)
}
var _ AdminService = (*adminService)(nil)
type groupService struct {
client GroupServiceClient
}
// UpdateGroupSettings updates the groupsettings.Groups corresponding to the
// passed group based on what the current state of the groupsetting.Groups is.
func (gs *groupService) UpdateGroupSettings(group GoogleGroup) error {
if *verbose {
log.Printf("groupService.UpdateGroupSettings %s", group.EmailId)
}
g2, err := gs.client.Get(group.EmailId)
if err != nil {
if apierr, ok := err.(*googleapi.Error); ok && apierr.Code == http.StatusNotFound {
log.Printf("skipping updating group settings as group %q has not yet been created\n", group.EmailId)
return nil
}
return fmt.Errorf("unable to retrieve group info for group %q: %w", group.EmailId, err)
}
var (
haveSettings groupssettings.Groups
wantSettings groupssettings.Groups
)
// We copy the settings we get from the API into haveSettings, and then copy
// it again into wantSettings so we have a version we can manipulate.
deepCopySettings(&g2, &haveSettings)
deepCopySettings(&haveSettings, &wantSettings)
// This sets safe/sane defaults
wantSettings.AllowExternalMembers = "true"
wantSettings.WhoCanJoin = "INVITED_CAN_JOIN"
wantSettings.WhoCanViewMembership = "ALL_MANAGERS_CAN_VIEW"
wantSettings.WhoCanViewGroup = "ALL_MEMBERS_CAN_VIEW"
wantSettings.WhoCanDiscoverGroup = "ALL_IN_DOMAIN_CAN_DISCOVER"
wantSettings.WhoCanModerateMembers = "OWNERS_AND_MANAGERS"
wantSettings.WhoCanModerateContent = "OWNERS_AND_MANAGERS"
wantSettings.WhoCanPostMessage = "ALL_MEMBERS_CAN_POST"
wantSettings.MessageModerationLevel = "MODERATE_NONE"
wantSettings.MembersCanPostAsTheGroup = "false"
for key, value := range group.Settings {
switch key {
case "AllowExternalMembers":
wantSettings.AllowExternalMembers = value
case "AllowWebPosting":
wantSettings.AllowWebPosting = value
case "WhoCanJoin":
wantSettings.WhoCanJoin = value
case "WhoCanViewMembership":
wantSettings.WhoCanViewMembership = value
case "WhoCanViewGroup":
wantSettings.WhoCanViewGroup = value
case "WhoCanDiscoverGroup":
wantSettings.WhoCanDiscoverGroup = value
case "WhoCanModerateMembers":
wantSettings.WhoCanModerateMembers = value
case "WhoCanPostMessage":
wantSettings.WhoCanPostMessage = value
case "MessageModerationLevel":
wantSettings.MessageModerationLevel = value
case "MembersCanPostAsTheGroup":
wantSettings.MembersCanPostAsTheGroup = value
}
}
if !reflect.DeepEqual(&haveSettings, &wantSettings) {
if config.ConfirmChanges {
_, err := gs.client.Patch(group.EmailId, &wantSettings)
if err != nil {
return fmt.Errorf("unable to update group info for group %q: %w", group.EmailId, err)
}
log.Printf("> Successfully updated group settings for %q to allow external members and other security settings\n", group.EmailId)
} else {
log.Printf("dry-run: would update group settings for %q\n", group.EmailId)
log.Printf("dry-run: current settings %+q", haveSettings)
log.Printf("dry-run: desired settings %+q", wantSettings)
}
}
return nil
}
// Get retrieves the group settings of a group with groupUniqueID.
func (gs *groupService) Get(groupUniqueID string) (*groupssettings.Groups, error) {
return gs.client.Get(groupUniqueID)
}
var _ GroupService = (*groupService)(nil)
// DeepCopy deepcopies a to b using json marshaling. This discards fields like
// the server response that don't have a specifc json field name.
func deepCopySettings(a, b interface{}) {
byt, _ := json.Marshal(a)
json.Unmarshal(byt, b)
}