├── graph.png ├── examples ├── cluster-role.yaml ├── role-binding.yaml └── deployment.yaml ├── .gitignore ├── CONTRIBUTING.md ├── .editorconfig ├── Dockerfile ├── Makefile ├── kubernetes-rbac-synchroniser_test.go ├── draw.xml ├── README.md ├── kubernetes-rbac-synchroniser.go └── LICENSE /graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudworkz/kubernetes-rbac-synchroniser/HEAD/graph.png -------------------------------------------------------------------------------- /examples/cluster-role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: sync-test 5 | rules: 6 | - apiGroups: 7 | - "" 8 | resources: ["*"] 9 | verbs: 10 | - list 11 | - get 12 | - watch 13 | -------------------------------------------------------------------------------- /examples/role-binding.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: RoleBinding 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: sync-test 6 | namespace: default 7 | subjects: 8 | - kind: User 9 | name: test@test.com 10 | apiGroup: rbac.authorization.k8s.io 11 | roleRef: 12 | kind: ClusterRole 13 | name: sync-test 14 | apiGroup: rbac.authorization.k8s.io 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | .credentials/ 17 | 18 | bin/ 19 | build/ 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing guidelines 2 | 3 | ### HowTo 4 | 5 | - Install minikube: `brew cask install virtualbox minikube` 6 | - Install go: `brew install go` 7 | - Install dependencies: `make install` 8 | - Start local k8s cluster: `minikube start --extra-config=apiserver.Authorization.Mode=RBAC` 9 | - Run rbac sync: `kubectl apply -f example/` 10 | - Run localy: `go run kubernetes-rbac-synchroniser.go -h` 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.go] 12 | indent_style = tab 13 | indent_size = 4 14 | charset = utf-8 15 | trim_trailing_whitespace = true 16 | insert_final_newline = true 17 | 18 | [Makefile] 19 | indent_style = tab 20 | indent_size = 2 21 | charset = utf-8 22 | trim_trailing_whitespace = true 23 | insert_final_newline = false 24 | 25 | [*.md] 26 | trim_trailing_whitespace = false 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # builder image 2 | FROM golang:1.11.4 as builder 3 | WORKDIR /go/src/github.com/yacut/kubernetes-rbac-synchroniser 4 | COPY . . 5 | RUN make install; \ 6 | CGO_ENABLED=0 GOOS=linux go build -o build/kubernetes-rbac-synchroniser; \ 7 | curl -o ca-certificates.crt https://curl.haxx.se/ca/cacert.pem; 8 | 9 | # final image 10 | FROM scratch 11 | COPY --from=builder /go/src/github.com/yacut/kubernetes-rbac-synchroniser/build/kubernetes-rbac-synchroniser /bin/kubernetes-rbac-synchroniser 12 | COPY --from=builder /go/src/github.com/yacut/kubernetes-rbac-synchroniser/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 13 | ENTRYPOINT ["/bin/kubernetes-rbac-synchroniser"] 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO := GO15VENDOREXPERIMENT=1 go 2 | BINARY ?= kubernetes-rbac-synchroniser 3 | pkgs = $(shell $(GO) list ./... | grep -v /vendor/) 4 | DOCKER_IMAGE_NAME ?= yacut/kubernetes-rbac-synchroniser 5 | DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git describe --tags --always)) 6 | 7 | test: 8 | @echo ">> running tests" 9 | @$(GO) test -short $(pkgs) 10 | 11 | format: 12 | @echo ">> formatting code" 13 | @$(GO) fmt $(pkgs) 14 | 15 | install: 16 | @echo ">> installing dependencies" 17 | @go get -u k8s.io/client-go/... 18 | @go get -u github.com/prometheus/client_golang/... 19 | @go get -u golang.org/x/oauth2/... 20 | @go get -u google.golang.org/api/groupssettings/v1 21 | @go get -u google.golang.org/api/admin/directory/v1 22 | @go get -u github.com/sirupsen/logrus 23 | 24 | build: 25 | @echo ">> building binaries" 26 | @$(GO) build -o build/$(BINARY) 27 | 28 | docker.build: 29 | @echo ">> building docker image" 30 | @docker build -t "$(DOCKER_IMAGE_NAME):$(DOCKER_IMAGE_TAG)" -t "$(DOCKER_IMAGE_NAME):latest" . 31 | 32 | build.push: 33 | @docker push "$(DOCKER_IMAGE_NAME)" 34 | 35 | clean: 36 | @rm -rf build 37 | @rm .credentials/kubernetes-rbac-synchroniser.json 38 | -------------------------------------------------------------------------------- /kubernetes-rbac-synchroniser_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "google.golang.org/api/admin/directory/v1" 7 | ) 8 | 9 | func TestUniq(t *testing.T) { 10 | uniqUserList1 := uniq(getFakeMembers()) 11 | list1Length := len(uniqUserList1) 12 | if list1Length != 1 { 13 | t.Errorf("Uniq was incorrect, got: %d, want: %d.", list1Length, 1) 14 | } 15 | 16 | var uniqUserList2 []*admin.Member 17 | var member1 = new(admin.Member) 18 | member1.Email = "member1@example.com" 19 | var member2 = new(admin.Member) 20 | member2.Email = "member2@example.com" 21 | var member3 = new(admin.Member) 22 | member3.Email = "member3@example.com" 23 | uniqUserList2 = append(uniqUserList2, member1) 24 | uniqUserList2 = append(uniqUserList2, member2) 25 | uniqUserList2 = append(uniqUserList2, member1) 26 | uniqUserList2 = append(uniqUserList2, member3) 27 | uniqUserList2 = append(uniqUserList2, member2) 28 | uniqUserList2 = uniq(uniqUserList2) 29 | list2Length := len(uniqUserList2) 30 | if list2Length != 3 { 31 | t.Errorf("Uniq was incorrect, got: %d, want: %d.", list2Length, 3) 32 | } 33 | if uniqUserList2[0].Email != member1.Email { 34 | t.Errorf("Uniq sort was incorrect, got: %q, want: %q.", uniqUserList2[0].Email, member1.Email) 35 | } 36 | if uniqUserList2[1].Email != member2.Email { 37 | t.Errorf("Uniq sort was incorrect, got: %q, want: %q.", uniqUserList2[1].Email, member2.Email) 38 | } 39 | if uniqUserList2[2].Email != member3.Email { 40 | t.Errorf("Uniq sort was incorrect, got: %q, want: %q.", uniqUserList2[2].Email, member3.Email) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: rbac-synchroniser-account 6 | namespace: kube-system 7 | 8 | --- 9 | apiVersion: rbac.authorization.k8s.io/v1beta1 10 | kind: ClusterRole 11 | metadata: 12 | name: rbac-synchroniser-role 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - namespaces 18 | verbs: 19 | - get 20 | - list 21 | - apiGroups: 22 | - rbac.authorization.k8s.io 23 | resources: 24 | - rolebindings 25 | - roles 26 | - clusterrolebindings 27 | - clusterroles 28 | verbs: ["*"] 29 | resourceNames: 30 | - sync-test 31 | 32 | --- 33 | apiVersion: rbac.authorization.k8s.io/v1beta1 34 | kind: ClusterRoleBinding 35 | metadata: 36 | name: rbac-synchroniser-role-binding 37 | roleRef: 38 | apiGroup: rbac.authorization.k8s.io 39 | kind: ClusterRole 40 | name: rbac-synchroniser-role 41 | subjects: 42 | - kind: ServiceAccount 43 | name: rbac-synchroniser-account 44 | namespace: kube-system 45 | 46 | --- 47 | apiVersion: apps/v1beta1 48 | kind: Deployment 49 | metadata: 50 | name: rbac-synchroniser 51 | namespace: kube-system 52 | spec: 53 | replicas: 1 54 | template: 55 | metadata: 56 | name: rbac-synchroniser 57 | annotations: 58 | prometheus.io/scrape: 'true' 59 | prometheus.io/port: '8080' 60 | labels: 61 | app: rbac-synchroniser 62 | spec: 63 | serviceAccountName: rbac-synchroniser-account 64 | containers: 65 | - name: rbac-synchroniser 66 | image: quay.io/google-cloud-tools/kubernetes-rbac-synchroniser 67 | imagePullPolicy: Always 68 | # Sync the "sync-fake@example.com" Google Group 69 | # with the "sync-test" rolebinding and the "sync-test" cluster role 70 | # in "default" namespace 71 | args: 72 | - '-update-interval=15m' 73 | - '-cluster-role-name=sync-test' 74 | - '-rolebinding-name=sync-test' 75 | - '-fake-group-response' 76 | - '-namespace-group=default:sync-fake@test.com' 77 | # - '-google-admin-email=admin@example.com' 78 | # - '-config-file-path=/secrets/credentials.json' 79 | livenessProbe: 80 | httpGet: 81 | path: /healthz 82 | port: http 83 | resources: 84 | requests: 85 | cpu: 0 86 | memory: 10Mi 87 | limits: 88 | cpu: 0.1 89 | memory: 50Mi 90 | # volumeMounts: 91 | # - name: google-admin-api-credentials 92 | # mountPath: /secrets 93 | # readOnly: true 94 | # volumes: 95 | # - name: google-admin-api-credentials 96 | # secret: 97 | # secretName: google-admin-api-credentials 98 | # items: 99 | # - key: credentials.json 100 | # path: credentials.json 101 | -------------------------------------------------------------------------------- /draw.xml: -------------------------------------------------------------------------------- 1 | 7V1dk6I8Fv413mxVW0AA4bI/xnmrdrbqrZ2L3b3qQojIDhIXYre+v34TSBBIpEGDDT06NTMaAsh5npyc83CCM/C8PXxPvd3mHyiA8czQgsMMvMwMw7U08i9tOBYNluUUDWEaBUWTfmr4Gf0FWSPbL9xHAcxqHTFCMY529UYfJQn0ca3NS1P0Xu+2RnH9rDsvhELDT9+LxdZ/RQHeFK2OsTi1/wGjcMPObDmg2LDy/F9hivYJO93MAOv8VWzeevxQ7DqzjReg90oT+DYDzylCuHi3PTzDmFqWW63Yb3lma/m1U5jgLjuYxQ5vXryH/Bvn3wsfuSnyq4G0vz4DT++bCMOfO8+nW98J9qRtg7cx21xej0Y+hLGXZey9j7aRz97H3grGT6WlnlGM0vxc3FbkODhFv2BlCwCuu1yWWzgogLSsozjmPROUQHrm1AsiYoJG8xolmHFNN9nnyjk0jX470YTMqm8wxfBQaWIm/Q7RFuL0SLqwraZV7MHIr9tgzlreT2QyHHbcTYVIxoKNE48ROCwPfkKRvGFAykEFH4NKcNrRt9tDSMfvPPR3c3jAqZfNQ4TCGL76MdoHr7vYw2uUbusoVy1O7GYCZ7F8kZidbrMd8O1RQJQBQk0akVH3gzLiT5RFOEIJ2bxCGKNtpcNjHIV0A0aUcB775JNzQXK4pzpHVZGwE128bFc4oHV0oN9ABX+4d+D8MWX8sZ255ogMsrXrCWTaAmFgQPwj+4hSvEEhSrz426m1gUGFLfAQ4X9X3v+HIjA3rJwUXoofqb+maFKkKD5F8zKKyyMkAe/EiENa2HaKwX8hxkeGlLfHiDSdvuMPRDlT8LYCqC0CauYvwccYbaBmaJ/6fNJgZiPfPoSsG2AeltqvFfoUkqEWvdXnHxmI+a7EHN6x0mGHogRnlSP/SRtOjLJAnVKEJ/V5oV9/8qb4Bic6lZfSjWGO4KN2+7zHPiMjmthr60UxuSA7JoZ8WpEmO8QlEBVikiGF63xLYRb95a3yDpQdzDakt/U0s15k/qPpZrZREOSk7ukljBZSiRTiw0xwDGVcw66iFhzUWHOsefx+HBJAfwB1zOv7o/U6I6xuupF+uOsd5qbAyza5H9EasUUzJgjyF5uPfAk0Z8KAcn7QhbGu1xzZLeYPBdOF49SnC9eQTBe6q4mTha4pmC30DkGkLN4gEX6CI3x89ZLgNYP+PiUf5kXcEXljDzlqLKXILr1tFFMI/oDxG6SH+Ng5aPlLzgLQmQYVlIEzt93qyxZBByanx5X+Qm+EKQ0uqXEYlkCu7z/3JAchbd/JyNvNqGsE5LuTKwUHr2WKqGIBWhw1hzqGa9w2N6jCTAeSkclDwCshsusQOUMgZAsIhZk+M7UikZgTp3k9OI5G/3xtcIZBZ9HdOcdRMT2enWcHNao+iFHNQYwqRq+E8g80dG0S/2O2t8WMN2e7ORzbrTow1hDAcHJLgDFKYB5agDkbbnIFSdSauicTMu2sHmwOoVz0y0l68EaO64JNB7XhLlO6FESeXZSuezZxXTYB9NtmE10k6Xs20TebMO7ZRG6GbtmEMYps4jLMJp5NGLJswhhfNjF6cIZBZ9hsQqFRJ5RNGLJswhhVNnEhMFPPJtyzwNyziU7ZRA/efH42wd3IZIshTDmlOhRD9MwR+4Jquo3YjqNcjTVtibMASnDVBVx50PfbDFtJwlgCnPdjRpAUEPVG29UbYEurF1qmhqvAFhWgX/sVTBOIYfaQrjz/ITsm/iZFSZTfaf5tKNBW8KCaAs3yAR3IONAaH1xDAi5hDFm/IpSvcDmoUrtCUarUrpwKXE7lK/Sgl5SvtHnshrqxXkPb96Wh+MJd5YLBhQUvvLilWvBisylfXcFL56HfQTkajxqo32ZkqxjMzXI0aX0B0CSD2bIUwCpqNt1hbQiF3nsGSCaVrKPwNd3HTU/eVQq8WqAT5gqBf2VXAh1mVyNiWVL+95b1gCgaUXR1gScDaXeqMGI20mRz5e2Uo4fmFKwGI1E6EsoEwGMA194+xhcC11vX+yLAKQXKXAwRPGlC8ETsnB7LuIp+OFcYfNvIaoDCYOCIcRIXum8fJznubaLjEmCtDvB0IufAgk5wwr3qaYwVsG21jODrQT65VNxtSHCAaavnSsWF/iz/V1UqDkR5vHv8dw/rW8L6RR03IBHlBgvqRWm9O6hTCerbowunM2BfOXQ3RdGdYiiy4VNC9+4YfeXQ3RQFdOGePHjMIBmDwYW4KY7cJ4ObUpwscSwJcNxvYF0yV7qNG1iWLrDFtiRsMS33+rnSEoefACufE6NtvrD9Kf//kS9Nla5TvbB0Td1S2zLs70YOYxByMIO9bDCmjxZ4pKgYS+/NI5F5ps/DCG/2K3rHnzg3TE5T1GEs9+SvDmzDNTVnBpb0qk0aK9nelhrpjZG5uHKUBjBtfL/8vE/VpwWwDSrY6jSqVgyRrRKyqlg5bBkCV/9e3n47Nzfcb7ZdBbeuNauURO+k6y1z2TWA2yLgI51zztzsOvMEiRtOLrrGFwIz/Fwg4Gc4loifvlhcD6B7q7X+I5KDJBNLO9zL5VMh98xkCs2Hag+/J1pVe9wzK7qVy332pBZJKNRPBh+2jWlW52vtquNW5nd1jsBVsHZbJ3HypZ0xLgWNcypK3IgYhUm4Z0iJ9pgWCz+Xz3LSJCoPyZAiH87LlR0piuFrmNfsD6HdtClFInns+/KKIgcSOPkC32CMducrrYZfR3EZOBNfR2GLt8T/SUbMU5QEURJeCIaCdROjB2MYNIZdN6HQqBNaN2GPfxX2hcAMuW7CrAFj2QMAsxB1SHEVdhswX1AQ6BeM9uDNmXUT1tzRrYXjmgtgOyZ3Nx+RTLfnmkXOzndVkF8uRPWyLPLREm8LsxyKL8mEZf7qwQT10tCiEWqIRFgMpAwtOtziHYcypGnPz9NQhgCfnj+UhlSIuY7ox/lNvvvIvfXILUsxKtA7Aw1dd5ASwFFXiN1aEuTFXp8iCTrinCwAfpcEL5IEy3VOLVVVw4mCfHq/i4IjEAWd7vz5yqKgI95/GIEoeBk4ExcFHfGewShEwdGDMQwaHRa2XSEKKjTqhERBR/r8oFGJghcCM3FR0JGt/2o+TOUuCraEoz14M3ZR0BWVBQH28SQgChG3PkwkJT8GIOQk5xMQs7lWeyGmH9JnN5YPCr4K1m6Vqn3TD8kAH2fmQSWIMNmS87/uUBz5x0/KQj7g0Dmd4StnIa6YGX9SFnI9OBPPQlwxIXyO9xkhN01GLgTjwixkUmAMg0aP5+12zEIGMuqEshBXltoVP2VU/KZP1bz2//aIM/whyyn+SDroxu6Qm5RvP/0EEjvSCgVHNUdK1RwmOJmA9/nxbFV3UHCWbOclVx2Iri8sf1mKIFkckJ2E7lpuoRdUPzltTG9qPluR+R5m1CkpMqG3i/LHJU/HjAtlLCRW1JQaU+WBTi1SWG5+pvPNYyCFMzrXBIt7hK3jSiHzrErj3yrbrZdpAOiODUAS+qym4xRpRDVWrxhHGZ4GCXV9vFaktQXTMKIxXiO+e9jfKDMjbSwid7G9yA3af+lU0Q2KQuv6oORu4HxukIROxaPgycfTb7AX3U8/cw++/R8= -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## kubernetes-rbac-synchroniser 2 | [![license](https://img.shields.io/github/license/google-cloud-tools/kubernetes-rbac-synchroniser.svg?maxAge=604800)](https://github.com/google-cloud-tools/kubernetes-rbac-synchroniser) 3 | [![Docker Repository on Quay](https://quay.io/repository/google-cloud-tools/kubernetes-rbac-synchroniser/status "Docker Repository on Quay")](https://quay.io/repository/google-cloud-tools/kubernetes-rbac-synchroniser) 4 | [![Docker Pulls](https://img.shields.io/docker/pulls/google-cloud-tools/kubernetes-rbac-synchroniser.svg?maxAge=604800)](https://hub.docker.com/r/google-cloud-tools/kubernetes-rbac-synchroniser) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/google-cloud-tools/kubernetes-rbac-synchroniser)](https://goreportcard.com/report/github.com/google-cloud-tools/kubernetes-rbac-synchroniser) 6 | 7 | ### What It Does 8 | 9 | RBAC Synchroniser pulls a Google Group, extracts Google Group Member Emails and updates the Kubernetes RoleBinding in the given namespace. 10 | 11 | [![graph](https://raw.githubusercontent.com/google-cloud-tools/kubernetes-rbac-synchroniser/master/graph.png)](https://raw.githubusercontent.com/google-cloud-tools/kubernetes-rbac-synchroniser/master/graph.png) 12 | 13 | ### Requirements 14 | 15 | - The service account's private key file: **-config-file-path** flag 16 | - The email of the user with permissions to access the Admin APIs: **-google-admin-email** flag 17 | 18 | > see guide: https://developers.google.com/admin-sdk/directory/v1/guides/delegation 19 | 20 | - The Google Group list per Kubernetes namespace: **-namespace-group** flag 21 | - Configure Minimal GKE IAM permissions for each Google Group: `gcloud beta iam roles create minimal_gke_role --project my_project --title "Container Engine Minimal" --description "Minimal GKE Role which allows 'gcloud container clusters get-credentials' command" --permissions "container.apiServices.get,container.apiServices.list,container.clusters.get,container.clusters.getCredentials"` 22 | 23 | > see: https://stackoverflow.com/questions/45945074/iam-and-rbac-conflicts-on-google-cloud-container-engine-gke/45945239#45945239 24 | 25 | ### Flags 26 | 27 | | Flag | Description | Defalut | 28 | | :------------------- | :------------------------------------------------------- |:----------- | 29 | | -cluster-role-name | The cluster role name with permissions. | "view" | 30 | | -config-file-path | The Path to the Service Account's Private Key file. | | 31 | | -google-admin-email | The Google Admin Email. | | 32 | | -fake-group-response | Fake Google Admin API Response. | | 33 | | -namespace-group | The group and namespace. May be used multiple times. | | 34 | | -in-cluster-config | Use in cluster kubeconfig. | true | 35 | | -kubeconfig | Absolute path to the kubeconfig file. | | 36 | | -listen-address | The address to listen on for HTTP requests. | ":8080" | 37 | | -rolebinding-name | The role binding name per namespace. | "developer" | 38 | | -update-interval | Update interval in seconds. | 15m0s | 39 | | -log-json | Log as JSON instead of the default ASCII formatter. | false | 40 | 41 | ### Prometheus metrics 42 | 43 | - **rbac_synchroniser_success**: Cumulative number of role update operations. 44 | - **rbac_synchroniser_errors**: Cumulative number of errors during role update operations. 45 | 46 | ### Examples 47 | 48 | [https://github.com/google-cloud-tools/kubernetes-rbac-synchroniser/tree/master/examples](https://github.com/google-cloud-tools/kubernetes-rbac-synchroniser/tree/master/examples) 49 | 50 | ### Links 51 | 52 | - https://developers.google.com/admin-sdk/directory/v1/guides/delegation 53 | - https://developers.google.com/admin-sdk/directory/v1/guides/manage-group-members 54 | - https://github.com/kubernetes/client-go 55 | - https://github.com/prometheus/client_golang 56 | -------------------------------------------------------------------------------- /kubernetes-rbac-synchroniser.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | "strings" 12 | "syscall" 13 | "time" 14 | 15 | "github.com/prometheus/client_golang/prometheus" 16 | "github.com/prometheus/client_golang/prometheus/promhttp" 17 | log "github.com/sirupsen/logrus" 18 | "golang.org/x/oauth2/google" 19 | "google.golang.org/api/admin/directory/v1" 20 | rbacv1beta1 "k8s.io/api/rbac/v1beta1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/client-go/kubernetes" 23 | "k8s.io/client-go/rest" 24 | "k8s.io/client-go/tools/clientcmd" 25 | ) 26 | 27 | type namespaceGroupListFlag []string 28 | 29 | func (v *namespaceGroupListFlag) Set(value string) error { 30 | *v = append(*v, value) 31 | return nil 32 | } 33 | 34 | func (v *namespaceGroupListFlag) String() string { 35 | return fmt.Sprint(*v) 36 | } 37 | 38 | var ( 39 | promSuccess = prometheus.NewCounterVec( 40 | prometheus.CounterOpts{ 41 | Name: "rbac_synchroniser_success", 42 | Help: "Cumulative number of role update operations", 43 | }, 44 | []string{"count"}, 45 | ) 46 | 47 | promErrors = prometheus.NewCounterVec( 48 | prometheus.CounterOpts{ 49 | Name: "rbac_synchroniser_errors", 50 | Help: "Cumulative number of errors during role update operations", 51 | }, 52 | []string{"count"}, 53 | ) 54 | ) 55 | var address string 56 | var clusterRoleName string 57 | var roleBindingName string 58 | var namespaceGroupList namespaceGroupListFlag 59 | var fakeGroupResponse bool 60 | var kubeConfig string 61 | var inClusterConfig bool 62 | var configFilePath string 63 | var googleAdminEmail string 64 | var updateInterval time.Duration 65 | var logJSON bool 66 | 67 | func main() { 68 | flag.StringVar(&address, "listen-address", ":8080", "The address to listen on for HTTP requests.") 69 | flag.StringVar(&clusterRoleName, "cluster-role-name", "view", "The cluster role name with permissions.") 70 | flag.StringVar(&roleBindingName, "rolebinding-name", "developer", "The role binding name per namespace.") 71 | flag.Var(&namespaceGroupList, "namespace-group", "The google group and namespace colon separated. May be used multiple times. e.g.: default:group1@test.com") 72 | flag.BoolVar(&fakeGroupResponse, "fake-group-response", false, "Fake Google Admin API Response. Always response with one group and one member: sync-fake-response@example.com.") 73 | flag.StringVar(&configFilePath, "config-file-path", "", "The Path to the Service Account's Private Key file. see https://developers.google.com/admin-sdk/directory/v1/guides/delegation") 74 | flag.StringVar(&googleAdminEmail, "google-admin-email", "", "The Google Admin Email. see https://developers.google.com/admin-sdk/directory/v1/guides/delegation") 75 | flag.BoolVar(&inClusterConfig, "in-cluster-config", true, "Use in cluster kubeconfig.") 76 | flag.StringVar(&kubeConfig, "kubeconfig", "", "Absolute path to the kubeconfig file.") 77 | flag.DurationVar(&updateInterval, "update-interval", time.Minute*15, "Update interval in seconds. e.g. 30s or 5m") 78 | flag.BoolVar(&logJSON, "log-json", false, "Log as JSON instead of the default ASCII formatter.") 79 | flag.Parse() 80 | 81 | if logJSON { 82 | log.SetFormatter(&log.JSONFormatter{ 83 | FieldMap: log.FieldMap{ 84 | log.FieldKeyTime: "@timestamp", 85 | log.FieldKeyLevel: "loglevel", 86 | }, 87 | }) 88 | } 89 | log.SetOutput(os.Stdout) 90 | 91 | if clusterRoleName == "" { 92 | flag.Usage() 93 | log.Fatal("Missing -cluster-role-name") 94 | } 95 | if roleBindingName == "" { 96 | flag.Usage() 97 | log.Fatal("Missing -role-name") 98 | } 99 | if len(namespaceGroupList) < 1 { 100 | flag.Usage() 101 | log.Fatal("Missing -namespace-group") 102 | } 103 | if configFilePath == "" { 104 | flag.Usage() 105 | log.Fatal("Missing -config-file-path") 106 | } 107 | if googleAdminEmail == "" { 108 | flag.Usage() 109 | log.Fatal("Missing -google-admin-email") 110 | } 111 | 112 | stopChan := make(chan struct{}, 1) 113 | 114 | go serveMetrics(address) 115 | go handleSigterm(stopChan) 116 | for { 117 | updateRoles() 118 | time.Sleep(updateInterval) 119 | } 120 | } 121 | 122 | func handleSigterm(stopChan chan struct{}) { 123 | signals := make(chan os.Signal, 1) 124 | signal.Notify(signals, syscall.SIGTERM) 125 | <-signals 126 | log.Info("Received SIGTERM. Terminating...") 127 | close(stopChan) 128 | } 129 | 130 | // Provides health check and metrics routes 131 | func serveMetrics(address string) { 132 | http.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { 133 | w.WriteHeader(http.StatusOK) 134 | w.Write([]byte("OK")) 135 | }) 136 | 137 | prometheus.MustRegister(promSuccess) 138 | prometheus.MustRegister(promErrors) 139 | http.Handle("/metrics", promhttp.Handler()) 140 | 141 | log.WithFields(log.Fields{ 142 | "address": address, 143 | }).Info("Server started") 144 | log.Fatal(http.ListenAndServe(address, nil)) 145 | } 146 | 147 | // Gets group users and updates kubernetes rolebindings 148 | func updateRoles() { 149 | service := getService(configFilePath, googleAdminEmail) 150 | for _, element := range namespaceGroupList { 151 | elementArray := strings.Split(element, ":") 152 | namespace, email := elementArray[0], elementArray[1] 153 | 154 | if namespace == "" || email == "" { 155 | log.WithFields(log.Fields{ 156 | "namespace": namespace, 157 | "email": email, 158 | }).Error("Could not update group. Namespace or/and email are empty.") 159 | return 160 | } 161 | 162 | result, error := getMembers(service, email) 163 | if error != nil { 164 | log.WithFields(log.Fields{ 165 | "error": error, 166 | }).Error("Unable to get members.") 167 | return 168 | } 169 | 170 | var kubeClusterConfig *rest.Config 171 | if kubeConfig != "" { 172 | outOfClusterConfig, err := clientcmd.BuildConfigFromFlags("", kubeConfig) 173 | if err != nil { 174 | log.WithFields(log.Fields{ 175 | "error": err, 176 | }).Error("Unable to get kube config.") 177 | return 178 | } 179 | kubeClusterConfig = outOfClusterConfig 180 | } else { 181 | inClusterConfig, err := rest.InClusterConfig() 182 | if err != nil { 183 | log.WithFields(log.Fields{ 184 | "error": err, 185 | }).Error("Unable to get in cluster kube config.") 186 | } 187 | kubeClusterConfig = inClusterConfig 188 | } 189 | clientset, err := kubernetes.NewForConfig(kubeClusterConfig) 190 | if err != nil { 191 | promErrors.WithLabelValues("get-kube-client").Inc() 192 | log.WithFields(log.Fields{ 193 | "error": err, 194 | }).Error("Unable to get in kube client.") 195 | return 196 | } 197 | 198 | var subjects []rbacv1beta1.Subject 199 | for _, member := range uniq(result) { 200 | subjects = append(subjects, rbacv1beta1.Subject{ 201 | Kind: "User", 202 | APIGroup: "rbac.authorization.k8s.io", 203 | Name: member.Email, 204 | }) 205 | } 206 | roleBinding := &rbacv1beta1.RoleBinding{ 207 | ObjectMeta: metav1.ObjectMeta{ 208 | Name: roleBindingName, 209 | Namespace: namespace, 210 | Annotations: map[string]string{ 211 | "lastSync": time.Now().UTC().Format(time.RFC3339), 212 | }, 213 | }, 214 | RoleRef: rbacv1beta1.RoleRef{ 215 | Kind: "ClusterRole", 216 | APIGroup: "rbac.authorization.k8s.io", 217 | Name: clusterRoleName, 218 | }, 219 | Subjects: subjects, 220 | } 221 | 222 | roleClient := clientset.RbacV1beta1().RoleBindings(namespace) 223 | updateResult, updateError := roleClient.Update(roleBinding) 224 | if updateError != nil { 225 | promErrors.WithLabelValues("role-update").Inc() 226 | log.WithFields(log.Fields{ 227 | "rolebinding": roleBindingName, 228 | "error": updateError, 229 | }).Error("Unable to update rolebinding.") 230 | return 231 | } 232 | log.WithFields(log.Fields{ 233 | "rolebinding": updateResult.GetObjectMeta().GetName(), 234 | "namespace": namespace, 235 | }).Info("Updated rolebinding.") 236 | promSuccess.WithLabelValues("role-update").Inc() 237 | } 238 | } 239 | 240 | // Build and returns an Admin SDK Directory service object authorized with 241 | // the service accounts that act on behalf of the given user. 242 | // Args: 243 | // configFilePath: The Path to the Service Account's Private Key file 244 | // googleAdminEmail: The email of the user. Needs permissions to access the Admin APIs. 245 | // Returns: 246 | // Admin SDK directory service object. 247 | func getService(configFilePath string, googleAdminEmail string) *admin.Service { 248 | if fakeGroupResponse { 249 | return nil 250 | } 251 | 252 | jsonCredentials, err := ioutil.ReadFile(configFilePath) 253 | if err != nil { 254 | promErrors.WithLabelValues("get-admin-config").Inc() 255 | log.WithFields(log.Fields{ 256 | "error": err, 257 | }).Error("Unable to read client secret file.") 258 | return nil 259 | } 260 | 261 | config, err := google.JWTConfigFromJSON(jsonCredentials, admin.AdminDirectoryGroupMemberReadonlyScope, admin.AdminDirectoryGroupReadonlyScope) 262 | if err != nil { 263 | promErrors.WithLabelValues("get-admin-config").Inc() 264 | log.WithFields(log.Fields{ 265 | "error": err, 266 | }).Error("Unable to parse client secret file to config.") 267 | return nil 268 | } 269 | config.Subject = googleAdminEmail 270 | ctx := context.Background() 271 | client := config.Client(ctx) 272 | 273 | srv, err := admin.New(client) 274 | if err != nil { 275 | promErrors.WithLabelValues("get-admin-client").Inc() 276 | log.WithFields(log.Fields{ 277 | "error": err, 278 | }).Error("Unable to retrieve Group Settings Client.") 279 | return nil 280 | } 281 | return srv 282 | } 283 | 284 | // Gets recursive the group members by email and returns the user list 285 | // Args: 286 | // service: Admin SDK directory service object. 287 | // email: The email of the group. 288 | // Returns: 289 | // Admin SDK member list. 290 | func getMembers(service *admin.Service, email string) ([]*admin.Member, error) { 291 | if fakeGroupResponse { 292 | return getFakeMembers(), nil 293 | } 294 | 295 | result, err := service.Members.List(email).Do() 296 | if err != nil { 297 | promErrors.WithLabelValues("get-members").Inc() 298 | log.WithFields(log.Fields{ 299 | "error": err, 300 | }).Error("Unable to get group members.") 301 | return nil, err 302 | } 303 | 304 | var userList []*admin.Member 305 | for _, member := range result.Members { 306 | if member.Type == "GROUP" { 307 | groupMembers, _ := getMembers(service, member.Email) 308 | userList = append(userList, groupMembers...) 309 | } else { 310 | userList = append(userList, member) 311 | } 312 | } 313 | 314 | return userList, nil 315 | } 316 | 317 | // Remove duplicates from user list 318 | // Args: 319 | // list: Admin SDK member list. 320 | // Returns: 321 | // Admin SDK member list. 322 | func uniq(list []*admin.Member) []*admin.Member { 323 | var uniqSet []*admin.Member 324 | loop: 325 | for _, l := range list { 326 | for _, x := range uniqSet { 327 | if l.Email == x.Email { 328 | continue loop 329 | } 330 | } 331 | uniqSet = append(uniqSet, l) 332 | } 333 | 334 | return uniqSet 335 | } 336 | 337 | // Build and returns a fake Admin members object. 338 | // Returns: 339 | // Admin SDK members object. 340 | func getFakeMembers() []*admin.Member { 341 | var fakeResult []*admin.Member 342 | var fakeMember = new(admin.Member) 343 | fakeMember.Email = "sync-fake-response@example.com" 344 | fakeResult = append(fakeResult, fakeMember) 345 | return fakeResult 346 | } 347 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------