diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..cb3c334 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,75 @@ +name: Publish new release + +on: + push: + branches: [ main ] + tags: + - v* + +permissions: + contents: write + +jobs: + golangci: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v4 + with: + go-version: '1.20' + cache: true + - uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + # Optional: golangci-lint command line arguments. + args: --issues-exit-code=1 --timeout=5m --disable typecheck + - name: go vet + run: go vet ./... + + scan-code: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + scanners: 'vuln,secret,config' + ignore-unfixed: true + format: 'sarif' + output: 'trivy-results.sarif' + severity: 'CRITICAL,HIGH, MEDIUM, LOW' + exit-code: '1' + + - name: Upload Trivy scan results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: 'trivy-results.sarif' + category: 'code' + + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '>=1.20.0' + cache: true + + - name: Install dependencies + run: go get . + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v4 + with: + distribution: goreleaser + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/renovate.json b/.github/renovate.json similarity index 100% rename from renovate.json rename to .github/renovate.json diff --git a/.gitignore b/.gitignore index bd2ca48..7a6b44a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ vendor ./vendor -users.yaml -groups.yaml +labels.yaml multena-rbac-collector diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2fc1d7e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Constantin Winkler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e1fb7e5 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# Multena RBAC Collector + +![Go Version](https://img.shields.io/badge/Go-v1.20-blue) +[![Go Report Card](https://goreportcard.com/badge/github.com/gepaplexx/multena-rbac-collector)](https://goreportcard.com/report/github.com/gepaplexx/multena-rbac-collector) +[![Release](https://img.shields.io/github/v/release/gepaplexx/multena-rbac-collector)](https://github.com/gepaplexx/multena-rbac-collector/releases/latest) +![License](https://img.shields.io/github/license/gepaplexx/multena-rbac-collector) + +The `multena-rbac-collector` is a Go program that collects the Role-Based Access Control (RBAC) data from an OpenShift +cluster, specifically users and groups with their respective access to namespaces. + +```mermaid +graph TB + A[Start] --> B[Collects Users, Groups, and Namespaces] + B --> C{Checks Access} + C -- Yes --> D[Records Access Rights] + D --> E{All Entities Checked?} + E -- Yes --> F[Write to YAML] + E -- No --> C + F --> G[End] +``` + +## Features + +- Collects the list of all users, groups, and namespaces from an OpenShift cluster +- Checks access for each user and group to each namespace +- Outputs the collected data into a YAML file + +## Prerequisites + +- OpenShift cluster +- Configured `kubeconfig` file for accessing the cluster +- Cluster admin privileges for the user that runs the program + +## Installation + +Clone the repository and build using Go: + +```bash +git clone https://github.com/gepaplexx/multena-rbac-collector.git +cd multena-rbac-collector +go build +``` + +## Usage + +Simply run the compiled binary: + +```bash +./multena-rbac-collector +``` + +This will create a `labels.yaml` file with the collected RBAC data. + +## Contributing + +We welcome contributions! Please open an issue if you have any questions or suggestions. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/collect.go b/collect.go index 6ccaf84..1a2bc21 100644 --- a/collect.go +++ b/collect.go @@ -9,6 +9,16 @@ import ( "strings" ) +var ( + userAllowedNamespaces map[string][]string + groupAllowedNamespaces map[string][]string +) + +// collectAll performs a series of collection tasks to gather information on namespaces, groups, and users from the +// Kubernetes cluster. It uses progress bar to provide an interactive console output during these operations. For each +// user and group, it checks the access to resources in namespaces. It returns two maps, one for users +// and one for groups, where each map key is a username or group name and the value is a list of namespace names where +// the user or group has access. If an error occurs during these operations, it also returns an error. func collectAll() (map[string][]string, map[string][]string, error) { bar := progressbar.NewOptions(3, progressbar.OptionEnableColorCodes(true), @@ -82,7 +92,8 @@ func collectAll() (map[string][]string, map[string][]string, error) { BarStart: "[", BarEnd: "]", })) - userAllowedNamespaces := make(map[string][]string) + + userAllowedNamespaces = make(map[string][]string, len(users)) for _, user := range users { bar.Describe(fmt.Sprintf("[cyan][2/3][reset] Checking access for user %s ...", user)) for _, ns := range namespaces { @@ -114,7 +125,8 @@ func collectAll() (map[string][]string, map[string][]string, error) { BarStart: "[", BarEnd: "]", })) - groupAllowedNamespaces := make(map[string][]string) + + groupAllowedNamespaces = make(map[string][]string, len(groups)) for _, group := range groups { bar.Describe(fmt.Sprintf("[cyan][2/3][reset] Checking access for group %s ...", group)) for _, ns := range namespaces { @@ -135,6 +147,10 @@ func collectAll() (map[string][]string, map[string][]string, error) { return userAllowedNamespaces, groupAllowedNamespaces, nil } +// checkAccessForUserOrGroup checks whether a given user or group has the specified access (verb) to a certain resource +// in a specific namespace. It achieves this by creating and submitting a SubjectAccessReview to Kubernetes' +// authorization API. The function returns true if the access is allowed, false otherwise, and an error if the check +// operation itself fails. func checkAccessForUserOrGroup(userOrGroup string, namespace string, verb string, resource string) (bool, error) { // Create a new SubjectAccessReview sar := &v1.SubjectAccessReview{ diff --git a/go.sum b/go.sum index 92c5b72..50b7a6e 100644 --- a/go.sum +++ b/go.sum @@ -256,8 +256,6 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -318,8 +316,6 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/init.go b/init.go index 2db55f3..e58e0c7 100644 --- a/init.go +++ b/init.go @@ -10,11 +10,14 @@ import ( "path/filepath" ) +// Declaring global variables for Kubernetes Clientset and UserClientset from OpenShift. var ( clientset *kubernetes.Clientset userClientset *versioned.Clientset ) +// init is a special function in Go that gets called upon the package initialization. +// This function handles the initialization and configuration of the Kubernetes and OpenShift clientsets. func init() { var kubeconfig *string if home := homedir.HomeDir(); home != "" { @@ -27,16 +30,19 @@ func init() { err := error(nil) Config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig) if err != nil { - + fmt.Println(err) + return } clientset, err = kubernetes.NewForConfig(Config) if err != nil { fmt.Println(err) + return } userClientset, err = versioned.NewForConfig(Config) if err != nil { fmt.Println(err) + return } } diff --git a/main.go b/main.go index 6bf4ece..33e6874 100644 --- a/main.go +++ b/main.go @@ -5,38 +5,44 @@ import ( "gopkg.in/yaml.v3" "io/fs" "os" + "os/signal" + "syscall" ) +// main This function is the entry point of the application. It sets up a mechanism to handle the SIGINT (Ctrl+C) and +// SIGTERM system signals. If such a signal is received, it calls the mapsToYaml function to save the collected data +// to a YAML file, and then exits the program with a status code of 1. After setting up this mechanism, it calls the +// collectAll function to collect the RBAC data from the Kubernetes cluster. If this function call results in an error, +// it prints the error and terminates the program. If the function call succeeds, it then calls the mapsToYaml function +// to save the collected data to a YAML file. func main() { - uan, gan, err := collectAll() - users := map[string]map[string][]string{"users": uan} - groups := map[string]map[string][]string{"groups": gan} - + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + mapsToYaml() + os.Exit(1) + }() + _, _, err := collectAll() if err != nil { fmt.Println(err) } - err = mapToYAML(users, "users.yaml") - if err != nil { - fmt.Println(err) - } - err = mapToYAML(groups, "groups.yaml") - if err != nil { - fmt.Println(err) - } - + mapsToYaml() } -func mapToYAML(data map[string]map[string][]string, filename string) error { - yamlBytes, err := yaml.Marshal(data) +// mapsToYaml This function converts the userAllowedNamespaces and groupAllowedNamespaces maps into a YAML format and +// writes the YAML-formatted data into a file named labels.yaml. If the YAML conversion or file writing operations +// result in an error, the function will panic and cause the program to exit. The fs.ModePerm mode is used for the file, +// which gives read, write, and execute permissions to all. +func mapsToYaml() { + tenants := map[string]map[string][]string{"users": userAllowedNamespaces, "groups": groupAllowedNamespaces} + yamlBytes, err := yaml.Marshal(tenants) if err != nil { - return err + panic(err) } - // Write the YAML data to a file - err = os.WriteFile(filename, yamlBytes, fs.ModePerm) + err = os.WriteFile("labels.yaml", yamlBytes, fs.ModePerm) if err != nil { - return err + panic(err) } - - return nil }