diff --git a/.git-crypt/.gitattributes b/.git-crypt/.gitattributes new file mode 100644 index 00000000000..665b10e8f03 --- /dev/null +++ b/.git-crypt/.gitattributes @@ -0,0 +1,4 @@ +# Do not edit this file. To specify the files to encrypt, create your own +# .gitattributes file in the directory where your files are. +* !filter !diff +*.gpg binary diff --git a/.git-crypt/keys/default/0/10433CA4A347A8515465ED50B34A59A9D39F838B.gpg b/.git-crypt/keys/default/0/10433CA4A347A8515465ED50B34A59A9D39F838B.gpg new file mode 100644 index 00000000000..209ba216f69 Binary files /dev/null and b/.git-crypt/keys/default/0/10433CA4A347A8515465ED50B34A59A9D39F838B.gpg differ diff --git a/.git-crypt/keys/default/0/A67E5FD880EB089F2317796780D83A796103BF59.gpg b/.git-crypt/keys/default/0/A67E5FD880EB089F2317796780D83A796103BF59.gpg new file mode 100644 index 00000000000..267df7106d1 Binary files /dev/null and b/.git-crypt/keys/default/0/A67E5FD880EB089F2317796780D83A796103BF59.gpg differ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..4d361af8b85 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +groups/*.json filter=git-crypt diff=git-crypt diff --git a/groups/README.md b/groups/README.md new file mode 100644 index 00000000000..e8a84b3d0f8 --- /dev/null +++ b/groups/README.md @@ -0,0 +1,6 @@ +# Automation of Google Groups maintenance for k8s-infra permissions + +- Edit groups.yaml to add a new group or update an existing group +- All groups MUST start with "k8s-infra-" prefix for the reconcile.go to work +- Use `go run reconcile.go` to dry run the changes +- Use `go run reconcile.go --confirm` if the changes suggested in the previous step looks good diff --git a/groups/config.yaml b/groups/config.yaml new file mode 100644 index 00000000000..2f5842ea24c --- /dev/null +++ b/groups/config.yaml @@ -0,0 +1,10 @@ +# Configuration for the kubernetes.io Google Groups setup + +# file path of the google groups service account token +token-file: k8s-infra-test-project-1896690daeb3.json + +# Email id of the bot service account +bot-id: wg-k8s-infra-api-test@kubernetes.io + +# Configuration file for the groups/members information +groups-file: groups.yaml diff --git a/groups/go.mod b/groups/go.mod new file mode 100644 index 00000000000..751ee1c3743 --- /dev/null +++ b/groups/go.mod @@ -0,0 +1,10 @@ +module k8s.io/k8s.io/groups + +go 1.12 + +require ( + golang.org/x/net v0.0.0-20190502183928-7f726cade0ab + golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a + google.golang.org/api v0.4.0 + gopkg.in/yaml.v2 v2.2.2 +) diff --git a/groups/go.sum b/groups/go.sum new file mode 100644 index 00000000000..86dcab0d30c --- /dev/null +++ b/groups/go.sum @@ -0,0 +1,60 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +go.opencensus.io v0.21.0 h1:mU6zScU4U1YAFPHEHYk+3JC4SY7JxgkqS10ZOSyksNg= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190502183928-7f726cade0ab h1:9RfW3ktsOZxgo9YNbBAjq1FWzc/igwEcUzZz8IXgSbk= +golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA= +golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +google.golang.org/api v0.4.0 h1:KKgc1aqhV8wDPbDzlDtpvyjZFY3vjz85FP7p4wcQUyI= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/groups/groups.yaml b/groups/groups.yaml new file mode 100644 index 00000000000..b7edf3e8021 --- /dev/null +++ b/groups/groups.yaml @@ -0,0 +1,90 @@ +# This file has the list of groups in kubernetes.io gsuite org that we use for +# granting permissions to various community resources. Please ensure that the +# group is prefixed with "k8s-infra" to avoid polluting the other existing gsuite +# mailing lists. +groups: + - email-id: k8s-infra-alerts@kubernetes.io + members: + - amwat@google.com + - bentheelder@google.com + - cblecker@gmail.com + - davanum@gmail.com + - ihor@cncf.io + - jgrafton@google.com + - mkumatag@in.ibm.com + - spiffxp@google.com + - thockin@google.com + - email-id: k8s-infra-cluster-admins@kubernetes.io + members: + - davanum@gmail.com + - thockin@google.com + - justinsb@google.com + - stefan.schimanski@gmail.com + - nikitaraghunath@gmail.com + - email-id: k8s-infra-dns-admins@kubernetes.io + members: + - bentheelder@google.com + - brendan.d.burns@gmail.com + - cblecker@gmail.com + - davanum@gmail.com + - hh@ii.coop + - ihor@cncf.io + - jgrafton@google.com + - spiffxp@google.com + - thockin@google.com + - email-id: k8s-infra-gcp-accounting@kubernetes.io + members: + - davanum@gmail.com + - spiffxp@google.com + - thockin@google.com + - justinsb@google.com + - ihor@cncf.io + - email-id: k8s-infra-gcp-auditors@kubernetes.io + members: + - davanum@gmail.com + - spiffxp@google.com + - thockin@google.com + - ihor@cncf.io + - hh@ii.coop + - email-id: k8s-infra-artifact-admins@kubernetes.io + members: + - davanum@gmail.com + - spiffxp@google.com + - thockin@google.com + - justinsb@google.com + - michelle.noorali@gmail.com + - email-id: k8s-infra-artifact-promoter-test@kubernetes.io + members: + - davanum@gmail.com + - ihor@cncf.io + - linusa@google.com + - spiffxp@google.com + - thockin@google.com + - email-id: k8s-infra-sig-release-prototype@kubernetes.io + members: + - davanum@gmail.com + - spiffxp@google.com + - thockin@google.com + - ihor@cncf.io + - hh@ii.coop + - tpepper@gmail.com + - bartek@smykla.com + - email-id: k8s-infra-staging-csi@kubernetes.io + members: + - davanum@gmail.com + - email-id: k8s-infra-staging-coredns@kubernetes.io + members: + - davanum@gmail.com + - email-id: k8s-infra-staging-cluster-api@kubernetes.io + members: + - davanum@gmail.com + - ha.chuck@gmail.com + - detiber@gmail.com + - vince@vincepri.com + - email-id: k8s-infra-staging-kops@kubernetes.io + members: + - ihor@cncf.io + - davanum@gmail.com + - spiffxp@google.com + - thockin@google.com + - justinsb@google.com diff --git a/groups/k8s-infra-test-project-1896690daeb3.json b/groups/k8s-infra-test-project-1896690daeb3.json new file mode 100644 index 00000000000..8a47d0fae4a Binary files /dev/null and b/groups/k8s-infra-test-project-1896690daeb3.json differ diff --git a/groups/reconcile.go b/groups/reconcile.go new file mode 100644 index 00000000000..7e0ad52374d --- /dev/null +++ b/groups/reconcile.go @@ -0,0 +1,429 @@ +/* +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 ( + "crypto/tls" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "strings" + "time" + + "golang.org/x/net/context" + "golang.org/x/oauth2/google" + "google.golang.org/api/admin/directory/v1" + "google.golang.org/api/googleapi" + "google.golang.org/api/groupssettings/v1" + "gopkg.in/yaml.v2" +) + +type Config struct { + // the email id for the bot/service account + BotID string `yaml:"bot-id"` + + // the file with the authentication information + TokenFile string `yaml:"token-file,omitempty"` + + // the file with the groups/members information + GroupsFile string `yaml:"groups-file,omitempty"` + + // If false, don't make any mutating API calls + ConfirmChanges bool +} + +type GroupsConfig struct { + // List of google groups + Groups []GoogleGroup `yaml:"groups,omitempty"` +} + +type GoogleGroup struct { + EmailId string `yaml:"email-id"` + Description string `yaml:"description"` + + // List of members in the google group + Members []string `yaml:"members,omitempty"` +} + +func Usage() { + fmt.Fprintf(os.Stderr, ` +Usage: %s [-config ] [--confirm] +Command line flags override config values. +`, os.Args[0]) + flag.PrintDefaults() +} + +var config Config +var groupsConfig GroupsConfig + +func main() { + configFilePath := flag.String("config", "config.yaml", "the config file in yaml format") + confirmChanges := flag.Bool("confirm", false, "false by default means that we do not push anything to google groups") + + flag.Usage = Usage + flag.Parse() + + err := readConfig(configFilePath, confirmChanges) + if err != nil { + log.Fatal(err) + } + + err = readGroupsConfig(config.GroupsFile) + if err != nil { + log.Fatal(err) + } + + jsonCredentials, err := ioutil.ReadFile(config.TokenFile) + if err != nil { + log.Fatal(err) + } + + credential, err := google.JWTConfigFromJSON(jsonCredentials, admin.AdminDirectoryUserReadonlyScope, + admin.AdminDirectoryGroupScope, + admin.AdminDirectoryGroupMemberScope, + groupssettings.AppsGroupsSettingsScope) + if err != nil { + log.Fatalf("Unable to parse client secret file to config: %v\n. "+ + "Please run 'git-crypt unlock'", err) + } + credential.Subject = config.BotID + + client := credential.Client(context.Background()) + srv, err := admin.New(client) + if err != nil { + log.Fatalf("Unable to retrieve directory Client %v", err) + } + + srv2, err := groupssettings.New(client) + if err != nil { + log.Fatalf("Unable to retrieve groupssettings Service %v", err) + } + + log.Println(" =================== Current Status ======================") + err = printGroupMembersAndSettings(srv, srv2) + if err != nil { + log.Fatal(err) + } + + log.Println(" ======================= Updates =========================") + for _, g := range groupsConfig.Groups { + if !strings.HasPrefix(g.EmailId, "k8s-infra-") { + log.Fatalf("We can reconcile only groups that start with 'k8s-infra-' prefix") + } + err = createGroupIfNecessary(srv, g.EmailId, g.Description) + if err != nil { + log.Fatal(err) + } + err = updateGroupSettingsToAllowExternalMembers(srv2, g.EmailId) + if err != nil { + log.Fatal(err) + } + err = addMembersToGroup(srv, g.EmailId, g.Members) + if err != nil { + log.Fatal(err) + } + err = removeMembersFromGroup(srv, g.EmailId, g.Members) + if err != nil { + log.Fatal(err) + } + } + err = deleteGroupsIfNecessary(srv) + if err != nil { + log.Fatal(err) + } +} + +func readConfig(configFilePath *string, confirmChanges *bool) error { + content, err := ioutil.ReadFile(*configFilePath) + if err != nil { + return fmt.Errorf("error reading config from file: %v", err) + } + if err = yaml.Unmarshal(content, &config); err != nil { + return fmt.Errorf("error reading config: %v", err) + } + if confirmChanges != nil { + config.ConfirmChanges = *confirmChanges + } + return err +} + +func readGroupsConfig(groupsConfigFilePath string) error { + var content []byte + var err error + groupsUrl, err := url.ParseRequestURI(groupsConfigFilePath) + if err == nil { + // We have a URL, so try reading from it + if len(groupsUrl.Host) > 0 { + if content, err = readFromUrl(groupsUrl); err != nil { + return fmt.Errorf("error reading groups config from file: %v", err) + } + } + } else { + // We don't have a URL, we have a file path, so try reading from the file + if content, err = ioutil.ReadFile(groupsConfigFilePath); err != nil { + return fmt.Errorf("error reading groups config from file: %v", err) + } + } + if err = yaml.Unmarshal(content, &groupsConfig); err != nil { + return fmt.Errorf("error reading groups config: %v", err) + } + return nil +} + +// readFromUrl reads the rule file from provided URL. +func readFromUrl(u *url.URL) ([]byte, error) { + client := &http.Client{Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, err + } + // timeout the request after 30 seconds + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + resp, err := client.Do(req.WithContext(ctx)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + return ioutil.ReadAll(resp.Body) +} + +func printGroupMembersAndSettings(srv *admin.Service, srv2 *groupssettings.Service) error { + g, err := srv.Groups.List().Customer("my_customer").OrderBy("email").Do() + if err != nil { + return fmt.Errorf("unable to retrieve users in domain: %v", err) + } + if len(g.Groups) == 0 { + log.Println("No groups found.") + return nil + } + for _, g := range g.Groups { + // Don't touch existing mailing lists, we should + // always prefix with "k8s-infra-" + if !strings.HasPrefix(g.Email, "k8s-infra-") { + continue + } + log.Printf("%s\n", g.Email) + + g2, err := srv2.Groups.Get(g.Email).Do() + if err != nil { + return fmt.Errorf("unable to retrieve group info for group %s: %v", g.Email, err) + } + log.Printf(">> Allow external members %s\n", g2.AllowExternalMembers) + + l, err := srv.Members.List(g.Email).Do() + if err != nil { + return fmt.Errorf("unable to retrieve members in group : %v", err) + } + + if len(l.Members) == 0 { + log.Println("No members found in group.") + } else { + for _, m := range l.Members { + log.Printf(">>> %s (%s)\n", m.Email, m.Role) + } + } + log.Printf("\n") + + } + return nil +} + +func createGroupIfNecessary(srv *admin.Service, groupEmailId string, description string) error { + _, err := srv.Groups.Get(groupEmailId).Do() + if err != nil { + if apierr, ok := err.(*googleapi.Error); ok && apierr.Code == http.StatusNotFound { + if !config.ConfirmChanges { + log.Printf("dry-run : skipping creation of group %s\n", groupEmailId) + } else { + log.Printf("Trying to create group: %s\n", groupEmailId) + g4, err := srv.Groups.Insert(&admin.Group{ + Email: groupEmailId, + Name: description, + Description: "Kubernetes wg-k8s-infra test group #2", + }).Do() + if err != nil { + return fmt.Errorf("unable to add new group %s: %v", groupEmailId, err) + } + log.Printf("> Successfully created group %s\n", g4.Email) + } + } else { + return fmt.Errorf("unable to fetch group: %#v", err.Error()) + } + } + return nil +} + +func deleteGroupsIfNecessary(service *admin.Service) error { + g, err := service.Groups.List().Customer("my_customer").OrderBy("email").Do() + if err != nil { + return fmt.Errorf("unable to retrieve users in domain: %v", err) + } + if len(g.Groups) == 0 { + log.Println("No groups found.") + return nil + } + for _, g := range g.Groups { + // Don't touch existing mailing lists, we should + // always prefix with "k8s-infra-" + if !strings.HasPrefix(g.Email, "k8s-infra-") { + continue + } + found := false + for _, g2 := range groupsConfig.Groups { + if g2.EmailId == g.Email { + found = true + break + } + } + if found { + continue + } + // We did not find the group in our groups.xml, so delete the group + if config.ConfirmChanges { + err := service.Groups.Delete(g.Email).Do() + if err != nil { + return fmt.Errorf("unable to remove group %s : %v", g.Email, err) + } + log.Printf("Removing group %s\n", g.Email) + } else { + log.Printf("dry-run : Skipping removing group %s\n", g.Email) + } + + } + return nil +} + +func updateGroupSettingsToAllowExternalMembers(srv *groupssettings.Service, groupEmailId string) error { + g2, err := srv.Groups.Get(groupEmailId).Do() + if err != nil { + if apierr, ok := err.(*googleapi.Error); ok && apierr.Code == http.StatusNotFound { + log.Printf("skipping updating group settings as group %s has not yet been created\n", groupEmailId) + return nil + } + return fmt.Errorf("unable to retrieve group info for group %s: %v", groupEmailId, err) + } + if g2.AllowExternalMembers != "true" || + g2.WhoCanJoin != "INVITED_CAN_JOIN" || + g2.WhoCanViewMembership != "ALL_MEMBERS_CAN_VIEW" || + g2.WhoCanViewGroup != "ALL_MEMBERS_CAN_VIEW" || + g2.WhoCanInvite != "ALL_MANAGERS_CAN_INVITE" || + g2.WhoCanAdd != "ALL_MANAGERS_CAN_ADD" || + g2.WhoCanApproveMembers != "ALL_MANAGERS_CAN_APPROVE" || + g2.WhoCanModifyMembers != "OWNERS_ONLY" || + g2.WhoCanModerateMembers != "OWNERS_ONLY" || + g2.WhoCanDiscoverGroup != "ALL_MEMBERS_CAN_DISCOVER" { + + if config.ConfirmChanges { + _, err := srv.Groups.Patch(groupEmailId, &groupssettings.Groups{ + AllowExternalMembers: "true", + WhoCanJoin: "INVITED_CAN_JOIN", + WhoCanViewMembership: "ALL_MEMBERS_CAN_VIEW", + WhoCanViewGroup: "ALL_MEMBERS_CAN_VIEW", + WhoCanInvite: "ALL_MANAGERS_CAN_INVITE", + WhoCanAdd: "ALL_MANAGERS_CAN_ADD", + WhoCanApproveMembers: "ALL_MANAGERS_CAN_APPROVE", + WhoCanModifyMembers: "OWNERS_ONLY", + WhoCanModerateMembers: "OWNERS_ONLY", + WhoCanDiscoverGroup: "ALL_MEMBERS_CAN_DISCOVER", + }).Do() + if err != nil { + return fmt.Errorf("unable to update group info for group %s: %v", groupEmailId, err) + } + log.Printf("> Successfully updated group settings for %s to allow external members and other security settings\n", groupEmailId) + } else { + log.Printf("dry-run : skipping updating group settings for %s\n", groupEmailId) + } + } + return nil +} + +func addMembersToGroup(service *admin.Service, groupEmailId string, members []string) error { + l, err := service.Members.List(groupEmailId).Do() + if err != nil { + if apierr, ok := err.(*googleapi.Error); ok && apierr.Code == http.StatusNotFound { + log.Printf("skipping adding members to group %s as it has not yet been created\n", groupEmailId) + return nil + } + return fmt.Errorf("unable to retrieve members in group %s: %v", groupEmailId, err) + } + + for _, m := range members { + found := false + for _, m2 := range l.Members { + if m2.Email == m { + found = true + break + } + } + if found { + continue + } + // We did not find the person in the google group, so we add them + if config.ConfirmChanges { + _, err := service.Members.Insert(groupEmailId, &admin.Member{ + Email: m, + Role: "MEMBER", + }).Do() + if err != nil { + return fmt.Errorf("unable to add %s to %s : %v", m, groupEmailId, err) + } + log.Printf("Added %s to %s as a MEMBER\n", m, groupEmailId) + } else { + log.Printf("dry-run : Skipping adding %s to %s\n", m, groupEmailId) + } + } + return nil +} + +func removeMembersFromGroup(service *admin.Service, groupEmailId string, members []string) error { + l, err := service.Members.List(groupEmailId).Do() + if err != nil { + if apierr, ok := err.(*googleapi.Error); ok && apierr.Code == http.StatusNotFound { + log.Printf("skipping removing members group %s as group has not yet been created\n", groupEmailId) + return nil + } + return fmt.Errorf("unable to retrieve members in group %s: %v", groupEmailId, err) + } + + for _, m := range l.Members { + found := false + for _, m2 := range members { + if m2 == m.Email { + found = true + break + } + } + if found { + continue + } + // a person was deleted from a group, let's remove them + if config.ConfirmChanges { + err := service.Members.Delete(groupEmailId, m.Email).Do() + if err != nil { + return fmt.Errorf("unable to remove %s from %s : %v", m.Email, groupEmailId, err) + } + log.Printf("Removing %s from %s as a MEMBER\n", m.Email, groupEmailId) + } else { + log.Printf("dry-run : Skipping removing %s from %s\n", m.Email, groupEmailId) + } + } + return nil +}