├── .github ├── dependabot.yml └── workflows │ ├── bump-formula.yaml │ └── test.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── README.md ├── cmd ├── add.go ├── env.go ├── exec.go ├── list.go ├── root.go └── version.go ├── go.mod ├── go.sum ├── main.go ├── script ├── build └── generate-permission-policies └── tools └── tools.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "19:30" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/bump-formula.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [ released ] 4 | workflow_dispatch: 5 | inputs: 6 | tag-name: 7 | description: 'The git tag name to bump the formula to' 8 | required: true 9 | 10 | jobs: 11 | homebrew: 12 | name: Bump Homebrew formula 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Extract version 16 | id: extract-version 17 | run: | 18 | if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then 19 | echo "tag-name=${{ inputs.tag-name }}" >> $GITHUB_OUTPUT 20 | else 21 | echo "tag-name=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT 22 | fi 23 | - uses: mislav/bump-homebrew-formula-action@v3 24 | with: 25 | formula-name: cf-vault 26 | formula-path: cf-vault.rb 27 | homebrew-tap: jacobbednarz/homebrew-tap 28 | tag-name: ${{ steps.extract-version.outputs.tag-name }} 29 | download-url: https://github.com/jacobbednarz/cf-vault/archive/refs/tags/${{ steps.extract-version.outputs.tag-name }}.tar.gz 30 | commit-message: | 31 | Updates {{formulaName}} to {{version}} 32 | env: 33 | COMMITTER_TOKEN: ${{ secrets.HOMEBREW_UPDATER_PAT }} 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: [pull_request] 2 | name: Test 3 | env: 4 | GOPROXY: "https://proxy.golang.org" 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout code 10 | uses: actions/checkout@v3 11 | - name: Install Go 12 | uses: actions/setup-go@v4 13 | with: 14 | go-version-file: 'go.mod' 15 | - name: Format 16 | run: gofmt -d . 17 | - name: Lint 18 | run: | 19 | go generate -tags tools tools/tools.go 20 | $(go env GOPATH)/bin/golint -set_exit_status . 21 | - name: Vet 22 | run: go vet 23 | - name: Test 24 | run: go test -v -race ./... 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | build/* 3 | dist/* 4 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - env: 3 | - CGO_ENABLED=0 4 | mod_timestamp: '{{ .CommitTimestamp }}' 5 | flags: 6 | - -trimpath 7 | asmflags: 8 | - all=-trimpath=$GOPATH 9 | gcflags: 10 | - all=-trimpath=$GOPATH 11 | ldflags: 12 | - -s -w -X github.com/jacobbednarz/cf-vault/cmd.Rev={{ .Version }} 13 | goos: 14 | - darwin 15 | - freebsd 16 | - linux 17 | - windows 18 | archives: 19 | - format: zip 20 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 21 | checksum: 22 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 23 | algorithm: sha256 24 | changelog: 25 | skip: true 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jacob Bednarz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cf-vault 2 | 3 | Manage your Cloudflare credentials, securely. 4 | 5 | The goal of this project is to ensure that when you need to interact with Cloudflare: 6 | 7 | - You're not storing credentials unencrypted; and 8 | - You're not exposing your credentials to the entire environment or to all 9 | processes; and 10 | - You're not using long lived credentials 11 | 12 | To achieve this, `cf-vault` uses the concept of profiles with associated scopes 13 | to either generate short lived API tokens or retrieve the API key from secure 14 | storage (such as Mac OS keychain). 15 | 16 | ## Demo 17 | 18 | ![demo](https://user-images.githubusercontent.com/283234/94203859-8c2d8680-ff03-11ea-8cd6-21161224c2ff.gif) 19 | 20 | 21 | ## Install 22 | 23 | ``` 24 | $ brew tap jacobbednarz/tap 25 | $ brew install jacobbednarz/tap/cf-vault 26 | ``` 27 | 28 | ## Getting started 29 | 30 | 1. First step is to generate a new API key or API token. Either will work 31 | however there are some subtle differences to take into consideration before 32 | choosing your path. 33 | 34 | - API tokens **are not** supported by all services yet. Regardless of whether 35 | you are using the short lived credentials or long lived token, it may not 36 | work for all services and you may need to have a backup profile defined 37 | using an API key to cover all scenarios . 38 | - API keys **are** supported everywhere however they cannot be scoped. API 39 | keys have the permission and scopes that your user account has. This can be 40 | dangerous so be sure to tread carefully as it may have unintended consequences. 41 | 42 | While it is possible (and better practice of [principle of least privilege]), 43 | to use an API token with only permissions to create a new API token, this 44 | isn't really viable for all use cases yet. The recommended approach is to use 45 | the API key for the profile and rely on a custom policy to scope the short 46 | lived credential. This allows the best of both worlds where if you need to 47 | use a service that doesn't support API tokens, you don't need to create a new 48 | profile. 49 | 50 | To create a new API token: 51 | 52 | ``` 53 | > https://dash.cloudflare.com/ 54 | > My Profile 55 | > API Tokens 56 | > Create API token 57 | ``` 58 | 59 | To retrieve your API key: 60 | 61 | ``` 62 | > https://dash.cloudflare.com/ 63 | > My Profile 64 | > API Tokens 65 | > Global API Key 66 | ``` 67 | 68 | 1. If you're using an API key, you can skip to the next step. Otherwise, 69 | navigate through the UI and configure what permissions and resources you'd 70 | like to assign to the token. If you're looking to use an API token to 71 | generate short lived API tokens, you should only need the single predefined 72 | "Create API tokens" permission. See the section below on generating the desired 73 | TOML output for instructions on how to do automatically convert policies from 74 | API responses. 75 | 76 | Note: Be sure to note down the API token **before** closing/navigating away 77 | from the UI as you won't be able to retrieve it again. 78 | 79 | 1. Once you have your API key or API token value, you can start using `cf-vault` 80 | by creating a profile. A profile is the collection of configuration that 81 | tells `cf-vault` how you intend to interact with the Cloudflare credentials. 82 | You need to start by calling `cf-vault add 83 | [your-profile-name]` where `[your-profile-name]` is a label for what the 84 | credential/use of the profile is. Some examples: 85 | 86 | - `cf-vault add write-everything` 87 | - `cf-vault add read-only` 88 | - `cf-vault add super-scary-access-everything` 89 | - `cf-vault add api-token-to-create-other-tokens` 90 | 91 | There is no limit on how many profiles you have if you prefer to have 92 | specific profiles for your use cases. 93 | 94 | 1. Now that you have created a profile, you can use it with `cf-vault exec 95 | [your-profile-name]`. 96 | 97 | If you do not wish to use the short lived credentials functionality, 98 | that's totally fine and you can do so by omitting the `session_duration` value 99 | and instead the long lived credentials you've setup will be used. 100 | 101 | ## Usage 102 | 103 | `cf-vault` allows you to manage your Cloudflare credentials in a safe place and 104 | only expose the credentials to the processes that require them and only for a 105 | limited timespan. 106 | 107 | ```shell 108 | $ env | grep -i cloudflare 109 | # => no results 110 | 111 | $ cf-vault exec work -- env | grep -i cloudflare 112 | CLOUDFLARE_VAULT_SESSION=work 113 | CLOUDFLARE_EMAIL=jacob@example.com 114 | CLOUDFLARE_API_KEY=s3cr3t 115 | CF_EMAIL=jacob@example.com 116 | CF_API_KEY=s3cr3t 117 | ``` 118 | 119 | If you don't provide a command, you will be dropped into a new shell with the 120 | credentials populated. 121 | 122 | ```shell 123 | $ cf-vault exec work 124 | $ env | grep -i cloudflare 125 | CLOUDFLARE_VAULT_SESSION=work 126 | CLOUDFLARE_EMAIL=jacob@example.com 127 | CLOUDFLARE_API_KEY=s3cr3t 128 | CF_EMAIL=jacob@example.com 129 | CF_API_KEY=s3cr3t 130 | 131 | $ exit 132 | $ env | grep -i cloudflare 133 | # => no results 134 | ``` 135 | 136 | ## Predefined short lived token policies 137 | 138 | If you don't need to generate a custom token policy, you can instead use one of 139 | the predefined templates which takes care of the heavy lifting for you. You can 140 | use `read-only` (read all resources) or `write-everything` (write all resources) 141 | as the `--profile-template` flag and it will generate everything needed behind 142 | the scenes on your behalf. Note: You **still** need to provide 143 | `--session-duration` as well otherwise the short lived tokens will not be 144 | generated. 145 | 146 | Examples: 147 | 148 | - `cf-vault add my-read-profile-name --profile-template "read-only" --session-duration "15m"` 149 | - `cf-vault add my-write-profile-name --profile-template "write-everything" --session-duration "15m"` 150 | 151 | ## Generating token policies 152 | 153 | While TOML is more readable, its not always straight forward to generate the 154 | desired output. Instead, you can use the Cloudflare dashboard to build the 155 | policy you'd like and then covert that to TOML using some tooling to avoid 156 | manually building your policy (though you can if you understand the syntax!). 157 | 158 | 1. Using `cf-vault add` create your profile following the prompts. 159 | 1. Create the token you'd like to use on the command line using the Cloudflare 160 | dashboard. 161 | 1. Make the API call to fetch the token you've just created. See 162 | https://developers.cloudflare.com/api/operations/user-api-tokens-token-details or 163 | https://developers.cloudflare.com/api/operations/user-api-tokens-list-tokens to fetch all tokens. 164 | 1. Write the contents of the single `result` JSON payload to a local file. For 165 | the example, I'll use `example_token.json` for the documentation. 166 | 1. Run the following command using `docker` which will pull the `go-toml` tool 167 | for coverting JSON -> TOML. Remember to replace `example_token.json` with 168 | your filename. 169 | 170 | ``` 171 | docker run -v $PWD:/workdir pelletier/go-toml jsontoml /workdir/example_token.json 172 | ``` 173 | 174 | 1. Paste the generated `policy` into your configuration file. You will need to 175 | adjust the structure slightly to match the hierarchy. For instance, if I have 176 | the following profile: 177 | 178 | ```toml 179 | [profiles] 180 | 181 | [profiles.doco-example] 182 | auth_type = "api_token" 183 | email = "me@example.com" 184 | session_duration = "15m" 185 | ``` 186 | 187 | and my policy output: 188 | 189 | ```toml 190 | [[policies]] 191 | effect = "allow" 192 | 193 | [[policies.permission_groups]] 194 | id = "eb258a38ea634c86a0c89da6b27cb6b6" 195 | name = "Access: Apps and Policies Read" 196 | 197 | [[policies.permission_groups]] 198 | id = "517b21aee92c4d89936c976ba6e4be55" 199 | name = "Zone Settings Read" 200 | 201 | [[policies.permission_groups]] 202 | id = "c8fed203ed3043cba015a93ad1616f1f" 203 | name = "Zone Read" 204 | 205 | # .. snip 206 | ``` 207 | 208 | The policy needs to be updated to prepend `profiles.doco-example` to the 209 | section keys. 210 | 211 | ```toml 212 | [[profiles.doco-example.policies]] 213 | effect = "allow" 214 | 215 | [[profiles.doco-example.policies.permission_groups]] 216 | id = "eb258a38ea634c86a0c89da6b27cb6b6" 217 | name = "Access: Apps and Policies Read" 218 | 219 | [[profiles.doco-example.policies.permission_groups]] 220 | id = "517b21aee92c4d89936c976ba6e4be55" 221 | name = "Zone Settings Read" 222 | 223 | [[profiles.doco-example.policies.permission_groups]] 224 | id = "c8fed203ed3043cba015a93ad1616f1f" 225 | name = "Zone Read" 226 | 227 | # .. snip 228 | ``` 229 | 230 | Making the complete configuration look like: 231 | 232 | ```toml 233 | [profiles] 234 | 235 | [profiles.doco-example] 236 | auth_type = "api_token" 237 | email = "me@example.com" 238 | session_duration = "15m" 239 | 240 | [[policies]] 241 | effect = "allow" 242 | 243 | [[policies.permission_groups]] 244 | id = "eb258a38ea634c86a0c89da6b27cb6b6" 245 | name = "Access: Apps and Policies Read" 246 | 247 | [[policies.permission_groups]] 248 | id = "517b21aee92c4d89936c976ba6e4be55" 249 | name = "Zone Settings Read" 250 | 251 | [[policies.permission_groups]] 252 | id = "c8fed203ed3043cba015a93ad1616f1f" 253 | name = "Zone Read" 254 | 255 | # .. snip 256 | ``` 257 | 258 | [principle of least privilege]: https://en.wikipedia.org/wiki/Principle_of_least_privilege 259 | -------------------------------------------------------------------------------- /cmd/add.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "regexp" 11 | "strings" 12 | 13 | "github.com/cloudflare/cloudflare-go" 14 | log "github.com/sirupsen/logrus" 15 | "golang.org/x/crypto/ssh/terminal" 16 | 17 | "github.com/99designs/keyring" 18 | "github.com/mitchellh/go-homedir" 19 | "github.com/spf13/cobra" 20 | 21 | "github.com/pelletier/go-toml" 22 | ) 23 | 24 | type tomlConfig struct { 25 | Profiles map[string]profile `toml:"profiles"` 26 | } 27 | 28 | type profile struct { 29 | Email string `toml:"email"` 30 | AuthType string `toml:"auth_type"` 31 | SessionDuration string `toml:"session_duration,omitempty"` 32 | Policies []policy `toml:"policies,omitempty"` 33 | } 34 | 35 | type policy struct { 36 | Effect string `toml:"effect"` 37 | ID string `toml:"id,omitempty"` 38 | PermissionGroups []permissionGroup `toml:"permission_groups"` 39 | Resources map[string]interface{} `toml:"resources"` 40 | } 41 | 42 | type permissionGroup struct { 43 | ID string `toml:"id"` 44 | Name string `toml:"name,omitempty"` 45 | } 46 | 47 | var addCmd = &cobra.Command{ 48 | Use: "add [profile]", 49 | Short: "Add a new profile to your configuration and keychain", 50 | Long: "", 51 | Example: ` 52 | Add a new profile (you will be prompted for credentials) 53 | 54 | $ cf-vault add example-profile 55 | `, 56 | Args: func(cmd *cobra.Command, args []string) error { 57 | if len(args) < 1 { 58 | return errors.New("requires a profile argument") 59 | } 60 | return nil 61 | }, 62 | PreRun: func(cmd *cobra.Command, args []string) { 63 | if verbose { 64 | log.SetLevel(log.DebugLevel) 65 | keyring.Debug = true 66 | } 67 | }, 68 | Run: func(cmd *cobra.Command, args []string) { 69 | profileName := strings.TrimSpace(args[0]) 70 | sessionDuration, _ := cmd.Flags().GetString("session-duration") 71 | profileTemplate, _ := cmd.Flags().GetString("profile-template") 72 | 73 | reader := bufio.NewReader(os.Stdin) 74 | fmt.Print("Email address: ") 75 | emailAddress, _ := reader.ReadString('\n') 76 | emailAddress = strings.TrimSpace(emailAddress) 77 | 78 | fmt.Print("Authentication value (API key or API token): ") 79 | byteAuthValue, err := terminal.ReadPassword(0) 80 | if err != nil { 81 | log.Fatal("unable to read authentication value: ", err) 82 | } 83 | authValue := string(byteAuthValue) 84 | fmt.Println() 85 | 86 | authType, err := determineAuthType(strings.TrimSpace(authValue)) 87 | if err != nil { 88 | log.Fatal("failed to detect authentication type: ", err) 89 | } 90 | 91 | home, err := homedir.Dir() 92 | if err != nil { 93 | log.Fatal("unable to find home directory: ", err) 94 | } 95 | 96 | os.MkdirAll(home+defaultConfigDirectory, 0700) 97 | if _, err := os.Stat(home + defaultFullConfigPath); os.IsNotExist(err) { 98 | file, err := os.Create(home + defaultFullConfigPath) 99 | if err != nil { 100 | log.Fatal(err) 101 | } 102 | defer file.Close() 103 | } 104 | 105 | existingConfigFileContents, err := ioutil.ReadFile(home + defaultFullConfigPath) 106 | if err != nil { 107 | log.Fatal(err) 108 | } 109 | 110 | tomlConfigStruct := tomlConfig{} 111 | toml.Unmarshal(existingConfigFileContents, &tomlConfigStruct) 112 | 113 | // If this is the first profile, initialise the map. 114 | if len(tomlConfigStruct.Profiles) == 0 { 115 | tomlConfigStruct.Profiles = make(map[string]profile) 116 | } 117 | 118 | newProfile := profile{ 119 | Email: emailAddress, 120 | AuthType: authType, 121 | } 122 | 123 | if sessionDuration != "" { 124 | newProfile.SessionDuration = sessionDuration 125 | } else { 126 | log.Debug("session-duration was not set, not using short lived tokens") 127 | } 128 | 129 | var api *cloudflare.API 130 | if authType == "api_token" { 131 | api, err = cloudflare.NewWithAPIToken(authValue) 132 | if err != nil { 133 | log.Fatal(err) 134 | } 135 | } else { 136 | api, err = cloudflare.New(authValue, emailAddress) 137 | if err != nil { 138 | log.Fatal(err) 139 | } 140 | } 141 | 142 | if profileTemplate != "" { 143 | // The policies require that one of the resources is the current user. 144 | // This leads to a potential chicken/egg scenario where the user doesn't 145 | // valid credentials but needs them to generate the resources. We 146 | // intentionally spit out `Debug` and `Fatal` messages here to show the 147 | // original error *and* the friendly version of how to resolve it. 148 | userDetails, err := api.UserDetails(context.Background()) 149 | if err != nil { 150 | log.Debug(err) 151 | log.Fatal("failed to fetch user ID from the Cloudflare API which is required to generate the predefined short lived token policies. If you are using API tokens, please allow the permission to access your user details and try again.") 152 | } 153 | 154 | generatedPolicy, err := generatePolicy(profileTemplate, userDetails.ID) 155 | if err != nil { 156 | log.Fatal(err) 157 | } 158 | newProfile.Policies = generatedPolicy 159 | } 160 | 161 | log.Debugf("new profile: %+v", newProfile) 162 | tomlConfigStruct.Profiles[profileName] = newProfile 163 | 164 | configFile, err := os.OpenFile(home+defaultFullConfigPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0700) 165 | if err != nil { 166 | log.Fatal("failed to open file at ", home+defaultFullConfigPath) 167 | } 168 | defer configFile.Close() 169 | if err := toml.NewEncoder(configFile).Encode(tomlConfigStruct); err != nil { 170 | log.Fatal(err) 171 | } 172 | 173 | ring, err := keyring.Open(keyringDefaults) 174 | if err != nil { 175 | log.Fatalf("failed to open keyring backend: %s", strings.ToLower(err.Error())) 176 | } 177 | 178 | resp := ring.Set(keyring.Item{ 179 | Key: fmt.Sprintf("%s-%s", profileName, authType), 180 | Data: []byte(authValue), 181 | }) 182 | 183 | if resp == nil { 184 | fmt.Println("\nSuccess! Credentials have been set and are now ready for use!") 185 | } else { 186 | // error of some sort 187 | log.Fatal("Error adding credentials to keyring: ", resp) 188 | } 189 | }, 190 | } 191 | 192 | func determineAuthType(s string) (string, error) { 193 | if apiTokenMatch, _ := regexp.MatchString("[A-Za-z0-9-_]{40}", s); apiTokenMatch { 194 | log.Debug("API token detected") 195 | return "api_token", nil 196 | } else if apiKeyMatch, _ := regexp.MatchString("[0-9a-f]{37}", s); apiKeyMatch { 197 | log.Debug("API key detected") 198 | return "api_key", nil 199 | } else { 200 | return "", errors.New("invalid API token or API key format") 201 | } 202 | } 203 | 204 | func generatePolicy(policyType, userID string) ([]policy, error) { 205 | readOnlyPolicy := []policy{ 206 | { 207 | Effect: "allow", 208 | Resources: map[string]interface{}{"com.cloudflare.api.account.*": "*"}, 209 | PermissionGroups: []permissionGroup{ 210 | {ID: "02b71f12bb0748e9af8126494e181342"}, // Magic Firewall Read 211 | {ID: "050531528b044d58bbb71666fef7c07c"}, // Page Shield Read 212 | {ID: "05880cd1bdc24d8bae0be2136972816b"}, // Workers Tail Read 213 | {ID: "07bea2220b2343fa9fae15656c0d8e88"}, // Bot Management Read 214 | {ID: "08e61dabe81a422dab0dea6fdef1a98a"}, // Access: Custom Page Read 215 | {ID: "0cf6473ad41449e7b7b743d14fc20c60"}, // Images Read 216 | {ID: "0f4841f80adb4bada5a09493300e7f8d"}, // Access: Device Posture Read 217 | {ID: "1047880d37b649b49db4a504a245896f"}, // Email Security DMARC Reports Read 218 | {ID: "192192df92ee43ac90f2aeeffce67e35"}, // D1 Read 219 | {ID: "1a71c399035b4950a1bd1466bbe4f420"}, // Workers Scripts Read 220 | {ID: "1b1ea24cf0904d33903f0cc7e54e280f"}, // Zone Versioning Read 221 | {ID: "1b600d9d8062443e986a973f097e728a"}, // Email Routing Rules Read 222 | {ID: "2072033d694d415a936eaeb94e6405b8"}, // Workers Routes Read 223 | {ID: "20e5ea084b2f491c86b8d8d90abff905"}, // Config Settings Read 224 | {ID: "211a4c0feb3e43b3a2d41f1443a433e7"}, // Zone Transform Rules Read 225 | {ID: "212c9ff247b9406d990c017482afb3a5"}, // IOT Read 226 | {ID: "26bc23f853634eb4bff59983b9064fde"}, // Access: Organizations, Identity Providers, and Groups Read 227 | {ID: "27beb7f8333b41e2b946f0e23cd8091e"}, // IP Prefixes: Read 228 | {ID: "29eefa0805f94fdfae2b058b5b52f319"}, // Disable ESC Read 229 | {ID: "319f5059d33a410da0fac4d35a716157"}, // Managed headers Read 230 | {ID: "3245da1cf36c45c3847bb9b483c62f97"}, // Cache Settings Read 231 | {ID: "3a46c728a0a040d5a65cd8e2f3bc6935"}, // Magic Firewall Packet Captures - Read PCAPs API 232 | {ID: "3b376e0aa52c41cbb6afc9cab945afa8"}, // Cloudflare DEX Read 233 | {ID: "3d85e9514f944bb4912c5871d92e5af5"}, // Magic Network Monitoring Config Read 234 | {ID: "3f376c8e6f764a938b848bd01c8995c4"}, // Teams Read 235 | {ID: "429a068902904c5a9ed9fc267c67da9a"}, // Mass URL Redirects Read 236 | {ID: "4657621393f94f83b8ef94adba382e48"}, // L4 DDoS Managed Ruleset Read 237 | {ID: "4ec32dfcb35641c5bb32d5ef1ab963b4"}, // Firewall Services Read 238 | {ID: "4f1071168de8466e9808de86febfc516"}, // Account Rule Lists Read 239 | {ID: "4f3196a5c95747b6ad82e34e1d0a694f"}, // Access: Certificates Read 240 | {ID: "517b21aee92c4d89936c976ba6e4be55"}, // Zone Settings Read 241 | {ID: "51be404b56244056868226263a44a632"}, // Bot Management Feedback Report Read 242 | {ID: "5272e56105d04b5897466995b9bd4643"}, // Email Routing Addresses Read 243 | {ID: "56b2af4817c84ad99187911dc3986c23"}, // Account WAF Read 244 | {ID: "58abbad6d2ce40abb2594fbe932a2e0e"}, // Rule Policies Read 245 | {ID: "595409c54a24444b80a495620b2d614c"}, // Select Configuration Read 246 | {ID: "5bdbde7e76144204a244274eac3eb0eb"}, // Zaraz Read 247 | {ID: "5d613a610b294788a29572aaac2f254d"}, // URL Scanner Read 248 | {ID: "5d78fd7895974fd0bdbbbb079482721b"}, // Turnstile Sites Read 249 | {ID: "5f48a472240a4b489a21d43bd19a06e1"}, // DNS Firewall Read 250 | {ID: "6a315a56f18441e59ed03352369ae956"}, // Logs Read 251 | {ID: "6b60a5a87cae475da7e76e77e4209dd5"}, // HTTP Applications Read 252 | {ID: "6ced5d0d69b1422396909a62c38ab41b"}, // API Gateway Read 253 | {ID: "74c654eb4aac40e28d6c6caa4c5aeb3d"}, // Snippets Read 254 | {ID: "7b32a91ece3140d4b3c2c56f23fc8e35"}, // Origin Read 255 | {ID: "7b7216b327b04b8fbc8f524e1f9b7531"}, // SSL and Certificates Read 256 | {ID: "7cf72faf220841aabcfdfab81c43c4f6"}, // Billing Read 257 | {ID: "7ea222f6d5064cfa89ea366d7c1fee89"}, // Access: Apps and Policies Read 258 | {ID: "82e64a83756745bbbb1c9c2701bf816b"}, // DNS Read 259 | {ID: "853643ed57244ed1a05a7c024af9ab5a"}, // Sanitize Read 260 | {ID: "8b47d2786a534c08a1f94ee8f9f599ef"}, // Workers KV Storage Read 261 | {ID: "8e31f574901c42e8ad89140b28d42112"}, // Web3 Hostnames Read 262 | {ID: "91f7ce32fa614d73b7e1fc8f0e78582b"}, // Access: Service Tokens Read 263 | {ID: "945315185a8f40518bf3e9e6d0bee126"}, // Domain Page Shield Read 264 | {ID: "967ecf860a244dd1911a0331a0af582a"}, // Magic Transit Prefix Read 265 | {ID: "99ff99e4e30247a99d3777a8c4c18541"}, // Access: SSH Auditing CA Read 266 | {ID: "9ade9cfc8f8949bcb2371be2f0ec8db1"}, // China Network Steering Read 267 | {ID: "9c88f9c5bce24ce7af9a958ba9c504db"}, // Analytics Read 268 | {ID: "9d24387c6e8544e2bc4024a03991339f"}, // Load Balancing: Monitors and Pools Read 269 | {ID: "a2431ca73b7d41f99c53303027392586"}, // Custom Pages Read 270 | {ID: "a2b55cd504d44ef18b7ba6a7f2b8fbb1"}, // Custom Errors Read 271 | {ID: "a7a233f9604845c787d4c8c39ac09c21"}, // Account: SSL and Certificates Read 272 | {ID: "a9a99455bf3245f6a5a244f909d74830"}, // Transform Rules Read 273 | {ID: "af1c363c35ba45b9a8c682ae50eb3f99"}, // DDoS Protection Read 274 | {ID: "b05b28e839c54467a7d6cba5d3abb5a3"}, // Access: Audit Logs Read 275 | {ID: "b415b70a4fd1412886f164451f20405c"}, // Page Rules Read 276 | {ID: "b4992e1108244f5d8bfbd5744320c2e1"}, // Workers R2 Storage Read 277 | {ID: "b89a480218d04ceb98b4fe57ca29dc1f"}, // Account Analytics Read 278 | {ID: "c1fde68c7bcc44588cbb6ddbc16d6480"}, // Account Settings Read 279 | {ID: "c49f8d15f9f44885a544d945ef5aa6ae"}, // HTTP DDoS Managed Ruleset Read 280 | {ID: "c4a30cd58c5d42619c86a3c36c441e2d"}, // Logs Read 281 | {ID: "c57ea647ef654b47bc8944fa739b570d"}, // Account Custom Pages Read 282 | {ID: "c8fed203ed3043cba015a93ad1616f1f"}, // Zone Read 283 | {ID: "cab5202d07ef47beae788e6bc95cb6fe"}, // Waiting Rooms Read 284 | {ID: "d8e12db741544d1586ec1d6f5d3c7786"}, // Dynamic URL Redirects Read 285 | {ID: "dbc512b354774852af2b5a5f4ba3d470"}, // Zone WAF Read 286 | {ID: "de21485a24744b76a004aa153898f7fe"}, // Stream Read 287 | {ID: "de7a688cc47d43bd9ea700b467a09c96"}, // Account Firewall Access Rules Read 288 | {ID: "df1577df30ee46268f9470952d7b0cdf"}, // Intel Read 289 | {ID: "e199d584e69344eba202452019deafe3"}, // Disable ESC Read 290 | {ID: "e247aedd66bd41cc9193af0213416666"}, // Pages Read 291 | {ID: "e763fae6ee95443b8f56f19213c5f2a5"}, // IP Prefixes: BGP On Demand Read 292 | {ID: "e9a975f628014f1d85b723993116f7d5"}, // Load Balancers Read 293 | {ID: "eb258a38ea634c86a0c89da6b27cb6b6"}, // Access: Apps and Policies Read 294 | {ID: "eb56a6953c034b9d97dd838155666f06"}, // Account API Tokens Read 295 | {ID: "eeffa4d16812430cb4a0ae9e7f46fc24"}, // Constellation Read 296 | {ID: "efea2ab8357b47888938f101ae5e053f"}, // Argo Tunnel Read 297 | {ID: "f3604047d46144d2a3e9cf4ac99d7f16"}, // Allow Request Tracer Read 298 | {ID: "fac65912d42144aa86b7dd33281bf79e"}, // Health Checks Read 299 | {ID: "fb39996ee9044d2a8725921e02744b39"}, // Account Rulesets Read 300 | {ID: "fd7f886c75a244389e892c4c3c068292"}, // Pubsub Configuration Read 301 | }, 302 | }, 303 | { 304 | Effect: "allow", 305 | Resources: map[string]interface{}{"com.cloudflare.api.account.zone.*": "*"}, 306 | PermissionGroups: []permissionGroup{ 307 | {ID: "07bea2220b2343fa9fae15656c0d8e88"}, // Bot Management Read 308 | {ID: "1047880d37b649b49db4a504a245896f"}, // Email Security DMARC Reports Read 309 | {ID: "1b1ea24cf0904d33903f0cc7e54e280f"}, // Zone Versioning Read 310 | {ID: "1b600d9d8062443e986a973f097e728a"}, // Email Routing Rules Read 311 | {ID: "2072033d694d415a936eaeb94e6405b8"}, // Workers Routes Read 312 | {ID: "20e5ea084b2f491c86b8d8d90abff905"}, // Config Settings Read 313 | {ID: "211a4c0feb3e43b3a2d41f1443a433e7"}, // Zone Transform Rules Read 314 | {ID: "319f5059d33a410da0fac4d35a716157"}, // Managed headers Read 315 | {ID: "3245da1cf36c45c3847bb9b483c62f97"}, // Cache Settings Read 316 | {ID: "4ec32dfcb35641c5bb32d5ef1ab963b4"}, // Firewall Services Read 317 | {ID: "517b21aee92c4d89936c976ba6e4be55"}, // Zone Settings Read 318 | {ID: "51be404b56244056868226263a44a632"}, // Bot Management Feedback Report Read 319 | {ID: "5bdbde7e76144204a244274eac3eb0eb"}, // Zaraz Read 320 | {ID: "6ced5d0d69b1422396909a62c38ab41b"}, // API Gateway Read 321 | {ID: "74c654eb4aac40e28d6c6caa4c5aeb3d"}, // Snippets Read 322 | {ID: "7b32a91ece3140d4b3c2c56f23fc8e35"}, // Origin Read 323 | {ID: "7b7216b327b04b8fbc8f524e1f9b7531"}, // SSL and Certificates Read 324 | {ID: "82e64a83756745bbbb1c9c2701bf816b"}, // DNS Read 325 | {ID: "853643ed57244ed1a05a7c024af9ab5a"}, // Sanitize Read 326 | {ID: "8e31f574901c42e8ad89140b28d42112"}, // Web3 Hostnames Read 327 | {ID: "945315185a8f40518bf3e9e6d0bee126"}, // Domain Page Shield Read 328 | {ID: "9c88f9c5bce24ce7af9a958ba9c504db"}, // Analytics Read 329 | {ID: "a2431ca73b7d41f99c53303027392586"}, // Custom Pages Read 330 | {ID: "a2b55cd504d44ef18b7ba6a7f2b8fbb1"}, // Custom Errors Read 331 | {ID: "b415b70a4fd1412886f164451f20405c"}, // Page Rules Read 332 | {ID: "c49f8d15f9f44885a544d945ef5aa6ae"}, // HTTP DDoS Managed Ruleset Read 333 | {ID: "c4a30cd58c5d42619c86a3c36c441e2d"}, // Logs Read 334 | {ID: "c8fed203ed3043cba015a93ad1616f1f"}, // Zone Read 335 | {ID: "cab5202d07ef47beae788e6bc95cb6fe"}, // Waiting Rooms Read 336 | {ID: "d8e12db741544d1586ec1d6f5d3c7786"}, // Dynamic URL Redirects Read 337 | {ID: "dbc512b354774852af2b5a5f4ba3d470"}, // Zone WAF Read 338 | {ID: "e199d584e69344eba202452019deafe3"}, // Disable ESC Read 339 | {ID: "e9a975f628014f1d85b723993116f7d5"}, // Load Balancers Read 340 | {ID: "eb258a38ea634c86a0c89da6b27cb6b6"}, // Access: Apps and Policies Read 341 | {ID: "fac65912d42144aa86b7dd33281bf79e"}, // Health Checks Read 342 | }, 343 | }, 344 | { 345 | Effect: "allow", 346 | Resources: map[string]interface{}{"com.cloudflare.api.user." + userID: "*"}, 347 | PermissionGroups: []permissionGroup{ 348 | {ID: "0cc3a61731504c89b99ec1be78b77aa0"}, // API Tokens Read 349 | {ID: "3518d0f75557482e952c6762d3e64903"}, // Memberships Read 350 | {ID: "8acbe5bb0d54464ab867149d7f7cf8ac"}, // User Details Read 351 | }, 352 | }, 353 | } 354 | 355 | writeEverythingPolicy := []policy{ 356 | { 357 | Effect: "allow", 358 | Resources: map[string]interface{}{"com.cloudflare.api.account.*": "*"}, 359 | PermissionGroups: []permissionGroup{ 360 | {ID: "06f0526e6e464647bd61b63c54935235"}, // Config Settings Write 361 | {ID: "094547ab6e77498c8c4dfa87fadd5c51"}, // Apps Write 362 | {ID: "09b2857d1c31407795e75e3fed8617a1"}, // D1 Write 363 | {ID: "09c77baecb6341a2b1ca2c62b658d290"}, // Magic Network Monitoring Config Write 364 | {ID: "0ac90a90249747bca6b047d97f0803e9"}, // Zone Transform Rules Write 365 | {ID: "0bc09a3cd4b54605990df4e307f138e1"}, // Magic Transit Prefix Write 366 | {ID: "0fd9d56bc2da43ad8ea22d610dd8cab1"}, // Managed headers Write 367 | {ID: "18555e39c5ba40d284dde87eda845a90"}, // Disable ESC Write 368 | {ID: "1af1fa2adc104452b74a9a3364202f20"}, // Account Settings Write 369 | {ID: "1e13c5124ca64b72b1969a67e8829049"}, // Access: Apps and Policies Write 370 | {ID: "2002629aaff0454085bf5a201ed70a72"}, // Bot Management Feedback Report Write 371 | {ID: "235eac9bb64942b49cb805cc851cb000"}, // Select Configuration Write 372 | {ID: "24fc124dc8254e0db468e60bf410c800"}, // Waiting Rooms Write 373 | {ID: "28f4b596e7d643029c524985477ae49a"}, // Workers Routes Write 374 | {ID: "29d3afbfd4054af9accdd1118815ed05"}, // Access: Certificates Write 375 | {ID: "2a400bcb29154daab509fe07e3facab0"}, // URL Scanner Write 376 | {ID: "2ae23e4939d54074b7d252d27ce75a77"}, // IP Prefixes: BGP On Demand Write 377 | {ID: "2edbf20661fd4661b0fe10e9e12f485c"}, // Account Rule Lists Write 378 | {ID: "2eee71c9364c4cacaf469e8370f09056"}, // Email Security DMARC Reports Write 379 | {ID: "2fc1072ee6b743828db668fcb3f9dee7"}, // Access: Device Posture Write 380 | {ID: "3030687196b94b638145a3953da2b699"}, // Zone Settings Write 381 | {ID: "3a1e1ef09dd34271bb44fc4c6a419952"}, // Cloudflare DEX 382 | {ID: "3b94c49258ec4573b06d51d99b6416c0"}, // Bot Management Write 383 | {ID: "3e0b5820118e47f3922f7c989e673882"}, // Logs Write 384 | {ID: "43137f8d07884d3198dc0ee77ca6e79b"}, // Firewall Services Write 385 | {ID: "440e6958bcc947329f8d56328d7322ce"}, // Page Shield 386 | {ID: "4736c02a9f224c8196ae5b127beae78c"}, // HTTP Applications Write 387 | {ID: "4755a26eedb94da69e1066d98aa820be"}, // DNS Write 388 | {ID: "4e5fd8ac327b4a358e48c66fcbeb856d"}, // Access: Custom Page Write 389 | {ID: "4ea7d6421801452dbf07cef853a5ef39"}, // Magic Firewall Packet Captures - Write PCAPs API 390 | {ID: "56907406c3d548ed902070ec4df0e328"}, // Account Rulesets Write 391 | {ID: "5bc3f8b21c554832afc660159ab75fa4"}, // Account API Tokens Write 392 | {ID: "5ea6da42edb34811a78d1b007557c0ca"}, // Web3 Hostnames Write 393 | {ID: "6134079371904d8ebd77931c8ca07e50"}, // Domain Page Shield 394 | {ID: "618ec6c64a3a42f8b08bdcb147ded4e4"}, // Images Write 395 | {ID: "61ddc58f1da14f95b33b41213360cbeb"}, // Rule Policies Write 396 | {ID: "6c80e02421494afc9ae14414ed442632"}, // Billing Write 397 | {ID: "6c9d1cfcfc6840a987d1b5bfb880a841"}, // Access: Apps and Policies Revoke 398 | {ID: "6d7f2f5f5b1d4a0e9081fdc98d432fd1"}, // Load Balancers Write 399 | {ID: "6db4e222e21248ac96a3f4c2a81e3b41"}, // Access: Apps and Policies Revoke 400 | {ID: "7121a0c7e9ed46e3829f9cca2bb572aa"}, // Access: Organizations, Identity Providers, and Groups Revoke 401 | {ID: "714f9c13a5684c2885a793f5edb36f59"}, // Stream Write 402 | {ID: "74e1036f577a48528b78d2413b40538d"}, // Dynamic URL Redirects Write 403 | {ID: "755c05aa014b4f9ab263aa80b8167bd8"}, // Turnstile Sites Write 404 | {ID: "79b3ec0d10ce4148a8f8bdc0cc5f97f2"}, // Email Routing Rules Write 405 | {ID: "7a4c3574054a4d0ba7c692893ba8bdd4"}, // L4 DDoS Managed Ruleset Write 406 | {ID: "7c81856725af47ce89a790d5fb36f362"}, // Constellation Write 407 | {ID: "865ebd55bc6d4b109de6813eccfefd13"}, // IOT Write 408 | {ID: "87065285ab38463481e72815eefd18c3"}, // Page Shield Write 409 | {ID: "89bb8c37d46042e98b84560eaaa6379f"}, // Sanitize Write 410 | {ID: "89d5bf002389496e9994b8c30608b5d0"}, // Zaraz Edit 411 | {ID: "8a9d35a7c8504208ad5c3e8d58e6162d"}, // Account Custom Pages Write 412 | {ID: "8bd1dac84d3d43e7bfb43145f010a15c"}, // Magic Firewall Write 413 | {ID: "8d28297797f24fb8a0c332fe0866ec89"}, // Pages Write 414 | {ID: "8e6ed1ef6e864ad0ae477ceffa5aa5eb"}, // Magic Network Monitoring Admin 415 | {ID: "910b6ecca1c5411bb894e787362d1312"}, // Pubsub Configuration Write 416 | {ID: "9110d9dd749e464fb9f3961a2064efc5"}, // Disable ESC Write 417 | {ID: "92209474242d459690e2cdb1985eaa6c"}, // Intel Write 418 | {ID: "92b8234e99f64e05bbbc59e1dc0f76b6"}, // IP Prefixes: Write 419 | {ID: "92c8dcd551cc42a6a57a54e8f8d3f3e3"}, // Cloudflare DEX Write 420 | {ID: "959972745952452f8be2452be8cbb9f2"}, // Access: Apps and Policies Write 421 | {ID: "96163bd1b0784f62b3e44ed8c2ab1eb6"}, // Logs Write 422 | {ID: "9ff81cbbe65c400b97d92c3c1033cab6"}, // Cache Settings Write 423 | {ID: "a1a6298e52584c8fb6313760a30c681e"}, // Zero Trust: Seats Write 424 | {ID: "a1c0fec57cf94af79479a6d827fa518c"}, // Access: Service Tokens Write 425 | {ID: "a416acf9ef5a4af19fb11ed3b96b1fe6"}, // Account Firewall Access Rules Write 426 | {ID: "a4308c6855c84eb2873e01b6cc85cbb3"}, // Origin Write 427 | {ID: "a9dba34cf5814d4ab2007b4ada0045bd"}, // Custom Errors Write 428 | {ID: "abe78e2276664f4db588c1f675a77486"}, // Mass URL Redirects Write 429 | {ID: "ae16e88bc7814753a1894c7ce187ab72"}, // Transform Rules Write 430 | {ID: "b33f02c6f7284e05a6f20741c0bb0567"}, // Teams Write 431 | {ID: "b88a3aa889474524bccea5cf18f122bf"}, // HTTP DDoS Managed Ruleset Write 432 | {ID: "bf7481a1826f439697cb59a20b22293e"}, // Workers R2 Storage Write 433 | {ID: "bfe0d8686a584fa680f4c53b5eb0de6d"}, // Access: Organizations, Identity Providers, and Groups Write 434 | {ID: "c03055bc037c4ea9afb9a9f104b7b721"}, // SSL and Certificates Write 435 | {ID: "c07321b023e944ff818fec44d8203567"}, // Argo Tunnel Write 436 | {ID: "c244ec076974430a88bda1cdd992d0d9"}, // Custom Pages Write 437 | {ID: "c6f6338ceae545d0b90daaa1fed855e6"}, // China Network Steering Write 438 | {ID: "c9915d86fbff46af9dd945c0a882294b"}, // Zone Versioning Write 439 | {ID: "cde8c82463b6414ca06e46b9633f52a6"}, // Account WAF Write 440 | {ID: "cdeb15b336e640a2965df8c65052f1e0"}, // Zaraz Admin 441 | {ID: "d2a1802cc9a34e30852f8b33869b2f3c"}, // Load Balancing: Monitors and Pools Write 442 | {ID: "d30c9ad8b5224e7cb8d41bcb4757effc"}, // Access: SSH Auditing CA Write 443 | {ID: "d44ed14bcc4340b194d3824d60edad3f"}, // DDoS Protection Write 444 | {ID: "da6d2d6f2ec8442eaadda60d13f42bca"}, // DNS Firewall Write 445 | {ID: "dadeaf3abdf14126a77a35e0c92fc36e"}, // Snippets Write 446 | {ID: "db37e5f1cb1a4e1aabaef8deaea43575"}, // Account: SSL and Certificates Write 447 | {ID: "e086da7e2179491d91ee5f35b3ca210a"}, // Workers Scripts Write 448 | {ID: "e0dc25a0fbdf4286b1ea100e3256b0e3"}, // Health Checks Write 449 | {ID: "e17beae8b8cb423a99b1730f21238bed"}, // Cache Purge 450 | {ID: "e4589eb09e63436686cd64252a3aebeb"}, // Email Routing Addresses Write 451 | {ID: "e6d2666161e84845a636613608cee8d5"}, // Zone Write 452 | {ID: "ed07f6c337da4195b4e72a1fb2c6bcae"}, // Page Rules Write 453 | {ID: "efb81b5cd37d49f3be1da9363a6d7a19"}, // Teams Report 454 | {ID: "f0235726de25444a84f704b7c93afadf"}, // API Gateway Write 455 | {ID: "f7f0eda5697f475c90846e879bab8666"}, // Workers KV Storage Write 456 | {ID: "fb6778dc191143babbfaa57993f1d275"}, // Zone WAF Write 457 | }, 458 | }, 459 | { 460 | Effect: "allow", 461 | Resources: map[string]interface{}{"com.cloudflare.api.account.zone.*": "*"}, 462 | PermissionGroups: []permissionGroup{ 463 | {ID: "06f0526e6e464647bd61b63c54935235"}, // Config Settings Write 464 | {ID: "094547ab6e77498c8c4dfa87fadd5c51"}, // Apps Write 465 | {ID: "0ac90a90249747bca6b047d97f0803e9"}, // Zone Transform Rules Write 466 | {ID: "0fd9d56bc2da43ad8ea22d610dd8cab1"}, // Managed headers Write 467 | {ID: "2002629aaff0454085bf5a201ed70a72"}, // Bot Management Feedback Report Write 468 | {ID: "24fc124dc8254e0db468e60bf410c800"}, // Waiting Rooms Write 469 | {ID: "28f4b596e7d643029c524985477ae49a"}, // Workers Routes Write 470 | {ID: "2eee71c9364c4cacaf469e8370f09056"}, // Email Security DMARC Reports Write 471 | {ID: "3030687196b94b638145a3953da2b699"}, // Zone Settings Write 472 | {ID: "3b94c49258ec4573b06d51d99b6416c0"}, // Bot Management Write 473 | {ID: "3e0b5820118e47f3922f7c989e673882"}, // Logs Write 474 | {ID: "43137f8d07884d3198dc0ee77ca6e79b"}, // Firewall Services Write 475 | {ID: "4755a26eedb94da69e1066d98aa820be"}, // DNS Write 476 | {ID: "5ea6da42edb34811a78d1b007557c0ca"}, // Web3 Hostnames Write 477 | {ID: "6134079371904d8ebd77931c8ca07e50"}, // Domain Page Shield 478 | {ID: "6d7f2f5f5b1d4a0e9081fdc98d432fd1"}, // Load Balancers Write 479 | {ID: "6db4e222e21248ac96a3f4c2a81e3b41"}, // Access: Apps and Policies Revoke 480 | {ID: "74e1036f577a48528b78d2413b40538d"}, // Dynamic URL Redirects Write 481 | {ID: "79b3ec0d10ce4148a8f8bdc0cc5f97f2"}, // Email Routing Rules Write 482 | {ID: "87065285ab38463481e72815eefd18c3"}, // Page Shield Write 483 | {ID: "89bb8c37d46042e98b84560eaaa6379f"}, // Sanitize Write 484 | {ID: "89d5bf002389496e9994b8c30608b5d0"}, // Zaraz Edit 485 | {ID: "9110d9dd749e464fb9f3961a2064efc5"}, // Disable ESC Write 486 | {ID: "959972745952452f8be2452be8cbb9f2"}, // Access: Apps and Policies Write 487 | {ID: "9ff81cbbe65c400b97d92c3c1033cab6"}, // Cache Settings Write 488 | {ID: "a4308c6855c84eb2873e01b6cc85cbb3"}, // Origin Write 489 | {ID: "a9dba34cf5814d4ab2007b4ada0045bd"}, // Custom Errors Write 490 | {ID: "b88a3aa889474524bccea5cf18f122bf"}, // HTTP DDoS Managed Ruleset Write 491 | {ID: "c03055bc037c4ea9afb9a9f104b7b721"}, // SSL and Certificates Write 492 | {ID: "c244ec076974430a88bda1cdd992d0d9"}, // Custom Pages Write 493 | {ID: "c9915d86fbff46af9dd945c0a882294b"}, // Zone Versioning Write 494 | {ID: "cdeb15b336e640a2965df8c65052f1e0"}, // Zaraz Admin 495 | {ID: "dadeaf3abdf14126a77a35e0c92fc36e"}, // Snippets Write 496 | {ID: "e0dc25a0fbdf4286b1ea100e3256b0e3"}, // Health Checks Write 497 | {ID: "e17beae8b8cb423a99b1730f21238bed"}, // Cache Purge 498 | {ID: "e6d2666161e84845a636613608cee8d5"}, // Zone Write 499 | {ID: "ed07f6c337da4195b4e72a1fb2c6bcae"}, // Page Rules Write 500 | {ID: "f0235726de25444a84f704b7c93afadf"}, // API Gateway Write 501 | {ID: "fb6778dc191143babbfaa57993f1d275"}, // Zone WAF Write 502 | }, 503 | }, 504 | { 505 | Effect: "allow", 506 | Resources: map[string]interface{}{"com.cloudflare.api.user." + userID: "*"}, 507 | PermissionGroups: []permissionGroup{ 508 | {ID: "55a5e17cc99e4a3fa1f3432d262f2e55"}, // User Details Write 509 | {ID: "9201bc6f42d440968aaab0c6f17ebb1d"}, // Memberships Write 510 | }, 511 | }, 512 | } 513 | 514 | switch policyType { 515 | case "write-everything": 516 | log.Debug("configuring a write-everything template") 517 | return writeEverythingPolicy, nil 518 | case "read-only": 519 | log.Debug("configuring a read-only template") 520 | return readOnlyPolicy, nil 521 | } 522 | 523 | return nil, fmt.Errorf("unable to generate policy for %q, valid policy names: [read-only, write-everything]", policyType) 524 | } 525 | -------------------------------------------------------------------------------- /cmd/env.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import "strings" 4 | 5 | // environ is a slice of strings representing the environment, in the form 6 | // "key=value". 7 | type environ []string 8 | 9 | // Unset an environment variable by key 10 | func (e *environ) Unset(key string) { 11 | for i := range *e { 12 | if strings.HasPrefix((*e)[i], key+"=") { 13 | (*e)[i] = (*e)[len(*e)-1] 14 | *e = (*e)[:len(*e)-1] 15 | break 16 | } 17 | } 18 | } 19 | 20 | // Set adds an environment variable, replacing any existing ones of the same key 21 | func (e *environ) Set(key, val string) { 22 | e.Unset(key) 23 | *e = append(*e, key+"="+val) 24 | } 25 | -------------------------------------------------------------------------------- /cmd/exec.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "strconv" 10 | "strings" 11 | "syscall" 12 | "time" 13 | 14 | "os/exec" 15 | 16 | "github.com/99designs/keyring" 17 | "github.com/cloudflare/cloudflare-go" 18 | "github.com/mitchellh/go-homedir" 19 | "github.com/pelletier/go-toml" 20 | log "github.com/sirupsen/logrus" 21 | "github.com/spf13/cobra" 22 | ) 23 | 24 | var execCmd = &cobra.Command{ 25 | Use: "exec [profile]", 26 | Short: "Execute a command with Cloudflare credentials populated", 27 | Long: "", 28 | Example: ` 29 | Execute a single command with credentials populated 30 | 31 | $ cf-vault exec example-profile -- env | grep -i cloudflare 32 | CLOUDFLARE_VAULT_SESSION=example-profile 33 | CLOUDFLARE_EMAIL=jacob@example.com 34 | CLOUDFLARE_API_KEY=s3cr3t 35 | CF_EMAIL=jacob@example.com 36 | CF_API_KEY=s3cr3t 37 | 38 | Spawn a new shell with credentials populated 39 | 40 | $ cf-vault exec example-profile -- 41 | $ env | grep -i cloudflare 42 | CLOUDFLARE_VAULT_SESSION=example-profile 43 | CLOUDFLARE_EMAIL=jacob@example.com 44 | CLOUDFLARE_API_KEY=s3cr3t 45 | CF_EMAIL=jacob@example.com 46 | CF_API_KEY=s3cr3t 47 | `, 48 | Args: func(cmd *cobra.Command, args []string) error { 49 | if len(args) < 1 { 50 | return errors.New("requires a profile argument") 51 | } 52 | return nil 53 | }, 54 | PreRun: func(cmd *cobra.Command, args []string) { 55 | if verbose { 56 | log.SetLevel(log.DebugLevel) 57 | keyring.Debug = true 58 | } 59 | }, 60 | Run: func(cmd *cobra.Command, args []string) { 61 | env := environ(os.Environ()) 62 | 63 | profileName := args[0] 64 | // Remove the extra executable name at the beginning of the slice. 65 | copy(args[0:], args[0+1:]) 66 | args[len(args)-1] = "" 67 | args = args[:len(args)-1] 68 | 69 | // Don't allow nesting of cf-vault sessions, it gets messy. 70 | if os.Getenv("CLOUDFLARE_VAULT_SESSION") != "" { 71 | log.Fatal("cf-vault sessions shouldn't be nested, unset CLOUDFLARE_VAULT_SESSION to continue or open a new shell session") 72 | } 73 | 74 | log.Debug("using profile: ", profileName) 75 | 76 | home, err := homedir.Dir() 77 | if err != nil { 78 | log.Fatal("unable to find home directory: ", err) 79 | } 80 | 81 | configData, err := ioutil.ReadFile(home + defaultFullConfigPath) 82 | if err != nil { 83 | log.Fatal(err) 84 | } 85 | 86 | config := tomlConfig{} 87 | err = toml.Unmarshal(configData, &config) 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | 92 | if _, ok := config.Profiles[profileName]; !ok { 93 | log.Fatalf("no profile matching %q found in the configuration file at %s", profileName, home+defaultFullConfigPath) 94 | } 95 | 96 | profile := config.Profiles[profileName] 97 | 98 | ring, err := keyring.Open(keyringDefaults) 99 | if err != nil { 100 | log.Fatalf("failed to open keyring backend: %s", strings.ToLower(err.Error())) 101 | } 102 | 103 | keychain, err := ring.Get(fmt.Sprintf("%s-%s", profileName, profile.AuthType)) 104 | if err != nil { 105 | log.Fatalf("failed to get item from keyring: %s", strings.ToLower(err.Error())) 106 | } 107 | 108 | env.Set("CLOUDFLARE_VAULT_SESSION", profileName) 109 | 110 | // Not using short lived tokens so set the static API token or API key. 111 | if profile.SessionDuration == "" { 112 | if profile.AuthType == "api_key" { 113 | env.Set("CLOUDFLARE_EMAIL", profile.Email) 114 | env.Set("CF_EMAIL", profile.Email) 115 | } 116 | env.Set(fmt.Sprintf("CLOUDFLARE_%s", strings.ToUpper(profile.AuthType)), string(keychain.Data)) 117 | env.Set(fmt.Sprintf("CF_%s", strings.ToUpper(profile.AuthType)), string(keychain.Data)) 118 | } else { 119 | var api *cloudflare.API 120 | if profile.AuthType == "api_token" { 121 | api, err = cloudflare.NewWithAPIToken(string(keychain.Data)) 122 | if err != nil { 123 | log.Fatal(err) 124 | } 125 | } else { 126 | api, err = cloudflare.New(string(keychain.Data), profile.Email) 127 | if err != nil { 128 | log.Fatal(err) 129 | } 130 | } 131 | 132 | policies := []cloudflare.APITokenPolicies{} 133 | 134 | for _, policy := range profile.Policies { 135 | permissionGroups := []cloudflare.APITokenPermissionGroups{} 136 | for _, group := range policy.PermissionGroups { 137 | permissionGroups = append(permissionGroups, cloudflare.APITokenPermissionGroups{ 138 | ID: group.ID, 139 | Name: group.Name, 140 | }) 141 | } 142 | 143 | policies = append(policies, cloudflare.APITokenPolicies{ 144 | Effect: policy.Effect, 145 | Resources: policy.Resources, 146 | PermissionGroups: permissionGroups, 147 | }) 148 | } 149 | 150 | parsedSessionDuration, err := time.ParseDuration(profile.SessionDuration) 151 | if err != nil { 152 | log.Fatal(err) 153 | } 154 | now, _ := time.Parse(time.RFC3339, time.Now().UTC().Format(time.RFC3339)) 155 | tokenExpiry := now.Add(time.Second * time.Duration(parsedSessionDuration.Seconds())) 156 | 157 | token := cloudflare.APIToken{ 158 | Name: fmt.Sprintf("%s-%d", projectName, tokenExpiry.Unix()), 159 | NotBefore: &now, 160 | ExpiresOn: &tokenExpiry, 161 | Policies: policies, 162 | } 163 | 164 | shortLivedToken, err := api.CreateAPIToken(context.Background(), token) 165 | if err != nil { 166 | log.Fatalf("failed to create API token: %s", err) 167 | } 168 | 169 | if shortLivedToken.Value != "" { 170 | env.Set("CLOUDFLARE_API_TOKEN", shortLivedToken.Value) 171 | env.Set("CF_API_TOKEN", shortLivedToken.Value) 172 | } 173 | 174 | env.Set("CLOUDFLARE_SESSION_EXPIRY", strconv.Itoa(int(tokenExpiry.Unix()))) 175 | } 176 | 177 | // Should a command not be provided, drop into a fresh shell with the 178 | // credentials populated alongside the existing env. 179 | if len(args) == 0 { 180 | log.Debug("launching new shell with credentials populated") 181 | syscall.Exec(os.Getenv("SHELL"), []string{os.Getenv("SHELL")}, env) 182 | } 183 | 184 | executable := args[0] 185 | pathtoExec, err := exec.LookPath(executable) 186 | if err != nil { 187 | log.Fatalf("couldn't find the executable '%s': %s", pathtoExec, err.Error()) 188 | } 189 | 190 | log.Debugf("found executable %s", pathtoExec) 191 | log.Debugf("executing command: %s", strings.Join(args, " ")) 192 | 193 | syscall.Exec(pathtoExec, args, env) 194 | }, 195 | } 196 | -------------------------------------------------------------------------------- /cmd/list.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | 8 | "github.com/mitchellh/go-homedir" 9 | "github.com/olekukonko/tablewriter" 10 | "github.com/pelletier/go-toml" 11 | log "github.com/sirupsen/logrus" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | var listCmd = &cobra.Command{ 16 | Use: "list", 17 | Short: "List all available profiles", 18 | Long: "", 19 | PreRun: func(cmd *cobra.Command, args []string) { 20 | if verbose { 21 | log.SetLevel(log.DebugLevel) 22 | } 23 | }, 24 | Run: func(cmd *cobra.Command, args []string) { 25 | home, err := homedir.Dir() 26 | if err != nil { 27 | log.Fatal("unable to find home directory: ", err) 28 | } 29 | 30 | configData, err := ioutil.ReadFile(home + defaultFullConfigPath) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | config := tomlConfig{} 36 | err = toml.Unmarshal(configData, &config) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | 41 | if len(config.Profiles) == 0 { 42 | fmt.Printf("no profiles found at %s\n", home+defaultFullConfigPath) 43 | os.Exit(0) 44 | } 45 | 46 | tableData := [][]string{} 47 | for profileName, profile := range config.Profiles { 48 | // Only display the email if we're using API tokens otherwise the value is 49 | // not used and pretty superfluous. 50 | var emailString string 51 | if profile.AuthType == "api_key" { 52 | emailString = profile.Email 53 | } 54 | 55 | tableData = append(tableData, []string{ 56 | profileName, 57 | profile.AuthType, 58 | emailString, 59 | }) 60 | } 61 | 62 | table := tablewriter.NewWriter(os.Stdout) 63 | table.SetHeader([]string{"Profile name", "Authentication type", "Email"}) 64 | table.SetAutoWrapText(false) 65 | table.SetAutoFormatHeaders(true) 66 | table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 67 | table.SetAlignment(tablewriter.ALIGN_LEFT) 68 | table.SetCenterSeparator("") 69 | table.SetColumnSeparator("") 70 | table.SetRowSeparator("") 71 | table.SetHeaderLine(false) 72 | table.SetBorder(false) 73 | table.SetTablePadding("\t") 74 | table.SetNoWhiteSpace(true) 75 | table.AppendBulk(tableData) 76 | table.Render() 77 | }, 78 | } 79 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/99designs/keyring" 8 | log "github.com/sirupsen/logrus" 9 | "github.com/spf13/cobra" 10 | "golang.org/x/crypto/ssh/terminal" 11 | ) 12 | 13 | var ( 14 | verbose bool 15 | projectName = "cf-vault" 16 | projectNameWithoutHyphen = "cfvault" 17 | defaultConfigDirectory = "/." + projectName 18 | defaultFullConfigPath = defaultConfigDirectory + "/config.toml" 19 | ) 20 | 21 | var keyringDefaults = keyring.Config{ 22 | FileDir: fmt.Sprintf("~/.%s/keys/", projectName), 23 | FilePasswordFunc: fileKeyringPassphrasePrompt, 24 | ServiceName: projectName, 25 | KeychainName: projectName, 26 | LibSecretCollectionName: projectNameWithoutHyphen, 27 | KWalletAppID: projectName, 28 | KWalletFolder: projectName, 29 | KeychainTrustApplication: true, 30 | WinCredPrefix: projectName, 31 | } 32 | 33 | var rootCmd = &cobra.Command{ 34 | Use: projectName, 35 | Long: "Manage your Cloudflare credentials, securely", 36 | PreRun: func(cmd *cobra.Command, args []string) { 37 | if verbose { 38 | log.SetLevel(log.DebugLevel) 39 | } 40 | 41 | if len(args) == 0 { 42 | cmd.Help() 43 | os.Exit(0) 44 | } 45 | }, 46 | Run: func(cmd *cobra.Command, args []string) {}, 47 | } 48 | 49 | // Get passphrase prompt (copied from https://github.com/99designs/aws-vault) 50 | func fileKeyringPassphrasePrompt(prompt string) (string, error) { 51 | if password, ok := os.LookupEnv("CF_VAULT_FILE_PASSPHRASE"); ok { 52 | return password, nil 53 | } 54 | 55 | fmt.Fprintf(os.Stderr, "%s: ", prompt) 56 | b, err := terminal.ReadPassword(int(os.Stdin.Fd())) 57 | if err != nil { 58 | return "", err 59 | } 60 | fmt.Println() 61 | return string(b), nil 62 | } 63 | 64 | func init() { 65 | log.SetLevel(log.WarnLevel) 66 | 67 | rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "increase the verbosity of the output") 68 | 69 | var profileTemplate string 70 | var sessionDuration string 71 | addCmd.Flags().StringVarP(&profileTemplate, "profile-template", "", "", "create profile with a predefined permissions and resources template") 72 | addCmd.Flags().StringVarP(&sessionDuration, "session-duration", "", "", "TTL of short lived tokens requests") 73 | 74 | rootCmd.AddCommand(addCmd) 75 | rootCmd.AddCommand(listCmd) 76 | rootCmd.AddCommand(execCmd) 77 | rootCmd.AddCommand(versionCmd) 78 | } 79 | 80 | // Execute is the main entrypoint for the CLI. 81 | func Execute() error { 82 | return rootCmd.Execute() 83 | } 84 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | 7 | log "github.com/sirupsen/logrus" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // Rev is set on build time and should follow the semantic version format for 12 | // versioning strings. 13 | var Rev = "0.0.0-dev" 14 | 15 | var versionCmd = &cobra.Command{ 16 | Use: "version", 17 | Short: fmt.Sprintf("Print the version string of %s", projectName), 18 | Long: "", 19 | PreRun: func(cmd *cobra.Command, args []string) { 20 | if verbose { 21 | log.SetLevel(log.DebugLevel) 22 | } 23 | }, 24 | Run: func(cmd *cobra.Command, args []string) { 25 | fmt.Printf("%s %s (%s,%s-%s)", projectName, Rev, runtime.Version(), runtime.Compiler, runtime.GOARCH) 26 | }, 27 | } 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jacobbednarz/cf-vault 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/99designs/keyring v1.1.6 7 | github.com/cloudflare/cloudflare-go v0.115.0 8 | github.com/mitchellh/go-homedir v1.1.0 9 | github.com/olekukonko/tablewriter v0.0.5 10 | github.com/pelletier/go-toml v1.9.5 11 | github.com/sirupsen/logrus v1.9.3 12 | github.com/spf13/cobra v1.9.1 13 | golang.org/x/crypto v0.39.0 14 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 15 | golang.org/x/tools/gopls v0.18.1 16 | ) 17 | 18 | require ( 19 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect 20 | github.com/danieljoos/wincred v1.0.2 // indirect 21 | github.com/dvsekhvalnov/jose2go v1.6.0 // indirect 22 | github.com/goccy/go-json v0.10.5 // indirect 23 | github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect 24 | github.com/google/go-cmp v0.6.0 // indirect 25 | github.com/google/go-querystring v1.1.0 // indirect 26 | github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect 27 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 28 | github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d // indirect 29 | github.com/mattn/go-runewidth v0.0.13 // indirect 30 | github.com/mtibben/percent v0.2.1 // indirect 31 | github.com/rivo/uniseg v0.2.0 // indirect 32 | github.com/spf13/pflag v1.0.6 // indirect 33 | github.com/stretchr/objx v0.4.0 // indirect 34 | golang.org/x/exp/typeparams v0.0.0-20241210194714-1829a127f884 // indirect 35 | golang.org/x/mod v0.25.0 // indirect 36 | golang.org/x/net v0.40.0 // indirect 37 | golang.org/x/sync v0.15.0 // indirect 38 | golang.org/x/sys v0.33.0 // indirect 39 | golang.org/x/telemetry v0.0.0-20241220003058-cc96b6e0d3d9 // indirect 40 | golang.org/x/term v0.32.0 // indirect 41 | golang.org/x/text v0.26.0 // indirect 42 | golang.org/x/time v0.9.0 // indirect 43 | golang.org/x/tools v0.33.0 // indirect 44 | golang.org/x/vuln v1.1.3 // indirect 45 | honnef.co/go/tools v0.5.1 // indirect 46 | mvdan.cc/gofumpt v0.7.0 // indirect 47 | mvdan.cc/xurls/v2 v2.5.0 // indirect 48 | ) 49 | 50 | replace github.com/keybase/go-keychain => github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 51 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= 2 | github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= 3 | github.com/99designs/keyring v1.1.6 h1:kVDC2uCgVwecxCk+9zoCt2uEL6dt+dfVzMvGgnVcIuM= 4 | github.com/99designs/keyring v1.1.6/go.mod h1:16e0ds7LGQQcT59QqkTg72Hh5ShM51Byv5PEmW6uoRU= 5 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= 6 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 7 | github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM= 8 | github.com/cloudflare/cloudflare-go v0.115.0/go.mod h1:Ds6urDwn/TF2uIU24mu7H91xkKP8gSAHxQ44DSZgVmU= 9 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 10 | github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU= 11 | github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/dvsekhvalnov/jose2go v0.0.0-20200901110807-248326c1351b/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= 16 | github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= 17 | github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= 18 | github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= 19 | github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= 20 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 21 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 22 | github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= 23 | github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= 24 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 25 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 26 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 27 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 28 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 29 | github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8= 30 | github.com/google/safehtml v0.1.0/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= 31 | github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= 32 | github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= 33 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 34 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 35 | github.com/jba/templatecheck v0.7.1 h1:yOEIFazBEwzdTPYHZF3Pm81NF1ksxx1+vJncSEwvjKc= 36 | github.com/jba/templatecheck v0.7.1/go.mod h1:n1Etw+Rrw1mDDD8dDRsEKTwMZsJ98EkktgNJC6wLUGo= 37 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 38 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 39 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 40 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 41 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 42 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 43 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 44 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 45 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 46 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 47 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 48 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 49 | github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= 50 | github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= 51 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 52 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 53 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 54 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 55 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 56 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 57 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 58 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 59 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 60 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 61 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 62 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 63 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 64 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 65 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 66 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 67 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 68 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 69 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 70 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 71 | github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= 72 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 73 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 74 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 75 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 76 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 77 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 78 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 79 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 80 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 81 | golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= 82 | golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= 83 | golang.org/x/exp/typeparams v0.0.0-20241210194714-1829a127f884 h1:1xaZTydL5Gsg78QharTwKfA9FY9CZ1VQj6D/AZEvHR0= 84 | golang.org/x/exp/typeparams v0.0.0-20241210194714-1829a127f884/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= 85 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug= 86 | golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 87 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 88 | golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= 89 | golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 90 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 91 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 92 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 93 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 94 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 95 | golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= 96 | golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 97 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 98 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 99 | golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 100 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 101 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 102 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 103 | golang.org/x/telemetry v0.0.0-20241220003058-cc96b6e0d3d9 h1:L2k9GUV2TpQKVRGMjN94qfUMgUwOFimSQ6gipyJIjKw= 104 | golang.org/x/telemetry v0.0.0-20241220003058-cc96b6e0d3d9/go.mod h1:8h4Hgq+jcTvCDv2+i7NrfWwpYHcESleo2nGHxLbFLJ4= 105 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 106 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 107 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 108 | golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= 109 | golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= 110 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 111 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 112 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 113 | golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= 114 | golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= 115 | golang.org/x/tools/gopls v0.18.1 h1:2xJBNzdImS5u/kV/ZzqDLSvlBSeZX+pWY9uKVP7Pask= 116 | golang.org/x/tools/gopls v0.18.1/go.mod h1:UdNu0zeGjkmjL9L20QDszXu9tP2798pUIHC980kOBrI= 117 | golang.org/x/vuln v1.1.3 h1:NPGnvPOTgnjBc9HTaUx+nj+EaUYxl5SJOWqaDYGaFYw= 118 | golang.org/x/vuln v1.1.3/go.mod h1:7Le6Fadm5FOqE9C926BCD0g12NWyhg7cxV4BwcPFuNY= 119 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 120 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 121 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 122 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 123 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 124 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 125 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 126 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 127 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 128 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 129 | honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= 130 | honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= 131 | mvdan.cc/gofumpt v0.7.0 h1:bg91ttqXmi9y2xawvkuMXyvAA/1ZGJqYAEGjXuP0JXU= 132 | mvdan.cc/gofumpt v0.7.0/go.mod h1:txVFJy/Sc/mvaycET54pV8SW8gWxTlUuGHVEcncmNUo= 133 | mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= 134 | mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= 135 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/jacobbednarz/cf-vault/cmd" 5 | ) 6 | 7 | func main() { 8 | cmd.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /script/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | version=$1 4 | 5 | if [[ -z "$version" ]]; then 6 | echo "usage: $0 " 7 | exit 1 8 | fi 9 | 10 | git_sha=`git rev-parse --short HEAD` 11 | version_with_sha="${version}+${git_sha}" 12 | 13 | if [ -d build ]; then 14 | rm -rf build 15 | fi 16 | mkdir -p build 17 | 18 | platforms=("windows/amd64" "linux/amd64" "darwin/amd64" "darwin/arm64") 19 | 20 | echo "==> Build started for v${version}" 21 | 22 | for platform in "${platforms[@]}" 23 | do 24 | platform_split=(${platform//\// }) 25 | GOOS=${platform_split[0]} 26 | GOARCH=${platform_split[1]} 27 | 28 | mkdir -p "build/${GOOS}" 29 | 30 | output_name="cf-vault_${version}_${GOOS}_${GOARCH}" 31 | 32 | printf "==> Building %s\t%s\n" "$platform" "build/$output_name" | expand -t 30 33 | 34 | # trim GOPATH from the stacktraces 35 | GCFLAGS="-gcflags=all=-trimpath=$GOPATH -asmflags=all=-trimpath=$GOPATH" 36 | 37 | if [ $GOOS = "windows" ]; then 38 | env GOOS=$GOOS GOARCH=$GOARCH go build $GCFLAGS -o "build/${GOOS}/${GOARCH}/cf-vault.exe" -ldflags "-X github.com/jacobbednarz/cf-vault/cmd.Rev=${version_with_sha}" . 39 | else 40 | env GOOS=$GOOS GOARCH=$GOARCH go build $GCFLAGS -o "build/${GOOS}/${GOARCH}/cf-vault" -ldflags "-X github.com/jacobbednarz/cf-vault/cmd.Rev=${version_with_sha}" . 41 | fi 42 | if [ $? -ne 0 ]; then 43 | echo "Building the binary has failed!" 44 | exit 1 45 | fi 46 | 47 | touch build/checksums.txt 48 | 49 | printf "==> Tarballing %s\t%s\n" "$platform" "build/${output_name}.tar.gz" | expand -t 30 50 | if [ $GOOS = "windows" ]; then 51 | tar -czf "build/${output_name}.tar.gz" -C "build/$GOOS/$GOARCH" "cf-vault.exe" 52 | else 53 | tar -czf "build/${output_name}.tar.gz" -C "build/$GOOS/$GOARCH" "cf-vault" 54 | fi 55 | 56 | if [ $? -ne 0 ]; then 57 | echo "Creating the tarball has failed!" 58 | exit 1 59 | fi 60 | 61 | echo "==> Adding file checksums to build/checksums.txt" 62 | shasum -a 256 build/$GOOS/$GOARCH/* >> "build/checksums.txt" 63 | done 64 | 65 | shasum -a 256 build/*.tar.gz >> "build/checksums.txt" 66 | 67 | echo "==> Build process complete" 68 | -------------------------------------------------------------------------------- /script/generate-permission-policies: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Pull permission groups from Cloudflare's API 4 | # Generate policy output for pasting into generatePolicy() 5 | 6 | 7 | if [[ ! -z "${CLOUDFLARE_API_TOKEN}" ]]; then 8 | auth_headers=( -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" ) 9 | 10 | elif [[ ! -z "${CLOUDFLARE_EMAIL}" && ! -z "${CLOUDFLARE_API_KEY}" ]]; then 11 | auth_headers=( -H "X-Auth-Email: $CF_EMAIL" 12 | -H "X-Auth-Key: $CF_API_KEY" ) 13 | 14 | else 15 | >&2 echo -e "ERROR: CLOUDFLARE_API_TOKEN or (CLOUDFLARE_EMAIL, CLOUDFLARE_API_KEY) should be set in the environment.\n " 16 | exit 1 17 | fi 18 | 19 | groups=$(curl -s "https://api.cloudflare.com/client/v4/user/tokens/permission_groups" \ 20 | "${auth_headers[@]}") 21 | 22 | if [[ "$(echo "${groups}" | jq -r '.success')" != "true" ]]; then 23 | echo "Error fetching permission groups:" 24 | echo "${groups}" | jq 25 | exit 1 26 | fi 27 | 28 | declare -A account_read zone_read user_read 29 | declare -A account_write zone_write user_write 30 | 31 | # account 32 | while IFS=\| read id name; do 33 | if [[ "${name}" =~ (^|[^[:alnum:]_])Read([^[:alnum:]_]|$) ]]; then 34 | account_read+=([${id}]=${name}) 35 | else 36 | account_write+=([${id}]=${name}) 37 | fi 38 | done < <(echo "${groups}" | jq -r '.result[] | select(.scopes[] | contains("com.cloudflare.api.account")) | "\(.id)|\(.name)"') 39 | 40 | # zone 41 | while IFS=\| read id name; do 42 | if [[ "${name}" =~ (^|[^[:alnum:]_])Read([^[:alnum:]_]|$) ]]; then 43 | zone_read+=([${id}]=${name}) 44 | else 45 | zone_write+=([${id}]=${name}) 46 | fi 47 | done < <(echo "${groups}" | jq -r '.result[] | select(.scopes[] | contains("com.cloudflare.api.account.zone")) | "\(.id)|\(.name)"') 48 | 49 | # user 50 | while IFS=\| read id name; do 51 | if [[ "${name}" =~ (^|[^[:alnum:]_])Read([^[:alnum:]_]|$) ]]; then 52 | user_read+=([${id}]=${name}) 53 | elif [[ "${name}" == "API Tokens Write" ]]; then 54 | # Tokens can't have this permission 55 | : 56 | else 57 | user_write+=([${id}]=${name}) 58 | fi 59 | done < <(echo "${groups}" | jq -r '.result[] | select(.scopes[] | contains("com.cloudflare.api.user")) | "\(.id)|\(.name)"') 60 | 61 | 62 | # 63 | # Read-only 64 | # 65 | cat <<"EOF" 66 | readOnlyPolicy := []policy{ 67 | { 68 | Effect: "allow", 69 | Resources: map[string]interface{}{"com.cloudflare.api.account.*": "*"}, 70 | PermissionGroups: []permissionGroup{ 71 | EOF 72 | 73 | for key in "${!account_read[@]}"; do 74 | echo -e "\t\t\t\t{ID: \"${key}\"}, // ${account_read[$key]}" 75 | done | sort 76 | 77 | cat <<"EOF" 78 | }, 79 | }, 80 | { 81 | Effect: "allow", 82 | Resources: map[string]interface{}{"com.cloudflare.api.account.zone.*": "*"}, 83 | PermissionGroups: []permissionGroup{ 84 | EOF 85 | 86 | for key in "${!zone_read[@]}"; do 87 | echo -e "\t\t\t\t{ID: \"${key}\"}, // ${zone_read[$key]}" 88 | done | sort 89 | 90 | cat <<"EOF" 91 | }, 92 | }, 93 | { 94 | Effect: "allow", 95 | Resources: map[string]interface{}{"com.cloudflare.api.user." + userID: "*"}, 96 | PermissionGroups: []permissionGroup{ 97 | EOF 98 | 99 | for key in "${!user_read[@]}"; do 100 | echo -e "\t\t\t\t{ID: \"${key}\"}, // ${user_read[$key]}" 101 | done | sort 102 | 103 | cat <<"EOF" 104 | }, 105 | }, 106 | } 107 | EOF 108 | 109 | 110 | # 111 | # Write everything 112 | # 113 | cat <<"EOF" 114 | 115 | writeEverythingPolicy := []policy{ 116 | { 117 | Effect: "allow", 118 | Resources: map[string]interface{}{"com.cloudflare.api.account.*": "*"}, 119 | PermissionGroups: []permissionGroup{ 120 | EOF 121 | 122 | for key in "${!account_write[@]}"; do 123 | echo -e "\t\t\t\t{ID: \"${key}\"}, // ${account_write[$key]}" 124 | done | sort 125 | 126 | cat <<"EOF" 127 | }, 128 | }, 129 | { 130 | Effect: "allow", 131 | Resources: map[string]interface{}{"com.cloudflare.api.account.zone.*": "*"}, 132 | PermissionGroups: []permissionGroup{ 133 | EOF 134 | 135 | for key in "${!zone_write[@]}"; do 136 | echo -e "\t\t\t\t{ID: \"${key}\"}, // ${zone_write[$key]}" 137 | done | sort 138 | 139 | cat <<"EOF" 140 | }, 141 | }, 142 | { 143 | Effect: "allow", 144 | Resources: map[string]interface{}{"com.cloudflare.api.user." + userID: "*"}, 145 | PermissionGroups: []permissionGroup{ 146 | EOF 147 | 148 | for key in "${!user_write[@]}"; do 149 | echo -e "\t\t\t\t{ID: \"${key}\"}, // ${user_write[$key]}" 150 | done | sort 151 | 152 | cat <<"EOF" 153 | }, 154 | }, 155 | } 156 | EOF 157 | -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | // +build tools 3 | 4 | package tools 5 | 6 | //go:generate go install golang.org/x/tools/gopls@latest 7 | //go:generate go install golang.org/x/lint/golint@latest 8 | 9 | import ( 10 | _ "golang.org/x/lint/golint" 11 | _ "golang.org/x/tools/gopls" 12 | ) 13 | --------------------------------------------------------------------------------