mirror of https://github.com/knative/community.git
Adopt K8s infra for gsuite mgmt (#905)
* first commit * adjust makefile * import the tool properly * reconcile groups * fix owners, typos and tighen validation checks
This commit is contained in:
parent
fb40880831
commit
62d1cf134b
|
@ -0,0 +1,30 @@
|
|||
# Copyright 2019 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.
|
||||
|
||||
SHELL := /usr/bin/env bash
|
||||
|
||||
ifeq (run, $(firstword $(MAKECMDGOALS)))
|
||||
runargs := $(wordlist 2, $(words $(MAKECMDGOALS)), $(MAKECMDGOALS))
|
||||
$(eval $(runargs):;@true)
|
||||
endif
|
||||
|
||||
default: run
|
||||
|
||||
.PHONY: run
|
||||
run: test
|
||||
go run . $(runargs)
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test
|
|
@ -0,0 +1,16 @@
|
|||
# The OWNERS file is used by prow to automatically merge approved PRs.
|
||||
|
||||
# root approval is restricted to members of k8s-infra-group-admins@
|
||||
# because they have the ability to manually run, troubleshoot, and undo
|
||||
# changes
|
||||
|
||||
options:
|
||||
no_parent_owners: true
|
||||
|
||||
approvers:
|
||||
- dprotaso
|
||||
- upodroid
|
||||
- chizhg
|
||||
|
||||
reviewers:
|
||||
- productivity-reviewers
|
|
@ -0,0 +1,40 @@
|
|||
# Automation of Google Groups maintenance for `knative.team`
|
||||
|
||||
- [Making changes](#making-changes)
|
||||
- [Staging access groups](#staging-access-groups)
|
||||
- [Manual deploy](#manual-deploy)
|
||||
|
||||
## Making changes
|
||||
|
||||
- Edit your SIG's `groups.yaml`, e.g. [`sig-release/groups.yaml`][/groups/sig-release/groups.yaml]
|
||||
- If adding or removing a group, edit [`restrictions.yaml`] to add or remove the group name
|
||||
- Use `make test` to ensure the changes meet conventions
|
||||
- Open a pull request
|
||||
- When the pull request merges, the [post-k8sio-groups] job will deploy the changes
|
||||
|
||||
### Staging access groups
|
||||
|
||||
Google Groups for granting push access to container repositories and/or buckets
|
||||
must be of the form:
|
||||
|
||||
```console
|
||||
k8s-infra-staging-<project-name>@knative.team
|
||||
```
|
||||
|
||||
**The project name has a max length of 18 characters.**
|
||||
|
||||
## Manual deploy
|
||||
|
||||
- Must be run by someone who is a member of the productivity-infra-gcp-org@knative.team group
|
||||
- Run `gcloud auth application-default login` to login
|
||||
- Use `make run` to dry run the changes
|
||||
- Use `make run -- --confirm` if the changes suggested in the previous step looks good
|
||||
|
||||
[post-k8sio-groups]: https://testgrid.k8s.io/sig-k8s-infra-k8sio#post-k8sio-groups
|
||||
|
||||
## How does this work?
|
||||
|
||||
- The groups are managed with the Google Admin SDK Groups API
|
||||
- Google has a process called Domain Wide Delegation(DWD) that allows a Google Service Account to
|
||||
impersonate a google workspace user. https://developers.google.com/admin-sdk/directory/v1/guides/delegation
|
||||
- Configuring DWD is one time process as long as the Google Service Account impersonating is not deleted.
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
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"
|
||||
|
||||
admin "google.golang.org/api/admin/directory/v1"
|
||||
groupssettings "google.golang.org/api/groupssettings/v1"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
type AdminServiceClient interface {
|
||||
GetGroup(groupKey string) (*admin.Group, error)
|
||||
GetMember(groupKey, memberKey string) (*admin.Member, error)
|
||||
ListGroups() (*admin.Groups, error)
|
||||
ListMembers(groupKey string) ([]*admin.Member, error)
|
||||
InsertGroup(group *admin.Group) (*admin.Group, error)
|
||||
InsertMember(groupKey string, member *admin.Member) (*admin.Member, error)
|
||||
UpdateGroup(groupKey string, group *admin.Group) (*admin.Group, error)
|
||||
UpdateMember(groupKey, memberKey string, member *admin.Member) (*admin.Member, error)
|
||||
DeleteGroup(groupKey string) error
|
||||
DeleteMember(groupKey, memberKey string) error
|
||||
}
|
||||
|
||||
func NewAdminServiceClient(ctx context.Context, clientOption option.ClientOption) (AdminServiceClient, error) {
|
||||
adminSvc, err := admin.NewService(ctx, clientOption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &adminServiceClient{service: adminSvc}, nil
|
||||
}
|
||||
|
||||
type adminServiceClient struct {
|
||||
service *admin.Service
|
||||
}
|
||||
|
||||
func (asc *adminServiceClient) GetGroup(groupKey string) (*admin.Group, error) {
|
||||
return asc.service.Groups.Get(groupKey).Do()
|
||||
}
|
||||
|
||||
func (asc *adminServiceClient) GetMember(groupKey, memberKey string) (*admin.Member, error) {
|
||||
return asc.service.Members.Get(groupKey, memberKey).Do()
|
||||
}
|
||||
|
||||
func (asc *adminServiceClient) ListGroups() (*admin.Groups, error) {
|
||||
return asc.service.Groups.List().Customer("my_customer").OrderBy("email").Do()
|
||||
}
|
||||
|
||||
func (asc *adminServiceClient) ListMembers(groupKey string) ([]*admin.Member, error) {
|
||||
var members []*admin.Member
|
||||
var resp *admin.Members
|
||||
var err error
|
||||
|
||||
call := asc.service.Members.List(groupKey)
|
||||
for {
|
||||
resp, err = call.Do()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
members = append(members, resp.Members...)
|
||||
if resp.NextPageToken == "" {
|
||||
break
|
||||
}
|
||||
|
||||
call.PageToken(resp.NextPageToken)
|
||||
}
|
||||
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func (asc *adminServiceClient) InsertGroup(group *admin.Group) (*admin.Group, error) {
|
||||
return asc.service.Groups.Insert(group).Do()
|
||||
}
|
||||
|
||||
func (asc *adminServiceClient) InsertMember(groupKey string, member *admin.Member) (*admin.Member, error) {
|
||||
return asc.service.Members.Insert(groupKey, member).Do()
|
||||
}
|
||||
|
||||
func (asc *adminServiceClient) UpdateGroup(groupKey string, group *admin.Group) (*admin.Group, error) {
|
||||
return asc.service.Groups.Update(groupKey, group).Do()
|
||||
}
|
||||
|
||||
func (asc *adminServiceClient) UpdateMember(groupKey, memberKey string, member *admin.Member) (*admin.Member, error) {
|
||||
return asc.service.Members.Update(groupKey, memberKey, member).Do()
|
||||
}
|
||||
|
||||
func (asc *adminServiceClient) DeleteGroup(groupKey string) error {
|
||||
return asc.service.Groups.Delete(groupKey).Do()
|
||||
}
|
||||
|
||||
func (asc *adminServiceClient) DeleteMember(groupKey, memberKey string) error {
|
||||
return asc.service.Members.Delete(groupKey, memberKey).Do()
|
||||
}
|
||||
|
||||
var _ AdminServiceClient = (*adminServiceClient)(nil)
|
||||
|
||||
type GroupServiceClient interface {
|
||||
Get(groupUniqueID string) (*groupssettings.Groups, error)
|
||||
Patch(groupUniqueID string, groups *groupssettings.Groups) (*groupssettings.Groups, error)
|
||||
}
|
||||
|
||||
func NewGroupServiceClient(ctx context.Context, clientOption option.ClientOption) (GroupServiceClient, error) {
|
||||
groupSvc, err := groupssettings.NewService(ctx, clientOption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &groupServiceClient{service: groupSvc}, nil
|
||||
}
|
||||
|
||||
type groupServiceClient struct {
|
||||
service *groupssettings.Service
|
||||
}
|
||||
|
||||
func (gsc *groupServiceClient) Get(groupUniqueID string) (*groupssettings.Groups, error) {
|
||||
return gsc.service.Groups.Get(groupUniqueID).Do()
|
||||
}
|
||||
|
||||
func (gsc *groupServiceClient) Patch(groupUniqueID string, groups *groupssettings.Groups) (*groupssettings.Groups, error) {
|
||||
return gsc.service.Groups.Patch(groupUniqueID, groups).Do()
|
||||
}
|
||||
|
||||
var _ GroupServiceClient = (*groupServiceClient)(nil)
|
|
@ -0,0 +1,6 @@
|
|||
# The OWNERS file is used by prow to automatically merge approved PRs.
|
||||
|
||||
approvers:
|
||||
- steering-committee
|
||||
reviewers:
|
||||
- steering-committee
|
|
@ -0,0 +1,15 @@
|
|||
groups:
|
||||
- email-id: code-of-conduct@knative.team
|
||||
name: code-of-conduct
|
||||
description: |-
|
||||
Group for handling Code of Conduct issues.
|
||||
Due to potentially sensitive nature of the issues discussed, this is a closed group.
|
||||
settings:
|
||||
ReconcileMembers: "true"
|
||||
WhoCanPostMessage: "ANYONE_CAN_POST"
|
||||
owners:
|
||||
- pmorie@knative.team
|
||||
- thisisnotapril@knative.team
|
||||
- vaikas@knative.team
|
||||
- lball@knative.team
|
||||
- csantanapr@knative.team
|
|
@ -0,0 +1,6 @@
|
|||
# The OWNERS file is used by prow to automatically merge approved PRs.
|
||||
|
||||
approvers:
|
||||
- technical-oversight-committee
|
||||
reviewers:
|
||||
- technical-oversight-committee
|
|
@ -0,0 +1,17 @@
|
|||
# Technical Oversight Committee
|
||||
groups:
|
||||
- email-id: toc@knative.team
|
||||
name: toc
|
||||
description: |-
|
||||
Technical Oversight Committee Email
|
||||
settings:
|
||||
WhoCanPostMessage: "ANYONE_CAN_POST"
|
||||
ReconcileMembers: "true"
|
||||
owners:
|
||||
- markusthoemmes@knative.team
|
||||
members:
|
||||
- evankanderson@knative.team
|
||||
- grantr@knative.team
|
||||
- mattmoor@knative.team
|
||||
- rhuss@knative.team
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# The OWNERS file is used by prow to automatically merge approved PRs.
|
||||
|
||||
approvers:
|
||||
- steering-committee
|
||||
reviewers:
|
||||
- steering-committee
|
|
@ -0,0 +1,28 @@
|
|||
groups:
|
||||
- email-id: steering@knative.team
|
||||
name: steering
|
||||
description: |-
|
||||
Private list for current and emeritus members of the Knative Steering Committee
|
||||
settings:
|
||||
ReconcileMembers: "true"
|
||||
owners:
|
||||
- pmorie@knative.team
|
||||
- thisisnotapril@knative.team
|
||||
- vaikas@knative.team
|
||||
- csantanapr@knative.team
|
||||
members:
|
||||
- anassi@google.com
|
||||
- lball@knative.team
|
||||
- pmorie@redhat.com
|
||||
- vaikas@gmail.com
|
||||
- itsmurugappan@knative.team
|
||||
|
||||
- email-id: elections@knative.team
|
||||
name: elections
|
||||
description: |-
|
||||
Knative Elections Mailing List
|
||||
settings:
|
||||
ReconcileMembers: "true"
|
||||
members:
|
||||
- fosterd@vmware.com
|
||||
- jberkus@redhat.com
|
|
@ -0,0 +1,6 @@
|
|||
# The OWNERS file is used by prow to automatically merge approved PRs.
|
||||
|
||||
approvers:
|
||||
- trademark-committee
|
||||
reviewers:
|
||||
- trademark-committee
|
|
@ -0,0 +1,12 @@
|
|||
groups:
|
||||
- email-id: trademark@knative.team
|
||||
name: trademark
|
||||
description: |-
|
||||
Trademark Committee Mailing List
|
||||
settings:
|
||||
WhoCanPostMessage: "ANYONE_CAN_POST"
|
||||
ReconcileMembers: "true"
|
||||
owners:
|
||||
- bsnchan@knative.team
|
||||
- duglin@gmail.com
|
||||
- avnur@knative.team
|
|
@ -0,0 +1,13 @@
|
|||
# Configuration for the kubernetes.io Google Groups setup
|
||||
|
||||
# Secret version of the service account key to use
|
||||
secret-version: projects/knative-gsuite/secrets/gsuite-group-manager_key/versions/latest
|
||||
|
||||
# Email id of the bot service account
|
||||
bot-id: prow-robot@knative.team # TO BE CREATED
|
||||
|
||||
# Directory with groups.yaml files, relative to location of this config file
|
||||
groups-path: .
|
||||
|
||||
# Path to restrictions.yaml file, relative to location of this config file
|
||||
restrictions-path: restrictions.yaml
|
|
@ -0,0 +1,272 @@
|
|||
/*
|
||||
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 fake
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
admin "google.golang.org/api/admin/directory/v1"
|
||||
groupssettings "google.golang.org/api/groupssettings/v1"
|
||||
)
|
||||
|
||||
// FakeAdminServiceClient implements the AdminServiceClient but is fake.
|
||||
type FakeAdminServiceClient struct {
|
||||
// Groups is a mapping from groupKey to *admin.Group
|
||||
Groups map[string]*admin.Group
|
||||
// Members is a mapping from groupKey -> members of that group
|
||||
// Members of a group are a mapping from memberKey -> *admin.Member
|
||||
Members map[string]map[string]*admin.Member
|
||||
// OnGroupInsert is a callback function called whenever an
|
||||
// Insert operation is done for a group. This is nescessary
|
||||
// because creating a group also involves reflecting that
|
||||
// creation in the group service fake client, where the GsGroups
|
||||
// map takes the group email as the key.
|
||||
onGroupInsert func(string)
|
||||
}
|
||||
|
||||
func NewFakeAdminServiceClient() *FakeAdminServiceClient {
|
||||
return &FakeAdminServiceClient{
|
||||
Groups: make(map[string]*admin.Group),
|
||||
Members: make(map[string]map[string]*admin.Member),
|
||||
}
|
||||
}
|
||||
|
||||
func NewAugmentedFakeAdminServiceClient() *FakeAdminServiceClient {
|
||||
fakeClient := NewFakeAdminServiceClient()
|
||||
fakeClient.Groups = map[string]*admin.Group{
|
||||
"group1@email.com": {
|
||||
Email: "group1@email.com",
|
||||
Name: "group1",
|
||||
Description: "group1",
|
||||
},
|
||||
"group2@email.com": {
|
||||
Email: "group2@email.com",
|
||||
Name: "group2",
|
||||
Description: "group2",
|
||||
},
|
||||
}
|
||||
|
||||
fakeClient.Members = map[string]map[string]*admin.Member{
|
||||
"group1@email.com": {
|
||||
"m1-group1@email.com": {
|
||||
Email: "m1-group1@email.com",
|
||||
Role: "MEMBER",
|
||||
Id: "m1-group1@email.com",
|
||||
},
|
||||
"m2-group1@email.com": {
|
||||
Email: "m2-group1@email.com",
|
||||
Role: "MANAGER",
|
||||
Id: "m2-group1@email.com",
|
||||
},
|
||||
},
|
||||
"group2@email.com": {
|
||||
"m1-group2@email.com": {
|
||||
Email: "m1-group2@email.com",
|
||||
Role: "MEMBER",
|
||||
Id: "m1-group2@email.com",
|
||||
},
|
||||
"m2-group2@email.com": {
|
||||
Email: "m2-group2@email.com",
|
||||
Role: "OWNER",
|
||||
Id: "m2-group2@email.com",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return fakeClient
|
||||
}
|
||||
|
||||
func (fasc *FakeAdminServiceClient) RegisterCallback(onGroupInsert func(string)) {
|
||||
fasc.onGroupInsert = onGroupInsert
|
||||
}
|
||||
|
||||
func (fasc *FakeAdminServiceClient) GetGroup(groupKey string) (*admin.Group, error) {
|
||||
group, ok := fasc.Groups[groupKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("group key %s not found", groupKey)
|
||||
}
|
||||
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (fasc *FakeAdminServiceClient) GetMember(groupKey, memberKey string) (*admin.Member, error) {
|
||||
members, ok := fasc.Members[groupKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("group with group key %s not found", groupKey)
|
||||
}
|
||||
|
||||
member, ok := members[memberKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("member with groupKey %s and memberKey %s not found", groupKey, memberKey)
|
||||
}
|
||||
|
||||
return member, nil
|
||||
}
|
||||
|
||||
func (fasc *FakeAdminServiceClient) ListGroups() (*admin.Groups, error) {
|
||||
groups := &admin.Groups{}
|
||||
for _, group := range fasc.Groups {
|
||||
groups.Groups = append(groups.Groups, group)
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (fasc *FakeAdminServiceClient) ListMembers(groupKey string) ([]*admin.Member, error) {
|
||||
_, ok := fasc.Members[groupKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("groupKey %s not found", groupKey)
|
||||
}
|
||||
|
||||
members := &admin.Members{}
|
||||
for _, member := range fasc.Members[groupKey] {
|
||||
members.Members = append(members.Members, member)
|
||||
}
|
||||
|
||||
return members.Members, nil
|
||||
}
|
||||
|
||||
func (fasc *FakeAdminServiceClient) InsertGroup(group *admin.Group) (*admin.Group, error) {
|
||||
fasc.Groups[group.Email] = group
|
||||
if fasc.onGroupInsert != nil {
|
||||
fasc.onGroupInsert(group.Email)
|
||||
}
|
||||
|
||||
fasc.Members[group.Email] = map[string]*admin.Member{}
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (fasc *FakeAdminServiceClient) InsertMember(groupKey string, member *admin.Member) (*admin.Member, error) {
|
||||
_, ok := fasc.Members[groupKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("groupKey %s not found", groupKey)
|
||||
}
|
||||
|
||||
fasc.Members[groupKey][member.Email] = member
|
||||
return member, nil
|
||||
}
|
||||
|
||||
func (fasc *FakeAdminServiceClient) UpdateGroup(groupKey string, group *admin.Group) (*admin.Group, error) {
|
||||
_, ok := fasc.Groups[groupKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("group key %s not found", groupKey)
|
||||
}
|
||||
|
||||
fasc.Groups[groupKey] = group
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (fasc *FakeAdminServiceClient) UpdateMember(groupKey, memberKey string, member *admin.Member) (*admin.Member, error) {
|
||||
_, ok := fasc.Members[groupKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("group with group key %s not found", groupKey)
|
||||
}
|
||||
|
||||
_, ok = fasc.Members[groupKey][memberKey]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("member with groupKey %s and memberKey %s not found", groupKey, memberKey)
|
||||
}
|
||||
|
||||
fasc.Members[groupKey][memberKey] = member
|
||||
return member, nil
|
||||
}
|
||||
|
||||
func (fasc *FakeAdminServiceClient) DeleteGroup(groupKey string) error {
|
||||
_, ok := fasc.Groups[groupKey]
|
||||
if !ok {
|
||||
return fmt.Errorf("group key %s not found", groupKey)
|
||||
}
|
||||
|
||||
delete(fasc.Groups, groupKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fasc *FakeAdminServiceClient) DeleteMember(groupKey, memberKey string) error {
|
||||
_, ok := fasc.Members[groupKey]
|
||||
if !ok {
|
||||
return fmt.Errorf("group with group key %s not found", groupKey)
|
||||
}
|
||||
|
||||
_, ok = fasc.Members[groupKey][memberKey]
|
||||
if !ok {
|
||||
return fmt.Errorf("member with groupKey %s and memberKey %s not found", groupKey, memberKey)
|
||||
}
|
||||
|
||||
delete(fasc.Members[groupKey], memberKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
// FakeGroupServiceClient implements the GroupServiceClient but is fake.
|
||||
type FakeGroupServiceClient struct {
|
||||
GsGroups map[string]*groupssettings.Groups
|
||||
}
|
||||
|
||||
func NewFakeGroupServiceClient() *FakeGroupServiceClient {
|
||||
return &FakeGroupServiceClient{
|
||||
GsGroups: make(map[string]*groupssettings.Groups),
|
||||
}
|
||||
}
|
||||
|
||||
func NewAugmentedFakeGroupServiceClient() *FakeGroupServiceClient {
|
||||
fakeClient := NewFakeGroupServiceClient()
|
||||
fakeClient.GsGroups = map[string]*groupssettings.Groups{
|
||||
"group1@email.com": {
|
||||
AllowExternalMembers: "true",
|
||||
WhoCanJoin: "CAN_REQUEST_TO_JOIN",
|
||||
WhoCanViewMembership: "ALL_MANAGERS_CAN_VIEW",
|
||||
WhoCanViewGroup: "ALL_MEMBERS_CAN_VIEW",
|
||||
WhoCanDiscoverGroup: "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
WhoCanModerateMembers: "OWNERS_AND_MANAGERS",
|
||||
WhoCanModerateContent: "OWNERS_AND_MANAGERS",
|
||||
WhoCanPostMessage: "ALL_MEMBERS_CAN_POST",
|
||||
MessageModerationLevel: "MODERATE_NONE",
|
||||
MembersCanPostAsTheGroup: "true",
|
||||
},
|
||||
"group2@email.com": {
|
||||
AllowExternalMembers: "true",
|
||||
WhoCanJoin: "INVITED_CAN_JOIN",
|
||||
WhoCanViewMembership: "ALL_MANAGERS_CAN_VIEW",
|
||||
WhoCanViewGroup: "ALL_MEMBERS_CAN_VIEW",
|
||||
WhoCanDiscoverGroup: "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
WhoCanModerateMembers: "OWNERS_ONLY",
|
||||
WhoCanModerateContent: "OWNERS_AND_MANAGERS",
|
||||
WhoCanPostMessage: "ALL_MEMBERS_CAN_POST",
|
||||
MessageModerationLevel: "MODERATE_NONE",
|
||||
MembersCanPostAsTheGroup: "false",
|
||||
},
|
||||
}
|
||||
|
||||
return fakeClient
|
||||
}
|
||||
|
||||
func (fgsc *FakeGroupServiceClient) Get(groupUniqueID string) (*groupssettings.Groups, error) {
|
||||
gsg, ok := fgsc.GsGroups[groupUniqueID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("groupUniqueID not found %ss", groupUniqueID)
|
||||
}
|
||||
|
||||
return gsg, nil
|
||||
}
|
||||
|
||||
func (fgsc *FakeGroupServiceClient) Patch(groupUniqueID string, groups *groupssettings.Groups) (*groupssettings.Groups, error) {
|
||||
_, ok := fgsc.GsGroups[groupUniqueID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("groupUniqueID not found %ss", groupUniqueID)
|
||||
}
|
||||
|
||||
fgsc.GsGroups[groupUniqueID] = groups
|
||||
return groups, nil
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
module k8s.io/k8s.io/groups
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
cloud.google.com/go/secretmanager v1.0.0
|
||||
github.com/bmatcuk/doublestar v1.1.1
|
||||
golang.org/x/net v0.0.0-20211007125505-59d4e928ea9d
|
||||
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1
|
||||
google.golang.org/api v0.58.0
|
||||
google.golang.org/genproto v0.0.0-20211007155348-82e027067bd4
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
|
||||
k8s.io/apimachinery v0.0.0-20190817020851-f2f3a405f61d
|
||||
k8s.io/test-infra v0.0.0-20191024183346-202cefeb6ff5
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.97.0 // indirect
|
||||
github.com/clarketm/json v1.13.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/grpc v1.41.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
)
|
||||
|
||||
replace k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20190817020851-f2f3a405f61d
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,70 @@
|
|||
groups:
|
||||
- email-id: wg-leads@knative.team
|
||||
name: wg-leads
|
||||
description: |-
|
||||
Knative Working Group leads
|
||||
settings:
|
||||
AllowWebPosting: "true"
|
||||
WhoCanViewGroup: "ANYONE_CAN_VIEW"
|
||||
WhoCanDiscoverGroup: "ANYONE_CAN_DISCOVER"
|
||||
WhoCanPostMessage: "ANYONE_CAN_POST"
|
||||
MessageModerationLevel: "MODERATE_NON_MEMBERS"
|
||||
ReconcileMembers: "true"
|
||||
owners:
|
||||
- dprotaso@gmail.com
|
||||
- evan.k.anderson@gmail.com
|
||||
- evana@vmware.com
|
||||
- evankanderson@knative.team
|
||||
- julz.friedman@uk.ibm.com
|
||||
- julz@knative.team
|
||||
- markusthoemmes@knative.team
|
||||
- markusthoemmes@me.com
|
||||
- me.markusthoemmes@googlemail.com
|
||||
- rhuss@knative.team
|
||||
- rhuss@redhat.com
|
||||
- tcnghia@gmail.com
|
||||
members:
|
||||
- chizhg@google.com
|
||||
- chizhg@knative.team
|
||||
- csantanapr@knative.team
|
||||
- devguyio@knative.team
|
||||
- dprotaso@knative.team
|
||||
- dsimansk@knative.team
|
||||
- dsimansk@redhat.com
|
||||
- eng.a.abdalla@gmail.com
|
||||
- fguardia@redhat.com
|
||||
- grantr@knative.team
|
||||
- houshengbo@knative.team
|
||||
- knakayam@redhat.com
|
||||
- lionelvillard@knative.team
|
||||
- matzew@knative.team
|
||||
- mwessend@redhat.com
|
||||
- mwessendorf@gmail.com
|
||||
- n3wscott@knative.team
|
||||
- nak3@knative.team
|
||||
- navidshaikh@knative.team
|
||||
- nshaikh@redhat.com
|
||||
- omerbensaadon@knative.team
|
||||
- shou@us.ibm.com
|
||||
- slinkydeveloper@knative.team
|
||||
- snneji@knative.team
|
||||
- travis.minke@sap.com
|
||||
- vagababov@gmail.com
|
||||
- villard@us.ibm.com
|
||||
- zhiminx@google.com
|
||||
- zhiminxiang@knative.team
|
||||
|
||||
- email-id: calendar-maintainers@knative.team
|
||||
name: calendar-maintainers
|
||||
description: |-
|
||||
Folks that can manage Knative calendar
|
||||
settings:
|
||||
AllowExternalMembers: "false"
|
||||
owners:
|
||||
- omerbensaadon@knative.team
|
||||
- pmorie@knative.team
|
||||
- thisisnotapril@knative.team
|
||||
- vaikas@knative.team
|
||||
members:
|
||||
- abrennan@knative.team
|
||||
- abrennan89@knative.team
|
|
@ -0,0 +1,348 @@
|
|||
/*
|
||||
Copyright 2019 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 (
|
||||
"flag"
|
||||
"fmt"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var cfg GroupsConfig
|
||||
var rConfig RestrictionsConfig
|
||||
|
||||
var groupsPath = flag.String("groups-path", "", "Directory containing groups.yaml files")
|
||||
var restrictionsPath = flag.String("restrictions-path", "", "Path to the configuration file containing restrictions")
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
var err error
|
||||
|
||||
if *restrictionsPath != "" && !filepath.IsAbs(*restrictionsPath) {
|
||||
fmt.Printf("restrictions-path \"%s\" must be an absolute path\n", *restrictionsPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *restrictionsPath == "" {
|
||||
baseDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Printf("Cannot get current working directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
rPath := filepath.Join(baseDir, defaultRestrictionsFile)
|
||||
restrictionsPath = &rPath
|
||||
}
|
||||
|
||||
if err := rConfig.Load(*restrictionsPath); err != nil {
|
||||
fmt.Printf("Could not load restrictions config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *groupsPath != "" && !filepath.IsAbs(*groupsPath) {
|
||||
fmt.Printf("groups-path \"%s\" must be an absolute path\n", *groupsPath)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if *groupsPath == "" {
|
||||
*groupsPath, err = os.Getwd()
|
||||
if err != nil {
|
||||
fmt.Printf("Cannot get current working directory: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if err := cfg.Load(*groupsPath, &rConfig); err != nil {
|
||||
fmt.Printf("Could not load groups config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
// TestMergedGroupsConfig tests that readGroupsConfig reads all
|
||||
// groups.yaml files and the merged config does not contain any duplicates.
|
||||
//
|
||||
// It tests that the config is merged by checking that the final
|
||||
// GroupsConfig contains at least one group that isn't in the
|
||||
// root groups.yaml file.
|
||||
func TestMergedGroupsConfig(t *testing.T) {
|
||||
var containsMergedConfig bool
|
||||
found := sets.String{}
|
||||
dups := sets.String{}
|
||||
|
||||
for _, g := range cfg.Groups {
|
||||
name := g.Name
|
||||
if name == "steering" {
|
||||
containsMergedConfig = true
|
||||
}
|
||||
|
||||
if found.Has(name) {
|
||||
dups.Insert(name)
|
||||
}
|
||||
found.Insert(name)
|
||||
}
|
||||
|
||||
if !containsMergedConfig {
|
||||
t.Errorf("Final GroupsConfig does not have merged configs from all groups.yaml files")
|
||||
}
|
||||
if n := len(dups); n > 0 {
|
||||
t.Errorf("%d duplicate groups: %s", n, strings.Join(dups.List(), ", "))
|
||||
}
|
||||
}
|
||||
|
||||
// TestStagingEmailLength tests that the number of characters in the
|
||||
// project name in emails used for staging repos does not exceed 18.
|
||||
//
|
||||
// This validation is needed because gcloud allows PROJECT_IDs of length
|
||||
// between 6 and 30. So after discounting the "k8s-staging" prefix,
|
||||
// we are left with 18 chars for the project name.
|
||||
func TestStagingEmailLength(t *testing.T) {
|
||||
var errs []error
|
||||
for _, g := range cfg.Groups {
|
||||
if strings.HasPrefix(g.EmailId, "k8s-infra-staging-") {
|
||||
projectName := strings.TrimSuffix(strings.TrimPrefix(g.EmailId, "k8s-infra-staging-"), "@knative.team")
|
||||
|
||||
len := utf8.RuneCountInString(projectName)
|
||||
if len > 18 {
|
||||
errs = append(errs, fmt.Errorf("Number of characters in project name \"%s\" should not exceed 18; is: %d", projectName, len))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestDescriptionLength tests that the number of characters in the
|
||||
// google groups description does not exceed 300.
|
||||
//
|
||||
// This validation is needed because gcloud allows apps:description
|
||||
// with length no greater than 300
|
||||
func TestDescriptionLength(t *testing.T) {
|
||||
var errs []error
|
||||
for _, g := range cfg.Groups {
|
||||
description := g.Description
|
||||
|
||||
len := utf8.RuneCountInString(description)
|
||||
//Ref: https://developers.google.com/admin-sdk/groups-settings/v1/reference/groups
|
||||
if len > 300 {
|
||||
errs = append(errs,
|
||||
fmt.Errorf("Number of characters in description \"%s\" for group name \"%s\" "+
|
||||
"should not exceed 300; is: %d", description, g.Name, len))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
for _, err := range errs {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce conventions for all groups
|
||||
func TestGroupConventions(t *testing.T) {
|
||||
for _, g := range cfg.Groups {
|
||||
// groups are easier to reason about if email and name match
|
||||
expectedEmailId := g.Name + "@knative.team"
|
||||
if g.EmailId != expectedEmailId {
|
||||
t.Errorf("group '%s': expected email '%s', got '%s'", g.Name, expectedEmailId, g.EmailId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce conventions for all k8s-infra groups
|
||||
func TestK8sInfraGroupConventions(t *testing.T) {
|
||||
for _, g := range cfg.Groups {
|
||||
if strings.HasPrefix(g.EmailId, "k8s-infra") {
|
||||
// no owners because we want to prevent manual membership changes
|
||||
if len(g.Owners) > 0 {
|
||||
t.Errorf("group '%s': must have no owners, only members", g.Name)
|
||||
}
|
||||
|
||||
// treat files here as source of truth for membership
|
||||
reconcileMembers, ok := g.Settings["ReconcileMembers"]
|
||||
if !ok || reconcileMembers != "true" {
|
||||
t.Errorf("group '%s': must have settings.ReconcileMembers = true", g.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce conventions for groups used by GKE Group-based RBAC
|
||||
// - there must be a gke-security-groups@ group
|
||||
// - its members must be k8s-infra-rbac-*@ groups (and vice-versa)
|
||||
// - all groups involved must have settings.WhoCanViewMembership = ALL_MEMBERS_CAN_VIEW
|
||||
func TestK8sInfraRBACGroupConventions(t *testing.T) {
|
||||
rbacEmails := make(map[string]bool)
|
||||
for _, g := range cfg.Groups {
|
||||
if strings.HasPrefix(g.EmailId, "k8s-infra-rbac") {
|
||||
rbacEmails[g.EmailId] = false
|
||||
// this is necessary for group-based rbac to work
|
||||
whoCanViewMembership, ok := g.Settings["WhoCanViewMembership"]
|
||||
if !ok || whoCanViewMembership != "ALL_MEMBERS_CAN_VIEW" {
|
||||
t.Errorf("group '%s': must have settings.WhoCanViewMembership = ALL_MEMBERS_CAN_VIEW", g.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
foundGKEGroup := false
|
||||
for _, g := range cfg.Groups {
|
||||
if g.EmailId == "gke-security-groups@knative.team" {
|
||||
foundGKEGroup = true
|
||||
// this is necessary for group-based rbac to work
|
||||
whoCanViewMembership, ok := g.Settings["WhoCanViewMembership"]
|
||||
if !ok || whoCanViewMembership != "ALL_MEMBERS_CAN_VIEW" {
|
||||
t.Errorf("group '%s': must have settings.WhoCanViewMembership = ALL_MEMBERS_CAN_VIEW", g.Name)
|
||||
}
|
||||
for _, email := range g.Members {
|
||||
if _, ok := rbacEmails[email]; !ok {
|
||||
t.Errorf("group '%s': invalid member '%s', must be a k8s-infra-rbac-*@knative.team group", g.Name, email)
|
||||
} else {
|
||||
rbacEmails[email] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundGKEGroup {
|
||||
t.Errorf("group '%s' is missing", "gke-security-groups@knative.team")
|
||||
}
|
||||
for email, found := range rbacEmails {
|
||||
if !found {
|
||||
t.Errorf("group '%s': must be a member of gke-security-groups@knative.team", email)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// An e-mail address can only show up once within a given group, whether that
|
||||
// be as a member, manager, or owner
|
||||
func TestNoDuplicateMembers(t *testing.T) {
|
||||
for _, g := range cfg.Groups {
|
||||
members := map[string]bool{}
|
||||
for _, m := range g.Members {
|
||||
if _, ok := members[m]; ok {
|
||||
t.Errorf("group '%s' cannot have duplicate member '%s'", g.EmailId, m)
|
||||
}
|
||||
members[m] = true
|
||||
}
|
||||
managers := map[string]bool{}
|
||||
for _, m := range g.Managers {
|
||||
if _, ok := members[m]; ok {
|
||||
t.Errorf("group '%s' manager '%s' cannot also be listed as a member", g.EmailId, m)
|
||||
}
|
||||
if _, ok := managers[m]; ok {
|
||||
t.Errorf("group '%s' cannot have duplicate manager '%s'", g.EmailId, m)
|
||||
}
|
||||
managers[m] = true
|
||||
}
|
||||
owners := map[string]bool{}
|
||||
for _, m := range g.Owners {
|
||||
if _, ok := members[m]; ok {
|
||||
t.Errorf("group '%s' owner '%s' cannot also be listed as a member", g.EmailId, m)
|
||||
}
|
||||
if _, ok := managers[m]; ok {
|
||||
t.Errorf("group '%s' owner '%s' cannot also be listed as a manager", g.EmailId, m)
|
||||
}
|
||||
if _, ok := owners[m]; ok {
|
||||
t.Errorf("group '%s' cannot have duplicate owner '%s'", g.EmailId, m)
|
||||
}
|
||||
owners[m] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: make very certain you know what you are doing if you change one
|
||||
// // of these groups, we don't want to accidentally lock ourselves out
|
||||
func TestHardcodedGroupsForParanoia(t *testing.T) {
|
||||
groups := map[string][]string{
|
||||
"kn-infra-gcp-org-admins@knative.team": {
|
||||
"chizhg@google.com",
|
||||
"cy@borg.dev",
|
||||
"chizhg@knative.team",
|
||||
"cy@knative.team",
|
||||
"racker-maha-66b7d200@gcp.rackspace.com",
|
||||
},
|
||||
}
|
||||
|
||||
found := make(map[string]bool)
|
||||
|
||||
for _, g := range cfg.Groups {
|
||||
if expected, ok := groups[g.EmailId]; ok {
|
||||
found[g.EmailId] = true
|
||||
sort.Strings(expected)
|
||||
actual := make([]string, len(g.Members))
|
||||
copy(actual, g.Members)
|
||||
sort.Strings(actual)
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Errorf("group '%s': expected members '%v', got '%v'", g.Name, expected, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for email := range groups {
|
||||
if _, ok := found[email]; !ok {
|
||||
t.Errorf("group '%s' is missing, should be present", email)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setting AllowWebPosting should be set for every group which should support
|
||||
// access to the group not only via gmail but also via web (you can see the list
|
||||
// and history of threads and also use web interface to operate the group)
|
||||
// More info:
|
||||
// https://developers.google.com/admin-sdk/groups-settings/v1/reference/groups#allowWebPosting
|
||||
func TestGroupsWhichShouldSupportHistory(t *testing.T) {
|
||||
groups := map[string]struct{}{
|
||||
"wg-leads@knative.team": {},
|
||||
}
|
||||
|
||||
found := make(map[string]struct{})
|
||||
|
||||
for _, group := range cfg.Groups {
|
||||
emailId := group.EmailId
|
||||
found[emailId] = struct{}{}
|
||||
if _, ok := groups[emailId]; ok {
|
||||
allowedWebPosting, ok := group.Settings["AllowWebPosting"]
|
||||
if !ok {
|
||||
t.Errorf(
|
||||
"group '%s': must have 'settings.allowedWebPosting = true'",
|
||||
group.Name,
|
||||
)
|
||||
} else if allowedWebPosting != "true" {
|
||||
t.Errorf(
|
||||
"group '%s': must have 'settings.allowedWebPosting = true'"+
|
||||
" but have 'settings.allowedWebPosting = %s' instead",
|
||||
group.Name,
|
||||
allowedWebPosting,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for email := range groups {
|
||||
if _, ok := found[email]; !ok {
|
||||
t.Errorf("group '%s' is missing, should be present", email)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,495 @@
|
|||
/*
|
||||
Copyright 2019 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 (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
secretmanager "cloud.google.com/go/secretmanager/apiv1"
|
||||
"github.com/bmatcuk/doublestar"
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/oauth2/google"
|
||||
admin "google.golang.org/api/admin/directory/v1"
|
||||
"google.golang.org/api/groupssettings/v1"
|
||||
"google.golang.org/api/option"
|
||||
secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
"k8s.io/test-infra/pkg/genyaml"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
// the email id for the bot/service account
|
||||
BotID string `yaml:"bot-id"`
|
||||
|
||||
// the gcloud secret containing a service account key to authenticate with
|
||||
SecretVersion string `yaml:"secret-version,omitempty"`
|
||||
|
||||
// GroupsPath is the path to the directory with
|
||||
// groups.yaml files containing groups/members information.
|
||||
// It must be an absolute path. If not specified,
|
||||
// it defaults to the directory containing the config.yaml file.
|
||||
GroupsPath string `yaml:"groups-path,omitempty"`
|
||||
|
||||
// RestrictionsPath is the absolute path to the configuration file
|
||||
// containing restrictions for which groups can be defined in sub-directories.
|
||||
// If not specified, it defaults to "restrictions.yaml" in the groups-path directory.
|
||||
RestrictionsPath string `yaml:"restrictions-path,omitempty"`
|
||||
|
||||
// If false, don't make any mutating API calls
|
||||
ConfirmChanges bool
|
||||
}
|
||||
|
||||
type GroupsConfig struct {
|
||||
// This file has the list of groups in kubernetes.io gsuite org that we use
|
||||
// for granting permissions to various community resources.
|
||||
Groups []GoogleGroup `yaml:"groups,omitempty" json:"groups,omitempty"`
|
||||
}
|
||||
|
||||
type GoogleGroup struct {
|
||||
EmailId string `yaml:"email-id" json:"email-id"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
Description string `yaml:"description" json:"description"`
|
||||
|
||||
Settings map[string]string `yaml:"settings,omitempty" json:"settings,omitempty"`
|
||||
|
||||
// +optional
|
||||
Owners []string `yaml:"owners,omitempty" json:"owners,omitempty"`
|
||||
|
||||
// +optional
|
||||
Managers []string `yaml:"managers,omitempty" json:"managers,omitempty"`
|
||||
|
||||
// +optional
|
||||
Members []string `yaml:"members,omitempty" json:"members,omitempty"`
|
||||
}
|
||||
|
||||
// RestrictionsConfig contains the list of restrictions for
|
||||
// which groups can be defined in sub-directories.
|
||||
type RestrictionsConfig struct {
|
||||
Restrictions []Restriction `yaml:"restrictions,omitempty" json:"restrictions,omitempty"`
|
||||
}
|
||||
|
||||
type Restriction struct {
|
||||
// Path is the relative path of a sub-directory to the groups-path.
|
||||
Path string `yaml:"path" json:"path"`
|
||||
// AllowedGroups is the list of regular expressions for email-ids
|
||||
// of groups that can be defined for the Path.
|
||||
//
|
||||
// Compiles to AllowedGroupsRe during config load.
|
||||
AllowedGroups []string `yaml:"allowedGroups" json:"allowedGroups"`
|
||||
|
||||
AllowedGroupsRe []*regexp.Regexp
|
||||
}
|
||||
|
||||
func Usage() {
|
||||
fmt.Fprintf(os.Stderr, `
|
||||
Usage: %s [-config <config-yaml-file>] [--confirm]
|
||||
Command line flags override config values.
|
||||
`, os.Args[0])
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
var (
|
||||
config Config
|
||||
groupsConfig GroupsConfig
|
||||
restrictionsConfig RestrictionsConfig
|
||||
|
||||
verbose = flag.Bool("v", false, "log extra information")
|
||||
|
||||
defaultConfigFile = "config.yaml"
|
||||
defaultRestrictionsFile = "restrictions.yaml"
|
||||
emptyRegexp = regexp.MustCompile("")
|
||||
defaultRestriction = Restriction{Path: "*", AllowedGroupsRe: []*regexp.Regexp{emptyRegexp}}
|
||||
)
|
||||
|
||||
func main() {
|
||||
configFilePath := flag.String("config", defaultConfigFile, "the config file in yaml format")
|
||||
confirmChanges := flag.Bool("confirm", false, "false by default means that we do not push anything to google groups")
|
||||
printConfig := flag.Bool("print", false, "print the existing group information")
|
||||
|
||||
flag.Usage = Usage
|
||||
flag.Parse()
|
||||
|
||||
if *printConfig {
|
||||
log.Printf("print: %v -- disabling confirm, will print existing group information", *confirmChanges)
|
||||
*confirmChanges = false
|
||||
}
|
||||
if !*confirmChanges {
|
||||
log.Printf("confirm: %v -- dry-run mode, changes will not be pushed", *confirmChanges)
|
||||
}
|
||||
|
||||
err := config.Load(*configFilePath, *confirmChanges)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Printf("config: BotID: %v", config.BotID)
|
||||
log.Printf("config: SecretVersion: %v", config.SecretVersion)
|
||||
log.Printf("config: GroupsPath: %v", config.GroupsPath)
|
||||
log.Printf("config: RestrictionsPath: %v", config.RestrictionsPath)
|
||||
log.Printf("config: ConfirmChanges: %v", config.ConfirmChanges)
|
||||
|
||||
err = restrictionsConfig.Load(config.RestrictionsPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = groupsConfig.Load(config.GroupsPath, &restrictionsConfig)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
serviceAccountKey, err := accessSecretVersion(config.SecretVersion)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to access secret-version %s, %v", config.SecretVersion, err)
|
||||
}
|
||||
|
||||
credential, err := google.JWTConfigFromJSON(serviceAccountKey, admin.AdminDirectoryUserReadonlyScope,
|
||||
admin.AdminDirectoryGroupScope,
|
||||
admin.AdminDirectoryGroupMemberScope,
|
||||
groupssettings.AppsGroupsSettingsScope)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to authenticate using key in secret-version %s, %v", config.SecretVersion, err)
|
||||
}
|
||||
credential.Subject = config.BotID
|
||||
|
||||
ctx := context.Background()
|
||||
client := credential.Client(ctx)
|
||||
clientOption := option.WithHTTPClient(client)
|
||||
|
||||
r, err := NewReconciler(ctx, clientOption)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if *printConfig {
|
||||
err = r.printGroupMembersAndSettings()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Println(" ======================= Updates =======================")
|
||||
err = r.ReconcileGroups(groupsConfig.Groups)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Reconciler syncs the actual state of the world with the configuration.
|
||||
// It does so by making use of AdminService and GroupService which are mockable
|
||||
// interfaces.
|
||||
type Reconciler struct {
|
||||
adminService AdminService
|
||||
groupService GroupService
|
||||
}
|
||||
|
||||
func NewReconciler(ctx context.Context, clientOption option.ClientOption) (*Reconciler, error) {
|
||||
as, err := NewAdminService(ctx, clientOption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gs, err := NewGroupService(ctx, clientOption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Reconciler{adminService: as, groupService: gs}, nil
|
||||
}
|
||||
|
||||
func (r *Reconciler) ReconcileGroups(groups []GoogleGroup) error {
|
||||
// aggregate the errors that occured and return them together in the end.
|
||||
var errs []error
|
||||
for _, g := range groups {
|
||||
if g.EmailId == "" {
|
||||
errs = append(errs, fmt.Errorf("group has no email-id: %#v", g))
|
||||
}
|
||||
|
||||
err := r.adminService.CreateOrUpdateGroupIfNescessary(g)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
err = r.groupService.UpdateGroupSettings(g)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
err = r.adminService.AddOrUpdateGroupMembers(g, OwnerRole, g.Owners)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
err = r.adminService.AddOrUpdateGroupMembers(g, ManagerRole, g.Managers)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
err = r.adminService.AddOrUpdateGroupMembers(g, MemberRole, g.Members)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
if g.Settings["ReconcileMembers"] == "true" {
|
||||
members := append(g.Owners, g.Managers...)
|
||||
members = append(members, g.Members...)
|
||||
err = r.adminService.RemoveMembersFromGroup(g, members)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
} else {
|
||||
members := append(g.Owners, g.Managers...)
|
||||
err = r.adminService.RemoveOwnerOrManagersFromGroup(g, members)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := r.adminService.DeleteGroupsIfNecessary()
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
return utilerrors.NewAggregate(errs)
|
||||
}
|
||||
|
||||
func (r *Reconciler) printGroupMembersAndSettings() error {
|
||||
g, err := r.adminService.ListGroups()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to retrieve users in domain: %w", err)
|
||||
}
|
||||
|
||||
var groupsConfig GroupsConfig
|
||||
for _, g := range g.Groups {
|
||||
group := GoogleGroup{
|
||||
EmailId: g.Email,
|
||||
Name: g.Name,
|
||||
Description: g.Description,
|
||||
}
|
||||
g2, err := r.groupService.Get(g.Email)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to retrieve group info for group %s: %w", g.Email, err)
|
||||
}
|
||||
group.Settings = make(map[string]string)
|
||||
group.Settings["AllowExternalMembers"] = g2.AllowExternalMembers
|
||||
group.Settings["WhoCanJoin"] = g2.WhoCanJoin
|
||||
group.Settings["WhoCanViewMembership"] = g2.WhoCanViewMembership
|
||||
group.Settings["WhoCanViewGroup"] = g2.WhoCanViewGroup
|
||||
group.Settings["WhoCanDiscoverGroup"] = g2.WhoCanDiscoverGroup
|
||||
group.Settings["WhoCanInvite"] = g2.WhoCanInvite
|
||||
group.Settings["WhoCanAdd"] = g2.WhoCanAdd
|
||||
group.Settings["WhoCanApproveMembers"] = g2.WhoCanApproveMembers
|
||||
group.Settings["WhoCanModifyMembers"] = g2.WhoCanModifyMembers
|
||||
group.Settings["WhoCanModerateMembers"] = g2.WhoCanModerateMembers
|
||||
group.Settings["MembersCanPostAsTheGroup"] = g2.MembersCanPostAsTheGroup
|
||||
|
||||
l, err := r.adminService.ListMembers(g.Email)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to retrieve members in group : %w", err)
|
||||
}
|
||||
|
||||
for _, m := range l {
|
||||
switch m.Role {
|
||||
case OwnerRole:
|
||||
group.Owners = append(group.Owners, m.Email)
|
||||
case ManagerRole:
|
||||
group.Managers = append(group.Managers, m.Email)
|
||||
case MemberRole:
|
||||
group.Members = append(group.Members, m.Email)
|
||||
}
|
||||
}
|
||||
|
||||
groupsConfig.Groups = append(groupsConfig.Groups, group)
|
||||
}
|
||||
|
||||
cm := genyaml.NewCommentMap("reconcile.go")
|
||||
yamlSnippet, err := cm.GenYaml(groupsConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate yaml for groups : %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(yamlSnippet)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Config) Load(configFilePath string, confirmChanges bool) error {
|
||||
log.Printf("reading config file: %s", configFilePath)
|
||||
content, err := ioutil.ReadFile(configFilePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading config file %s: %w", configFilePath, err)
|
||||
}
|
||||
if err = yaml.Unmarshal(content, &c); err != nil {
|
||||
return fmt.Errorf("error parsing config file %s: %w", configFilePath, err)
|
||||
}
|
||||
|
||||
if !filepath.IsAbs(c.GroupsPath) {
|
||||
c.GroupsPath = filepath.Clean(filepath.Join(filepath.Dir(configFilePath), c.GroupsPath))
|
||||
}
|
||||
c.GroupsPath, err = filepath.Abs(c.GroupsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error converting groups-path %v to absolute path: %w", c.GroupsPath, err)
|
||||
}
|
||||
|
||||
if c.RestrictionsPath == "" {
|
||||
c.RestrictionsPath = filepath.Join(filepath.Dir(configFilePath), defaultRestrictionsFile)
|
||||
}
|
||||
c.RestrictionsPath, err = filepath.Abs(c.RestrictionsPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error converting retrictions-path %v to absolute path: %w", c.RestrictionsPath, err)
|
||||
}
|
||||
|
||||
c.ConfirmChanges = confirmChanges
|
||||
return err
|
||||
}
|
||||
|
||||
// Load populates the RestrictionsConfig with data parsed from path and returns
|
||||
// nil if successful, or an error otherwise
|
||||
func (rc *RestrictionsConfig) Load(path string) error {
|
||||
log.Printf("reading restrictions config file: %s", path)
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading restrictions config file %s: %w", path, err)
|
||||
}
|
||||
if err = yaml.Unmarshal(content, &rc); err != nil {
|
||||
return fmt.Errorf("error parsing restrictions config file %s: %w", path, err)
|
||||
}
|
||||
|
||||
ret := make([]Restriction, 0, len(rc.Restrictions))
|
||||
for _, r := range rc.Restrictions {
|
||||
r.AllowedGroupsRe = make([]*regexp.Regexp, 0, len(r.AllowedGroups))
|
||||
for _, g := range r.AllowedGroups {
|
||||
re, err := regexp.Compile(g)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing group pattern %q for path %q: %w", g, r.Path, err)
|
||||
}
|
||||
r.AllowedGroupsRe = append(r.AllowedGroupsRe, re)
|
||||
}
|
||||
ret = append(ret, r)
|
||||
}
|
||||
rc.Restrictions = ret
|
||||
return err
|
||||
}
|
||||
|
||||
// readGroupsConfig starts at the rootDir and recursively walksthrough
|
||||
// all directories and files. It reads the GroupsConfig from all groups.yaml
|
||||
// files and verifies that the groups in GroupsConfig satisfy the
|
||||
// restrictions in restrictionsConfig.
|
||||
// Finally, it adds all the groups in each GroupsConfig to config.Groups.
|
||||
func (gc *GroupsConfig) Load(rootDir string, restrictions *RestrictionsConfig) error {
|
||||
log.Printf("reading groups.yaml files recursively at %s", rootDir)
|
||||
|
||||
return filepath.Walk(rootDir, func(path string, info os.FileInfo, _ error) error {
|
||||
if filepath.Base(path) == "groups.yaml" {
|
||||
cleanPath := strings.Trim(strings.TrimPrefix(path, rootDir), string(filepath.Separator))
|
||||
log.Printf("groups: %s", cleanPath)
|
||||
|
||||
var (
|
||||
groupsConfigAtPath GroupsConfig
|
||||
content []byte
|
||||
err error
|
||||
)
|
||||
|
||||
if content, err = ioutil.ReadFile(path); err != nil {
|
||||
return fmt.Errorf("error reading groups config file %s: %w", path, err)
|
||||
}
|
||||
if err = yaml.Unmarshal(content, &groupsConfigAtPath); err != nil {
|
||||
return fmt.Errorf("error parsing groups config at %s: %w", path, err)
|
||||
}
|
||||
|
||||
r := restrictions.GetRestrictionForPath(path, rootDir)
|
||||
mergedGroups, err := mergeGroups(gc.Groups, groupsConfigAtPath.Groups, r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't merge groups: %w", err)
|
||||
}
|
||||
gc.Groups = mergedGroups
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// GetRestrictionForPath returns the first Restriction whose Path matches the
|
||||
// given path relative to the given rootDir, or defaultRestriction if no
|
||||
// Restriction is found
|
||||
func (rc *RestrictionsConfig) GetRestrictionForPath(path, rootDir string) Restriction {
|
||||
cleanPath := strings.Trim(strings.TrimPrefix(path, rootDir), string(filepath.Separator))
|
||||
for _, r := range rc.Restrictions {
|
||||
if match, err := doublestar.Match(r.Path, cleanPath); err == nil && match {
|
||||
return r
|
||||
}
|
||||
}
|
||||
return defaultRestriction
|
||||
}
|
||||
|
||||
func mergeGroups(a []GoogleGroup, b []GoogleGroup, r Restriction) ([]GoogleGroup, error) {
|
||||
emails := map[string]struct{}{}
|
||||
for _, v := range a {
|
||||
emails[v.EmailId] = struct{}{}
|
||||
}
|
||||
for _, v := range b {
|
||||
if v.EmailId == "" {
|
||||
return nil, fmt.Errorf("groups must have email-id")
|
||||
}
|
||||
if !matchesRegexList(v.EmailId, r.AllowedGroupsRe) {
|
||||
return nil, fmt.Errorf("cannot define group %q in %q", v.EmailId, r.Path)
|
||||
}
|
||||
if _, ok := emails[v.EmailId]; ok {
|
||||
return nil, fmt.Errorf("cannot overwrite group definitions (duplicate group name %s)", v.EmailId)
|
||||
}
|
||||
}
|
||||
return append(a, b...), nil
|
||||
}
|
||||
|
||||
func matchesRegexList(s string, list []*regexp.Regexp) bool {
|
||||
for _, r := range list {
|
||||
if r.MatchString(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// accessSecretVersion accesses the payload for the given secret version if one exists
|
||||
// secretVersion is of the form projects/{project}/secrets/{secret}/versions/{version}
|
||||
func accessSecretVersion(secretVersion string) ([]byte, error) {
|
||||
ctx := context.Background()
|
||||
client, err := secretmanager.NewClient(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create secretmanager client: %w", err)
|
||||
}
|
||||
|
||||
req := &secretmanagerpb.AccessSecretVersionRequest{
|
||||
Name: secretVersion,
|
||||
}
|
||||
|
||||
result, err := client.AccessSecretVersion(ctx, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to access secret version: %w", err)
|
||||
}
|
||||
|
||||
return result.Payload.Data, nil
|
||||
}
|
|
@ -0,0 +1,709 @@
|
|||
/*
|
||||
Copyright 2019 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 (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
admin "google.golang.org/api/admin/directory/v1"
|
||||
groupssettings "google.golang.org/api/groupssettings/v1"
|
||||
"k8s.io/k8s.io/groups/fake"
|
||||
)
|
||||
|
||||
// state is used to hold information needed
|
||||
// to construct the state of the world.
|
||||
type state struct {
|
||||
adminClient AdminServiceClient
|
||||
groupClient GroupServiceClient
|
||||
}
|
||||
|
||||
// isReconciled constructs the current state of the world using adminClient
|
||||
// and groupClient and checks if it is reconciled against the desiredState
|
||||
// and matches it.
|
||||
func (s state) isReconciled(desiredState []GoogleGroup) error {
|
||||
// incrementally construct the state of the world and check
|
||||
// against the desiredState.
|
||||
// see if the groups that exist are reconciled or not.
|
||||
res, _ := s.adminClient.ListGroups()
|
||||
if !checkForAdminGroupGoogleGroupEquality(res.Groups, desiredState) {
|
||||
return fmt.Errorf(
|
||||
"groups do not match (email, name, description): desired: %#v, actual: %#v",
|
||||
desiredState,
|
||||
constructGoogleGroupFromAdminGroup(res.Groups),
|
||||
)
|
||||
}
|
||||
|
||||
// now that we know that atleast the groups that exist are the same, we
|
||||
// create a map of emailID -> *admin.Group to make sure we are checking
|
||||
// the same group in every iteration of the next step.
|
||||
currGroups := make(map[string]*admin.Group)
|
||||
for i, g := range res.Groups {
|
||||
currGroups[g.Email] = res.Groups[i]
|
||||
}
|
||||
|
||||
// provided the groups that exist are the same, check for member equality
|
||||
// and groupSettings.Group equality.
|
||||
for i := 0; i < len(desiredState); i++ {
|
||||
// safe to assume that the EmailID will exist as a key because
|
||||
// we already checked if the groups that exist are the same or
|
||||
// not.
|
||||
currGroup := currGroups[desiredState[i].EmailId]
|
||||
desiredMembers := constructMemberListFromGoogleGroup(desiredState[i])
|
||||
currentMembers, err := s.adminClient.ListMembers(currGroup.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !checkForMemberListEquality(desiredMembers, currentMembers) {
|
||||
return fmt.Errorf(
|
||||
"member lists do not match (email, role): desired: %#v, actual: %#v",
|
||||
getMemberListInPrintableForm(desiredMembers),
|
||||
getMemberListInPrintableForm(currentMembers),
|
||||
)
|
||||
}
|
||||
currentSettings, err := s.groupClient.Get(currGroup.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
desiredSettings := constructGroupSettingsFromGoogleGroup(desiredState[i])
|
||||
if !reflect.DeepEqual(desiredSettings, currentSettings) {
|
||||
return fmt.Errorf(
|
||||
"group settings do not match, desired: %#v, actual: %#v",
|
||||
desiredSettings,
|
||||
currentSettings,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func constructMemberListFromGoogleGroup(g GoogleGroup) []*admin.Member {
|
||||
res := make([]*admin.Member, len(g.Members)+len(g.Managers)+len(g.Owners))
|
||||
index := 0
|
||||
for _, m := range g.Members {
|
||||
res[index] = &admin.Member{Email: m, Id: m, Role: MemberRole}
|
||||
index++
|
||||
}
|
||||
for _, m := range g.Managers {
|
||||
res[index] = &admin.Member{Email: m, Id: m, Role: ManagerRole}
|
||||
index++
|
||||
}
|
||||
for _, m := range g.Owners {
|
||||
res[index] = &admin.Member{Email: m, Id: m, Role: OwnerRole}
|
||||
index++
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func constructGroupSettingsFromGoogleGroup(g GoogleGroup) *groupssettings.Groups {
|
||||
requiredSettingsDefaults := map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "INVITED_CAN_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "false",
|
||||
}
|
||||
|
||||
for s, v := range requiredSettingsDefaults {
|
||||
if _, ok := g.Settings[s]; !ok {
|
||||
g.Settings[s] = v
|
||||
}
|
||||
}
|
||||
|
||||
return &groupssettings.Groups{
|
||||
AllowExternalMembers: g.Settings["AllowExternalMembers"],
|
||||
WhoCanJoin: g.Settings["WhoCanJoin"],
|
||||
WhoCanViewMembership: g.Settings["WhoCanViewMembership"],
|
||||
WhoCanViewGroup: g.Settings["WhoCanViewGroup"],
|
||||
WhoCanDiscoverGroup: g.Settings["WhoCanDiscoverGroup"],
|
||||
WhoCanModerateMembers: g.Settings["WhoCanModerateMembers"],
|
||||
WhoCanModerateContent: g.Settings["WhoCanModerateContent"],
|
||||
WhoCanPostMessage: g.Settings["WhoCanPostMessage"],
|
||||
MessageModerationLevel: g.Settings["MessageModerationLevel"],
|
||||
MembersCanPostAsTheGroup: g.Settings["MembersCanPostAsTheGroup"],
|
||||
}
|
||||
}
|
||||
|
||||
func constructGoogleGroupFromAdminGroup(ag []*admin.Group) []GoogleGroup {
|
||||
res := make([]GoogleGroup, len(ag))
|
||||
for i := 0; i < len(res); i++ {
|
||||
res[i] = GoogleGroup{EmailId: ag[i].Email, Name: ag[i].Name, Description: ag[i].Description}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func TestRestrictionForPath(t *testing.T) {
|
||||
rc := &RestrictionsConfig{
|
||||
Restrictions: []Restriction{
|
||||
{
|
||||
Path: "full-path-to-sig-foo/file.yaml",
|
||||
AllowedGroups: []string{
|
||||
"sig-foo-group-a",
|
||||
"sig-foo-group-b",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
testcases := []struct {
|
||||
name string
|
||||
path string
|
||||
expectedPath string
|
||||
expectedGroups []string
|
||||
}{
|
||||
{
|
||||
name: "empty path",
|
||||
path: "",
|
||||
expectedPath: "*",
|
||||
expectedGroups: nil,
|
||||
},
|
||||
{
|
||||
name: "full path match",
|
||||
path: "full-path-to-sig-foo/file.yaml",
|
||||
expectedPath: "full-path-to-sig-foo/file.yaml",
|
||||
expectedGroups: []string{
|
||||
"sig-foo-group-a",
|
||||
"sig-foo-group-b",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "path not found",
|
||||
path: "does-not/exist.yaml",
|
||||
expectedPath: "*",
|
||||
expectedGroups: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
path := tc.path
|
||||
root := "root" // TODO: add testcases that vary root
|
||||
expected := Restriction{
|
||||
Path: tc.expectedPath,
|
||||
AllowedGroups: tc.expectedGroups,
|
||||
}
|
||||
actual := rc.GetRestrictionForPath(path, root)
|
||||
if expected.Path != actual.Path {
|
||||
t.Errorf("Unexpected restriction.path for %v, %v: expected %v, got %v", path, root, expected.Path, actual.Path)
|
||||
}
|
||||
if !reflect.DeepEqual(expected.AllowedGroups, actual.AllowedGroups) {
|
||||
t.Errorf("Unexpected restriction.allowedGroups for %v, %v: expected %v, got %v", path, root, expected.AllowedGroups, actual.AllowedGroups)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileGroups(t *testing.T) {
|
||||
config.ConfirmChanges = true
|
||||
cases := []struct {
|
||||
desc string
|
||||
// desired state is not nescessarily the same as expected state.
|
||||
// We might need the two of them to differ in cases where we want
|
||||
// to test out functionality where our desired state warrants for
|
||||
// a change but in actuality this change should not occur - which
|
||||
// is signified via expected state. shouldConsiderExpectedState is
|
||||
// what differentiates what we should consider when comparing results.
|
||||
// If false - desired is same as expected. This is done because in
|
||||
// most cases these two are same and we can avoid clutter.
|
||||
shouldConsiderExpectedState bool
|
||||
desiredState []GoogleGroup
|
||||
expectedState []GoogleGroup
|
||||
}{
|
||||
{
|
||||
desc: "state matches, nothing to reconcile",
|
||||
desiredState: []GoogleGroup{
|
||||
{
|
||||
EmailId: "group1@email.com", Name: "group1", Description: "group1",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "CAN_REQUEST_TO_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "true",
|
||||
},
|
||||
Members: []string{"m1-group1@email.com"},
|
||||
Managers: []string{"m2-group1@email.com"},
|
||||
},
|
||||
{
|
||||
EmailId: "group2@email.com", Name: "group2", Description: "group2",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "INVITED_CAN_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_ONLY",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "false",
|
||||
},
|
||||
Members: []string{"m1-group2@email.com"},
|
||||
Owners: []string{"m2-group2@email.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "new members added, attempt to reconcile",
|
||||
desiredState: []GoogleGroup{
|
||||
{
|
||||
EmailId: "group1@email.com", Name: "group1", Description: "group1",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "CAN_REQUEST_TO_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "true",
|
||||
},
|
||||
Members: []string{"m1-group1@email.com"},
|
||||
Managers: []string{"m2-group1@email.com"},
|
||||
Owners: []string{"m3-group1@email.com"}, // new member
|
||||
},
|
||||
{
|
||||
EmailId: "group2@email.com", Name: "group2", Description: "group2",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "INVITED_CAN_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_ONLY",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "false",
|
||||
},
|
||||
Members: []string{"m1-group2@email.com"},
|
||||
Managers: []string{"m3-group2@email.com"}, // new member
|
||||
Owners: []string{"m2-group2@email.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "members with OWNER/MANAGER role deleted from group1 and group2, attempt to reconcile",
|
||||
desiredState: []GoogleGroup{
|
||||
{
|
||||
EmailId: "group1@email.com", Name: "group1", Description: "group1",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "CAN_REQUEST_TO_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "true",
|
||||
},
|
||||
Members: []string{"m1-group1@email.com"},
|
||||
Managers: []string{}, // member removed
|
||||
},
|
||||
{
|
||||
EmailId: "group2@email.com", Name: "group2", Description: "group2",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "INVITED_CAN_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_ONLY",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "false",
|
||||
},
|
||||
Members: []string{"m1-group2@email.com"},
|
||||
Owners: []string{}, // member removed
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "member with MANAGER role deleted in group1 and member with MEMBER role added in group2, attempt to reconcile",
|
||||
desiredState: []GoogleGroup{
|
||||
{
|
||||
EmailId: "group1@email.com", Name: "group1", Description: "group1",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "CAN_REQUEST_TO_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "true",
|
||||
},
|
||||
Members: []string{"m1-group1@email.com"},
|
||||
Managers: []string{}, // member removed
|
||||
},
|
||||
{
|
||||
EmailId: "group2@email.com", Name: "group2", Description: "group2",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "INVITED_CAN_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_ONLY",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "false",
|
||||
},
|
||||
Members: []string{"m1-group2@email.com", "m3-group2@email.com"}, // member added
|
||||
Owners: []string{"m2-group2@email.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "member with MEMBER role deleted from group1 with ReconcileMembers setting enabled, attempt to reconcile",
|
||||
desiredState: []GoogleGroup{
|
||||
{
|
||||
EmailId: "group1@email.com", Name: "group1", Description: "group1",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "CAN_REQUEST_TO_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "true",
|
||||
"ReconcileMembers": "true",
|
||||
},
|
||||
Members: []string{}, // member removed
|
||||
Managers: []string{"m2-group1@email.com"},
|
||||
},
|
||||
{
|
||||
EmailId: "group2@email.com", Name: "group2", Description: "group2",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "INVITED_CAN_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_ONLY",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "false",
|
||||
},
|
||||
Members: []string{"m1-group2@email.com"},
|
||||
Owners: []string{"m2-group2@email.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "member with MEMBER role deleted from group1 with ReconcileMembers setting disabled, no change",
|
||||
desiredState: []GoogleGroup{
|
||||
{
|
||||
EmailId: "group1@email.com", Name: "group1", Description: "group1",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "CAN_REQUEST_TO_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "true",
|
||||
"ReconcileMembers": "false",
|
||||
},
|
||||
Members: []string{}, // member removed
|
||||
Managers: []string{"m2-group1@email.com"},
|
||||
},
|
||||
{
|
||||
EmailId: "group2@email.com", Name: "group2", Description: "group2",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "INVITED_CAN_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_ONLY",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "false",
|
||||
},
|
||||
Members: []string{"m1-group2@email.com"},
|
||||
Owners: []string{"m2-group2@email.com"},
|
||||
},
|
||||
},
|
||||
shouldConsiderExpectedState: true,
|
||||
expectedState: []GoogleGroup{
|
||||
{
|
||||
EmailId: "group1@email.com", Name: "group1", Description: "group1",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "CAN_REQUEST_TO_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "true",
|
||||
"ReconcileMembers": "false", // setting disabled.
|
||||
},
|
||||
Members: []string{"m1-group1@email.com"}, // member not removed.
|
||||
Managers: []string{"m2-group1@email.com"},
|
||||
},
|
||||
{
|
||||
EmailId: "group2@email.com", Name: "group2", Description: "group2",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "INVITED_CAN_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_ONLY",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "false",
|
||||
},
|
||||
Members: []string{"m1-group2@email.com"},
|
||||
Owners: []string{"m2-group2@email.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "group2 deleted, attempt to reconcile",
|
||||
desiredState: []GoogleGroup{
|
||||
{
|
||||
EmailId: "group1@email.com", Name: "group1", Description: "group1",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "CAN_REQUEST_TO_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "true",
|
||||
},
|
||||
Members: []string{"m1-group1@email.com"},
|
||||
Managers: []string{"m2-group1@email.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "group3 added, member with OWNER role added to group1 and member with MANAGER role added to group2",
|
||||
desiredState: []GoogleGroup{
|
||||
{
|
||||
EmailId: "group1@email.com", Name: "group1", Description: "group1",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "CAN_REQUEST_TO_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "true",
|
||||
},
|
||||
Members: []string{"m1-group1@email.com"},
|
||||
Managers: []string{"m2-group1@email.com"},
|
||||
Owners: []string{"m3-group1@email.com"}, // member added
|
||||
},
|
||||
{
|
||||
EmailId: "group2@email.com", Name: "group2", Description: "group2",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "INVITED_CAN_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_ONLY",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "false",
|
||||
},
|
||||
Members: []string{"m1-group2@email.com"},
|
||||
Owners: []string{"m2-group2@email.com"},
|
||||
Managers: []string{"m3-group2@email.com"}, // member added
|
||||
},
|
||||
{
|
||||
// group added
|
||||
EmailId: "group3@email.com", Name: "group3", Description: "group3",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "CAN_REQUEST_TO_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "true",
|
||||
},
|
||||
Members: []string{"m1-group3@email.com"},
|
||||
Managers: []string{"m2-group3@email.com"},
|
||||
Owners: []string{"m3-group3@email.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "move member from MEMBER role in group1 to OWNER role.",
|
||||
desiredState: []GoogleGroup{
|
||||
{
|
||||
EmailId: "group1@email.com", Name: "group1", Description: "group1",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "CAN_REQUEST_TO_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "true",
|
||||
},
|
||||
Members: []string{},
|
||||
Managers: []string{"m2-group1@email.com"},
|
||||
Owners: []string{"m1-group1@email.com"},
|
||||
},
|
||||
{
|
||||
EmailId: "group2@email.com", Name: "group2", Description: "group2",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "INVITED_CAN_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_ONLY",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "false",
|
||||
},
|
||||
Members: []string{"m1-group2@email.com"},
|
||||
Owners: []string{"m2-group2@email.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "move member from OWNER role in group2 to MEMBER role.",
|
||||
desiredState: []GoogleGroup{
|
||||
{
|
||||
EmailId: "group1@email.com", Name: "group1", Description: "group1",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "CAN_REQUEST_TO_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "true",
|
||||
},
|
||||
Members: []string{"m1-group1@email.com"},
|
||||
Managers: []string{"m2-group1@email.com"},
|
||||
},
|
||||
{
|
||||
EmailId: "group2@email.com", Name: "group2", Description: "group2",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "INVITED_CAN_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_ONLY",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "false",
|
||||
},
|
||||
Members: []string{"m1-group2@email.com", "m2-group2@email.com"},
|
||||
Owners: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
errFunc := func(err error) bool {
|
||||
return err != nil
|
||||
}
|
||||
for _, c := range cases {
|
||||
groupsConfig.Groups = c.desiredState
|
||||
fakeAdminClient := fake.NewAugmentedFakeAdminServiceClient()
|
||||
fakeGroupClient := fake.NewAugmentedFakeGroupServiceClient()
|
||||
|
||||
fakeAdminClient.RegisterCallback(func(groupKey string) {
|
||||
_, ok := fakeGroupClient.GsGroups[groupKey]
|
||||
if !ok {
|
||||
fakeGroupClient.GsGroups[groupKey] = &groupssettings.Groups{}
|
||||
}
|
||||
})
|
||||
|
||||
adminSvc, _ := NewAdminServiceWithClientAndErrFunc(fakeAdminClient, errFunc)
|
||||
groupSvc, _ := NewGroupServiceWithClientAndErrFunc(fakeGroupClient, errFunc)
|
||||
|
||||
reconciler := &Reconciler{adminService: adminSvc, groupService: groupSvc}
|
||||
err := reconciler.ReconcileGroups(c.desiredState)
|
||||
if err != nil {
|
||||
t.Errorf("error reconciling groups for case %s: %s", c.desc, err.Error())
|
||||
}
|
||||
|
||||
s := state{adminClient: fakeAdminClient, groupClient: fakeGroupClient}
|
||||
|
||||
var expectedState []GoogleGroup
|
||||
if c.shouldConsiderExpectedState {
|
||||
expectedState = c.expectedState
|
||||
} else {
|
||||
expectedState = c.desiredState
|
||||
}
|
||||
if err = s.isReconciled(expectedState); err != nil {
|
||||
t.Errorf("reconciliation unsuccessful for case %s: %w", c.desc, err)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
restrictions:
|
||||
- path: "committee-code-of-conduct/groups.yaml"
|
||||
allowedGroups:
|
||||
- "^code-of-conduct@knative.team$"
|
||||
- path: "committee-trademark/groups.yaml"
|
||||
allowedGroups:
|
||||
- "^trademark@knative.team$"
|
||||
- path: "committee-oversight/groups.yaml"
|
||||
allowedGroups:
|
||||
- "^toc@knative.team$"
|
||||
- path: "committee-steering/groups.yaml"
|
||||
allowedGroups:
|
||||
- "^steering@knative.team$"
|
||||
- "^elections@knative.team$"
|
||||
- path: "wg-productivity/groups.yaml"
|
||||
allowedGroups:
|
||||
- "^gke-security-groups@knative.team$"
|
||||
- "^kn-infra-gcp-org-admins@knative.team$"
|
||||
- "^k8s-infra-rbac-[a-z]{0,8}@knative.team$"
|
||||
- "^automation@knative.team$"
|
||||
- path: "wg-security/groups.yaml"
|
||||
allowedGroups:
|
||||
- "^security@knative.team$"
|
||||
- path: "groups.yaml"
|
||||
allowedGroups:
|
||||
- "^wg-leads@knative.team$"
|
||||
- "^calendar-maintainers@knative.team$"
|
||||
- path: "**/*" # prevent any other file from containing anything
|
|
@ -0,0 +1,545 @@
|
|||
/*
|
||||
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"
|
||||
"strings"
|
||||
|
||||
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.Member, 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 NewAdminServiceWithClient(client)
|
||||
}
|
||||
|
||||
func NewAdminServiceWithClient(client AdminServiceClient) (AdminService, error) {
|
||||
errFunc := func(err error) bool {
|
||||
apierr, ok := err.(*googleapi.Error)
|
||||
return ok && apierr.Code == http.StatusNotFound
|
||||
}
|
||||
return &adminService{
|
||||
client: client,
|
||||
checkForAPIErr404: errFunc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewAdminServiceWithClientAndErrFunc(client AdminServiceClient, errFunc clientErrCheckFunc) (AdminService, error) {
|
||||
return &adminService{
|
||||
client: client,
|
||||
checkForAPIErr404: errFunc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewGroupService(ctx context.Context, clientOption option.ClientOption) (GroupService, error) {
|
||||
client, err := NewGroupServiceClient(ctx, clientOption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewGroupServiceWithClient(client)
|
||||
}
|
||||
|
||||
func NewGroupServiceWithClient(client GroupServiceClient) (GroupService, error) {
|
||||
errFunc := func(err error) bool {
|
||||
apierr, ok := err.(*googleapi.Error)
|
||||
return ok && apierr.Code == http.StatusNotFound
|
||||
}
|
||||
return &groupService{
|
||||
client: client,
|
||||
checkForAPIErr404: errFunc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewGroupServiceWithClientAndErrFunc(client GroupServiceClient, errFunc clientErrCheckFunc) (GroupService, error) {
|
||||
return &groupService{
|
||||
client: client,
|
||||
checkForAPIErr404: errFunc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// clientErrCheckFunc is stubbed out here for testing
|
||||
// purposes in order to implement custome error checking
|
||||
// logic. This function is used to check errors returned
|
||||
// by the client. The boolean return signifies if the
|
||||
// check for the error passed or not.
|
||||
type clientErrCheckFunc func(error) bool
|
||||
|
||||
type adminService struct {
|
||||
client AdminServiceClient
|
||||
checkForAPIErr404 clientErrCheckFunc
|
||||
}
|
||||
|
||||
// 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 as.checkForAPIErr404(err) {
|
||||
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 {
|
||||
if EmailAddressEquals(m.Email, memberEmailId) {
|
||||
member = m
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if member != nil {
|
||||
// update if necessary
|
||||
if member.Role != role {
|
||||
member.Role = role
|
||||
if config.ConfirmChanges {
|
||||
log.Printf("Updating %s to %q as a %s\n", memberEmailId, group.EmailId, role)
|
||||
_, err := as.client.UpdateMember(group.EmailId, member.Email, member)
|
||||
if err != nil {
|
||||
logErr := fmt.Errorf("unable to update %s in %q as %s: %w", memberEmailId, group.EmailId, role, err)
|
||||
log.Printf("%s\n", logErr)
|
||||
errs = append(errs, logErr)
|
||||
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 {
|
||||
log.Printf("Adding %s to %q as a %s\n", memberEmailId, group.EmailId, role)
|
||||
_, err := as.client.InsertMember(group.EmailId, member)
|
||||
if err != nil {
|
||||
logErr := fmt.Errorf("unable to add %s to %q as %s: %w", memberEmailId, group.EmailId, role, err)
|
||||
log.Printf("%s\n", logErr)
|
||||
errs = append(errs, logErr)
|
||||
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 as.checkForAPIErr404(err) {
|
||||
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 EmailAddressEquals(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 {
|
||||
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("Removed 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 as.checkForAPIErr404(err) {
|
||||
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 {
|
||||
found := false
|
||||
for _, m2 := range members {
|
||||
if EmailAddressEquals(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 {
|
||||
log.Printf("Removing %s from %q as OWNER or MANAGER\n", m.Email, group.EmailId)
|
||||
err := as.client.DeleteMember(group.EmailId, m.Id)
|
||||
if err != nil {
|
||||
logErr := fmt.Errorf("unable to remove %s from %q as a %s: %w", m.Email, group.EmailId, m.Role, err)
|
||||
log.Printf("%s\n", logErr)
|
||||
errs = append(errs, logErr)
|
||||
continue
|
||||
}
|
||||
log.Printf("Removed %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 as.checkForAPIErr404(err) {
|
||||
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 {
|
||||
found := false
|
||||
for _, m2 := range members {
|
||||
if EmailAddressEquals(m2, m.Email) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
|
||||
// a person was deleted from a group, let's remove them
|
||||
if config.ConfirmChanges {
|
||||
log.Printf("Removing %s from %q as a %s\n", m.Email, group.EmailId, m.Role)
|
||||
err := as.client.DeleteMember(group.EmailId, m.Id)
|
||||
if err != nil {
|
||||
logErr := fmt.Errorf("unable to remove %s from %q as a %s: %w", m.Email, group.EmailId, m.Role, err)
|
||||
log.Printf("%s\n", logErr)
|
||||
errs = append(errs, logErr)
|
||||
continue
|
||||
}
|
||||
log.Printf("Removed %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.Member, error) {
|
||||
return as.client.ListMembers(groupKey)
|
||||
}
|
||||
|
||||
var _ AdminService = (*adminService)(nil)
|
||||
|
||||
type groupService struct {
|
||||
client GroupServiceClient
|
||||
checkForAPIErr404 clientErrCheckFunc
|
||||
}
|
||||
|
||||
// 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 gs.checkForAPIErr404(err) {
|
||||
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)
|
||||
}
|
||||
|
||||
// EmailAddressEquals checks equivalence between two e-mail addresses according
|
||||
// to the following rules:
|
||||
// - email addresses are case-insensitive (e.g. FOO@bar.com == foo@bar.com)
|
||||
// - local parts are dot-inensitive (e.g. foo@bar.com == f.o.o@bar.com)
|
||||
func EmailAddressEquals(a, b string) bool {
|
||||
// if they match case-insensitive, don't bother checking dot-insensitive
|
||||
if strings.EqualFold(a, b) {
|
||||
return true
|
||||
}
|
||||
aParts := strings.Split(a, "@")
|
||||
bParts := strings.Split(b, "@")
|
||||
// These aren't valid e-mail addresses if they don't have exactly one @
|
||||
// and we already know they're not case-insensitively equal, so return false
|
||||
if len(aParts) != 2 || len(bParts) != 2 {
|
||||
return false
|
||||
}
|
||||
// strip dots from local parts and case-insensitively compare the resulting
|
||||
// email addresses
|
||||
aLocal := strings.ReplaceAll(aParts[0], ".", "")
|
||||
bLocal := strings.ReplaceAll(bParts[0], ".", "")
|
||||
return strings.EqualFold(aLocal+"@"+aParts[1], bLocal+"@"+bParts[1])
|
||||
}
|
|
@ -0,0 +1,644 @@
|
|||
/*
|
||||
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 (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
admin "google.golang.org/api/admin/directory/v1"
|
||||
groupssettings "google.golang.org/api/groupssettings/v1"
|
||||
"k8s.io/k8s.io/groups/fake"
|
||||
)
|
||||
|
||||
// This checks for equality of two member lists based on two things:
|
||||
// 1. Email ID
|
||||
// 2. Role
|
||||
func checkForMemberListEquality(a, b []*admin.Member) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
aMap := make(map[string]*admin.Member)
|
||||
bMap := make(map[string]*admin.Member)
|
||||
for i := 0; i < len(b); i++ {
|
||||
aMap[a[i].Email] = a[i]
|
||||
bMap[b[i].Email] = b[i]
|
||||
}
|
||||
|
||||
for aEmail, aMember := range aMap {
|
||||
inB, ok := bMap[aEmail]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if inB.Role != aMember.Role {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// This checks for equality of two group lists based on three things:
|
||||
// 1. Email ID
|
||||
// 2. Name
|
||||
// 3. Descirption
|
||||
func checkForGroupListEquality(a, b []*admin.Group) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
aMap := make(map[string]*admin.Group)
|
||||
bMap := make(map[string]*admin.Group)
|
||||
for i := 0; i < len(b); i++ {
|
||||
aMap[a[i].Email] = a[i]
|
||||
bMap[b[i].Email] = b[i]
|
||||
}
|
||||
|
||||
for aEmail, aGroup := range aMap {
|
||||
inB, ok := bMap[aEmail]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if inB.Name != aGroup.Name || inB.Description != aGroup.Description {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// This checks for equality of one admin.Group list and one GoogleGroup list based on 3 things:
|
||||
// 1. Email ID
|
||||
// 2. Name
|
||||
// 3. Descirption
|
||||
func checkForAdminGroupGoogleGroupEquality(a []*admin.Group, b []GoogleGroup) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
aMap := make(map[string]*admin.Group)
|
||||
bMap := make(map[string]GoogleGroup)
|
||||
for i := 0; i < len(b); i++ {
|
||||
aMap[a[i].Email] = a[i]
|
||||
bMap[b[i].EmailId] = b[i]
|
||||
}
|
||||
|
||||
for aEmail, aGroup := range aMap {
|
||||
inB, ok := bMap[aEmail]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if inB.Name != aGroup.Name || inB.Description != aGroup.Description {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func getMemberListInPrintableForm(members []*admin.Member) []admin.Member {
|
||||
res := make([]admin.Member, len(members))
|
||||
for i := 0; i < len(res); i++ {
|
||||
res[i] = *members[i]
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func getGroupListInPrintableForm(groups []*admin.Group) []admin.Group {
|
||||
res := make([]admin.Group, len(groups))
|
||||
for i := 0; i < len(res); i++ {
|
||||
res[i] = *groups[i]
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func TestAddOrUpdateGroupMembers(t *testing.T) {
|
||||
config.ConfirmChanges = true
|
||||
cases := []struct {
|
||||
desc string
|
||||
g GoogleGroup
|
||||
members []string
|
||||
expectedMembers []*admin.Member
|
||||
role string
|
||||
}{
|
||||
{
|
||||
desc: "all members already exist, no create/update",
|
||||
g: GoogleGroup{EmailId: "group1@email.com"},
|
||||
members: []string{"m1-group1@email.com"},
|
||||
expectedMembers: []*admin.Member{
|
||||
{Email: "m1-group1@email.com", Role: MemberRole},
|
||||
{Email: "m2-group1@email.com", Role: ManagerRole},
|
||||
},
|
||||
role: MemberRole,
|
||||
},
|
||||
{
|
||||
desc: "new members to add, create operation",
|
||||
g: GoogleGroup{EmailId: "group1@email.com"},
|
||||
members: []string{"new-group1@email.com"},
|
||||
expectedMembers: []*admin.Member{
|
||||
{Email: "m1-group1@email.com", Role: MemberRole},
|
||||
{Email: "m2-group1@email.com", Role: ManagerRole},
|
||||
{Email: "new-group1@email.com", Role: MemberRole},
|
||||
},
|
||||
role: MemberRole,
|
||||
},
|
||||
{
|
||||
desc: "change member role, update operation",
|
||||
g: GoogleGroup{EmailId: "group1@email.com"},
|
||||
members: []string{"m1-group1@email.com"},
|
||||
expectedMembers: []*admin.Member{
|
||||
{Email: "m1-group1@email.com", Role: OwnerRole},
|
||||
{Email: "m2-group1@email.com", Role: ManagerRole},
|
||||
},
|
||||
role: OwnerRole,
|
||||
},
|
||||
}
|
||||
|
||||
errFunc := func(err error) bool {
|
||||
return err != nil
|
||||
}
|
||||
for _, c := range cases {
|
||||
fakeClient := fake.NewAugmentedFakeAdminServiceClient()
|
||||
|
||||
adminSvc, err := NewAdminServiceWithClientAndErrFunc(fakeClient, errFunc)
|
||||
if err != nil {
|
||||
t.Errorf("error creating client %w", err)
|
||||
}
|
||||
err = adminSvc.AddOrUpdateGroupMembers(c.g, c.role, c.members)
|
||||
if err != nil {
|
||||
t.Errorf("error while executing AddOrUpdateGroupMembers for case %s: %s", c.desc, err.Error())
|
||||
}
|
||||
|
||||
result, err := fakeClient.ListMembers(c.g.EmailId)
|
||||
if err != nil {
|
||||
t.Errorf("error while listing members for groupKey %s and case %s: %w", c.g.EmailId, c.desc, err)
|
||||
}
|
||||
if !checkForMemberListEquality(result, c.expectedMembers) {
|
||||
t.Errorf("unexpected list of members for %s, expected: %#v, got: %#v",
|
||||
c.desc,
|
||||
getMemberListInPrintableForm(c.expectedMembers),
|
||||
getMemberListInPrintableForm(result),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateOrUpdateGroupIfNescessary(t *testing.T) {
|
||||
config.ConfirmChanges = true
|
||||
cases := []struct {
|
||||
desc string
|
||||
g GoogleGroup
|
||||
expectedGroups []*admin.Group
|
||||
}{
|
||||
{
|
||||
desc: "group already exists, do nothing",
|
||||
g: GoogleGroup{EmailId: "group1@email.com", Name: "group1", Description: "group1"},
|
||||
expectedGroups: []*admin.Group{
|
||||
{Email: "group1@email.com", Name: "group1", Description: "group1"},
|
||||
{Email: "group2@email.com", Name: "group2", Description: "group2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "group does not exist, add group",
|
||||
g: GoogleGroup{EmailId: "group3@email.com", Name: "group3", Description: "group3"},
|
||||
expectedGroups: []*admin.Group{
|
||||
{Email: "group1@email.com", Name: "group1", Description: "group1"},
|
||||
{Email: "group2@email.com", Name: "group2", Description: "group2"},
|
||||
{Email: "group3@email.com", Name: "group3", Description: "group3"},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "group exists, but group name was modified, update group",
|
||||
g: GoogleGroup{EmailId: "group1@email.com", Name: "group1New", Description: "group1"},
|
||||
expectedGroups: []*admin.Group{
|
||||
{Email: "group1@email.com", Name: "group1New", Description: "group1"},
|
||||
{Email: "group2@email.com", Name: "group2", Description: "group2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "group exists, but group description was modified, update group",
|
||||
g: GoogleGroup{EmailId: "group1@email.com", Name: "group1", Description: "group1New"},
|
||||
expectedGroups: []*admin.Group{
|
||||
{Email: "group1@email.com", Name: "group1", Description: "group1New"},
|
||||
{Email: "group2@email.com", Name: "group2", Description: "group2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
errFunc := func(err error) bool {
|
||||
return err != nil
|
||||
}
|
||||
for _, c := range cases {
|
||||
fakeClient := fake.NewAugmentedFakeAdminServiceClient()
|
||||
|
||||
adminSvc, err := NewAdminServiceWithClientAndErrFunc(fakeClient, errFunc)
|
||||
if err != nil {
|
||||
t.Errorf("error creating client %w", err)
|
||||
}
|
||||
|
||||
err = adminSvc.CreateOrUpdateGroupIfNescessary(c.g)
|
||||
if err != nil {
|
||||
t.Errorf("error while executing CreateOrUpdateGroupIfNescessary for case %s: %s", c.desc, err.Error())
|
||||
}
|
||||
|
||||
result, err := fakeClient.ListGroups()
|
||||
if err != nil {
|
||||
t.Errorf("error while listing groups for case %s: %w", c.desc, err)
|
||||
}
|
||||
|
||||
if !checkForGroupListEquality(result.Groups, c.expectedGroups) {
|
||||
t.Errorf("unexpected list of groups for %s, expected: %#v, got: %#v",
|
||||
c.desc,
|
||||
getGroupListInPrintableForm(c.expectedGroups),
|
||||
getGroupListInPrintableForm(result.Groups),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteGroupsIfNecessary(t *testing.T) {
|
||||
config.ConfirmChanges = true
|
||||
cases := []struct {
|
||||
desc string
|
||||
desiredState []GoogleGroup
|
||||
}{
|
||||
{
|
||||
desc: "states match, nothing to reconcile",
|
||||
desiredState: []GoogleGroup{
|
||||
{EmailId: "group1@email.com", Name: "group1", Description: "group1"},
|
||||
{EmailId: "group2@email.com", Name: "group2", Description: "group2"},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "mismatch in desired state, delete group2",
|
||||
desiredState: []GoogleGroup{
|
||||
{EmailId: "group1@email.com", Name: "group1", Description: "group1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
errFunc := func(err error) bool {
|
||||
return err != nil
|
||||
}
|
||||
for _, c := range cases {
|
||||
fakeClient := fake.NewAugmentedFakeAdminServiceClient()
|
||||
|
||||
adminSvc, err := NewAdminServiceWithClientAndErrFunc(fakeClient, errFunc)
|
||||
if err != nil {
|
||||
t.Errorf("error creating client %w", err)
|
||||
}
|
||||
groupsConfig.Groups = c.desiredState
|
||||
|
||||
err = adminSvc.DeleteGroupsIfNecessary()
|
||||
if err != nil {
|
||||
t.Errorf("error while executing DeleteGroupsIfNecessary for case %s: %s", c.desc, err.Error())
|
||||
}
|
||||
|
||||
result, err := fakeClient.ListGroups()
|
||||
if err != nil {
|
||||
t.Errorf("error while listing groups for case %s: %w", c.desc, err)
|
||||
}
|
||||
|
||||
if !checkForAdminGroupGoogleGroupEquality(result.Groups, c.desiredState) {
|
||||
t.Errorf("unexpected list of groups for %s, expected: %#v, got: %#v",
|
||||
c.desc,
|
||||
c.desiredState,
|
||||
getGroupListInPrintableForm(result.Groups),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveOwnerOrManagersFromGroup(t *testing.T) {
|
||||
config.ConfirmChanges = true
|
||||
cases := []struct {
|
||||
desc string
|
||||
g GoogleGroup
|
||||
desiredState []string
|
||||
expectedMembers []*admin.Member
|
||||
}{
|
||||
{
|
||||
desc: "state matches, no deletion",
|
||||
g: GoogleGroup{EmailId: "group1@email.com"},
|
||||
desiredState: []string{"m1-group1@email.com", "m2-group1@email.com"},
|
||||
expectedMembers: []*admin.Member{
|
||||
{Email: "m1-group1@email.com", Role: MemberRole},
|
||||
{Email: "m2-group1@email.com", Role: ManagerRole},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "state does not match, but member to delete has MEMBER role, skip deletion",
|
||||
g: GoogleGroup{EmailId: "group1@email.com"},
|
||||
desiredState: []string{"m2-group1@email.com"},
|
||||
expectedMembers: []*admin.Member{
|
||||
{Email: "m1-group1@email.com", Role: MemberRole},
|
||||
{Email: "m2-group1@email.com", Role: ManagerRole},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "state does not match, member to delete is OWNER/MANAGER, perform deletion",
|
||||
g: GoogleGroup{EmailId: "group1@email.com"},
|
||||
desiredState: []string{"m1-group1@email.com"},
|
||||
expectedMembers: []*admin.Member{
|
||||
{Email: "m1-group1@email.com", Role: MemberRole},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
errFunc := func(err error) bool {
|
||||
return err != nil
|
||||
}
|
||||
for _, c := range cases {
|
||||
fakeClient := fake.NewAugmentedFakeAdminServiceClient()
|
||||
|
||||
adminSvc, err := NewAdminServiceWithClientAndErrFunc(fakeClient, errFunc)
|
||||
if err != nil {
|
||||
t.Errorf("error creating client %w", err)
|
||||
}
|
||||
|
||||
err = adminSvc.RemoveOwnerOrManagersFromGroup(c.g, c.desiredState)
|
||||
if err != nil {
|
||||
t.Errorf("error while executing RemoveOwnerOrManagersFromGroup for case %s: %s", c.desc, err.Error())
|
||||
}
|
||||
|
||||
result, err := fakeClient.ListMembers(c.g.EmailId)
|
||||
if err != nil {
|
||||
t.Errorf("error while listing members for groupKey %s and case %s: %w", c.g.EmailId, c.desc, err)
|
||||
}
|
||||
|
||||
if !checkForMemberListEquality(result, c.expectedMembers) {
|
||||
t.Errorf("unexpected list of members for %s, expected: %#v, got: %#v",
|
||||
c.desc,
|
||||
getMemberListInPrintableForm(c.expectedMembers),
|
||||
getMemberListInPrintableForm(result),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveMembersFromGroup(t *testing.T) {
|
||||
config.ConfirmChanges = true
|
||||
cases := []struct {
|
||||
desc string
|
||||
g GoogleGroup
|
||||
desiredState []string
|
||||
expectedMembers []*admin.Member
|
||||
}{
|
||||
{
|
||||
desc: "state matches, no deletion",
|
||||
g: GoogleGroup{EmailId: "group1@email.com"},
|
||||
desiredState: []string{"m1-group1@email.com", "m2-group1@email.com"},
|
||||
expectedMembers: []*admin.Member{
|
||||
{Email: "m1-group1@email.com", Role: MemberRole},
|
||||
{Email: "m2-group1@email.com", Role: ManagerRole},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "state does not match, member to delete in MEMBER role, perform deletion",
|
||||
g: GoogleGroup{EmailId: "group1@email.com"},
|
||||
desiredState: []string{"m2-group1@email.com"},
|
||||
expectedMembers: []*admin.Member{
|
||||
{Email: "m2-group1@email.com", Role: ManagerRole},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "state does not match, member to delete is OWNER/MANAGER, perform deletion",
|
||||
g: GoogleGroup{EmailId: "group1@email.com"},
|
||||
desiredState: []string{"m1-group1@email.com"},
|
||||
expectedMembers: []*admin.Member{
|
||||
{Email: "m1-group1@email.com", Role: MemberRole},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
errFunc := func(err error) bool {
|
||||
return err != nil
|
||||
}
|
||||
for _, c := range cases {
|
||||
fakeClient := fake.NewAugmentedFakeAdminServiceClient()
|
||||
|
||||
adminSvc, err := NewAdminServiceWithClientAndErrFunc(fakeClient, errFunc)
|
||||
if err != nil {
|
||||
t.Errorf("error creating client %w", err)
|
||||
}
|
||||
|
||||
err = adminSvc.RemoveMembersFromGroup(c.g, c.desiredState)
|
||||
if err != nil {
|
||||
t.Errorf("error while executing RemoveMembersFromGroup for case %s: %s", c.desc, err.Error())
|
||||
}
|
||||
|
||||
result, err := fakeClient.ListMembers(c.g.EmailId)
|
||||
if err != nil {
|
||||
t.Errorf("error while listing members for groupKey %s and case %s: %w", c.g.EmailId, c.desc, err)
|
||||
}
|
||||
|
||||
if !checkForMemberListEquality(result, c.expectedMembers) {
|
||||
t.Errorf("unexpected list of members for %s, expected: %#v, got: %#v",
|
||||
c.desc,
|
||||
getMemberListInPrintableForm(c.expectedMembers),
|
||||
getMemberListInPrintableForm(result),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateGroupSettings(t *testing.T) {
|
||||
config.ConfirmChanges = true
|
||||
cases := []struct {
|
||||
desc string
|
||||
g GoogleGroup
|
||||
expectedSettings *groupssettings.Groups
|
||||
}{
|
||||
{
|
||||
desc: "group settings match, no change",
|
||||
g: GoogleGroup{
|
||||
EmailId: "group1@email.com",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "CAN_REQUEST_TO_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "true",
|
||||
},
|
||||
},
|
||||
expectedSettings: &groupssettings.Groups{
|
||||
AllowExternalMembers: "true",
|
||||
WhoCanJoin: "CAN_REQUEST_TO_JOIN",
|
||||
WhoCanViewMembership: "ALL_MANAGERS_CAN_VIEW",
|
||||
WhoCanViewGroup: "ALL_MEMBERS_CAN_VIEW",
|
||||
WhoCanDiscoverGroup: "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
WhoCanModerateMembers: "OWNERS_AND_MANAGERS",
|
||||
WhoCanModerateContent: "OWNERS_AND_MANAGERS",
|
||||
WhoCanPostMessage: "ALL_MEMBERS_CAN_POST",
|
||||
MessageModerationLevel: "MODERATE_NONE",
|
||||
MembersCanPostAsTheGroup: "true",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "group settings don't match, attempt to update",
|
||||
g: GoogleGroup{
|
||||
EmailId: "group1@email.com",
|
||||
Settings: map[string]string{
|
||||
"AllowExternalMembers": "true",
|
||||
"WhoCanJoin": "CAN_REQUEST_TO_JOIN",
|
||||
"WhoCanViewMembership": "ALL_MANAGERS_CAN_VIEW",
|
||||
"WhoCanViewGroup": "ALL_MEMBERS_CAN_VIEW",
|
||||
"WhoCanDiscoverGroup": "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
"WhoCanModerateMembers": "OWNERS_ONLY",
|
||||
"WhoCanModerateContent": "OWNERS_AND_MANAGERS",
|
||||
"WhoCanPostMessage": "ALL_MEMBERS_CAN_POST",
|
||||
"MessageModerationLevel": "MODERATE_NONE",
|
||||
"MembersCanPostAsTheGroup": "false",
|
||||
},
|
||||
},
|
||||
expectedSettings: &groupssettings.Groups{
|
||||
AllowExternalMembers: "true",
|
||||
WhoCanJoin: "CAN_REQUEST_TO_JOIN",
|
||||
WhoCanViewMembership: "ALL_MANAGERS_CAN_VIEW",
|
||||
WhoCanViewGroup: "ALL_MEMBERS_CAN_VIEW",
|
||||
WhoCanDiscoverGroup: "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
WhoCanModerateMembers: "OWNERS_ONLY",
|
||||
WhoCanModerateContent: "OWNERS_AND_MANAGERS",
|
||||
WhoCanPostMessage: "ALL_MEMBERS_CAN_POST",
|
||||
MessageModerationLevel: "MODERATE_NONE",
|
||||
MembersCanPostAsTheGroup: "false",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "group settings don't match, attempt to update (test for defaults being set)",
|
||||
g: GoogleGroup{EmailId: "group1@email.com", Settings: map[string]string{}},
|
||||
expectedSettings: &groupssettings.Groups{
|
||||
AllowExternalMembers: "true",
|
||||
WhoCanJoin: "INVITED_CAN_JOIN",
|
||||
WhoCanViewMembership: "ALL_MANAGERS_CAN_VIEW",
|
||||
WhoCanViewGroup: "ALL_MEMBERS_CAN_VIEW",
|
||||
WhoCanDiscoverGroup: "ALL_IN_DOMAIN_CAN_DISCOVER",
|
||||
WhoCanModerateMembers: "OWNERS_AND_MANAGERS",
|
||||
WhoCanModerateContent: "OWNERS_AND_MANAGERS",
|
||||
WhoCanPostMessage: "ALL_MEMBERS_CAN_POST",
|
||||
MessageModerationLevel: "MODERATE_NONE",
|
||||
MembersCanPostAsTheGroup: "false",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
errFunc := func(err error) bool {
|
||||
return err != nil
|
||||
}
|
||||
for _, c := range cases {
|
||||
fakeClient := fake.NewAugmentedFakeGroupServiceClient()
|
||||
|
||||
groupSvc, err := NewGroupServiceWithClientAndErrFunc(fakeClient, errFunc)
|
||||
if err != nil {
|
||||
t.Errorf("error creating client %w", err)
|
||||
}
|
||||
|
||||
err = groupSvc.UpdateGroupSettings(c.g)
|
||||
if err != nil {
|
||||
t.Errorf("error while executing UpdateGroupSettings for case %s: %s", c.desc, err.Error())
|
||||
}
|
||||
|
||||
result, err := fakeClient.Get(c.g.EmailId)
|
||||
if err != nil {
|
||||
t.Errorf("error while getting groupsettings of group with groupKey %s: %w", c.g.EmailId, err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, c.expectedSettings) {
|
||||
t.Errorf("unexpected groupsettings for case %s, expected: %#v, got: %#v", c.desc, c.expectedSettings, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailAddressEquals(t *testing.T) {
|
||||
testcases := []struct {
|
||||
name string
|
||||
a string
|
||||
b string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
a: "",
|
||||
b: "",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "empty vs not",
|
||||
a: "",
|
||||
b: "foo@bar.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "equal",
|
||||
a: "foo@bar.com",
|
||||
b: "foo@bar.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "different",
|
||||
a: "foo@bar.com",
|
||||
b: "bar@foo.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "equal case-insensitive",
|
||||
a: "foo@bar.com",
|
||||
b: "FOO@bar.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "equal dot-insensitive",
|
||||
a: "foo@bar.com",
|
||||
b: "f.o.o@bar.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "equal case-and-dot-insensitive",
|
||||
a: "foo@bar.com",
|
||||
b: "F.O.O@bar.com",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "host is dot-sensitive",
|
||||
a: "foo@bar.com",
|
||||
b: "f.o.o@b.a.r.com",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "host is case-insensitive",
|
||||
a: "foo@bar.com",
|
||||
b: "foo@BAR.com",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual := EmailAddressEquals(tc.a, tc.b)
|
||||
if actual != tc.expected {
|
||||
t.Errorf("Expected %v emailequals %v to be %v, got: %v", tc.a, tc.b, tc.expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
# The OWNERS file is used by prow to automatically merge approved PRs.
|
||||
|
||||
approvers:
|
||||
- productivity-writers
|
||||
reviewers:
|
||||
- productivity-writers
|
||||
- productivity-reviewers
|
|
@ -0,0 +1,74 @@
|
|||
groups:
|
||||
#
|
||||
# k8s-infra owners for sig-owned subprojects
|
||||
#
|
||||
# Each group here represents highly privileged access to kubernetes project
|
||||
# infrastructure owned or managed by this SIG. A high level of trust is
|
||||
# required for membership in these groups.
|
||||
#
|
||||
|
||||
###
|
||||
### GROUPS FOR GKE/GCP RBAC
|
||||
###
|
||||
|
||||
- email-id: kn-infra-gcp-org-admins@knative.team
|
||||
name: kn-infra-gcp-org-admins
|
||||
description: |-
|
||||
granted owner access to the knative.team GCP organization, as well as
|
||||
additional privileges necessary for billing and admin purposes
|
||||
|
||||
access granted via custom organization role organization.admin
|
||||
settings:
|
||||
ReconcileMembers: "true"
|
||||
members:
|
||||
- chizhg@google.com
|
||||
- chizhg@knative.team
|
||||
- cy@knative.team
|
||||
- racker-maha-66b7d200@gcp.rackspace.com # Mahamed Ali
|
||||
- cy@borg.dev # Mahamed Ali
|
||||
|
||||
# Every GKE RBAC group should be added here.
|
||||
- email-id: gke-security-groups@knative.team
|
||||
name: gke-security-groups
|
||||
description: |-
|
||||
Security Groups for GKE clusters
|
||||
settings:
|
||||
ReconcileMembers: "true"
|
||||
WhoCanViewMembership: "ALL_MEMBERS_CAN_VIEW" # needed for RBAC
|
||||
members:
|
||||
- k8s-infra-rbac-prow@knative.team
|
||||
|
||||
# GKE RBAC groups:
|
||||
# - grant access to the `namespace-user` role for a single namespace on the `aaa` cluster
|
||||
# - must have WhoCanViewMemberShip: "ALL_MEMBERS_CAN_VIEW"
|
||||
# - must be members of gke-security-groups@kubernetes.io
|
||||
|
||||
- email-id: k8s-infra-rbac-prow@knative.team
|
||||
name: k8s-infra-rbac-prow
|
||||
description: |-
|
||||
Grants access to the prow cluster
|
||||
settings:
|
||||
ReconcileMembers: "true"
|
||||
WhoCanViewMembership: "ALL_MEMBERS_CAN_VIEW" # required
|
||||
members:
|
||||
- chizhg@google.com
|
||||
|
||||
###
|
||||
### Productivity WG related mailing lists
|
||||
###
|
||||
|
||||
- email-id: automation@knative.team
|
||||
name: automation
|
||||
description: |-
|
||||
User group for administrators of knative-automation.
|
||||
settings:
|
||||
AllowExternalMembers: "true"
|
||||
owners:
|
||||
- evana@gcp.vmware.com
|
||||
- evankanderson@knative.team
|
||||
- markusthoemmes@gmail.com
|
||||
- mattmoor@knative.team
|
||||
- mattomata@gmail.com
|
||||
- tcnghia@gmail.com
|
||||
managers:
|
||||
- chizhg@google.com
|
|
@ -0,0 +1,6 @@
|
|||
# The OWNERS file is used by prow to automatically merge approved PRs.
|
||||
|
||||
approvers:
|
||||
- security-writers
|
||||
reviewers:
|
||||
- security-writers
|
|
@ -0,0 +1,20 @@
|
|||
# Security WG
|
||||
groups:
|
||||
- email-id: security@knative.team
|
||||
name: security
|
||||
description: |-
|
||||
Security WG email
|
||||
settings:
|
||||
WhoCanPostMessage: "ANYONE_CAN_POST"
|
||||
ReconcileMembers: "true"
|
||||
owners:
|
||||
- evan.k.anderson@gmail.com
|
||||
- evana@vmware.com
|
||||
- evankanderson@knative.team
|
||||
- julz.friedman@uk.ibm.com
|
||||
- julz@knative.team
|
||||
members:
|
||||
- pablo@triggermesh.com
|
||||
- paulschw@us.ibm.com
|
||||
- vaikas@gmail.com
|
||||
|
Loading…
Reference in New Issue