├── docs ├── archive.md ├── disabled-pat-setting-in-group.png ├── backlogs.md ├── design-principles.md └── gitlab-api-specs.md ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── fossa.yml │ ├── sast.yml │ ├── lint.yml │ ├── release.yml │ ├── test.yml │ └── codeql-analysis.yml ├── scripts ├── tag-push.sh └── setup_dev_vault.sh ├── NOTICE ├── .goreleaser.yml ├── .golangci.yml ├── plugin ├── const.go ├── util.go ├── config.go ├── util_test.go ├── backend_test.go ├── gitlab_client_test.go ├── gitlab_client.go ├── backend.go ├── path_token_role.go ├── path_token_role_test.go ├── role.go ├── token.go ├── path_token_test.go ├── path_token.go ├── path_config_test.go ├── path_config.go ├── path_role.go └── path_role_test.go ├── main.go ├── Makefile ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── go.mod ├── README.md ├── LICENSE └── go.sum /docs/archive.md: -------------------------------------------------------------------------------- 1 | # Archive 2 | 3 | Any technical archive documents that had been desinged but deprecated 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vault-plugin-secrets-gitlab 2 | plugins/ 3 | .vscode/ 4 | dist/ 5 | coverage/ 6 | .tools/ 7 | .go/ 8 | .idea/ 9 | -------------------------------------------------------------------------------- /docs/disabled-pat-setting-in-group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splunk/vault-plugin-secrets-gitlab/HEAD/docs/disabled-pat-setting-in-group.png -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/fossa.yml: -------------------------------------------------------------------------------- 1 | name: OSS Scan 2 | jobs: 3 | fossa-scan: 4 | uses: splunk/oss-scanning-public/.github/workflows/oss-scan.yml@main 5 | secrets: inherit 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] -------------------------------------------------------------------------------- /scripts/tag-push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # should use ssh that's associated to deploy keys instead of user's PAT 5 | URL=`git remote get-url origin | sed -e "s/https:\/\/gitlab-ci-token:.*@//g"` 6 | git remote set-url origin "https://gitlab-ci-token:${GITLAB_TOKEN}@${URL}" 7 | 8 | # tag should trigger a pipeline 9 | git push origin --tags 10 | -------------------------------------------------------------------------------- /.github/workflows/sast.yml: -------------------------------------------------------------------------------- 1 | name: SAST 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: read 11 | jobs: 12 | SAST: 13 | name: SAST 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v5 18 | - name: semgrep 19 | uses: returntocorp/semgrep-action@v1 20 | with: 21 | publishToken: ${{ secrets.SEMGREP_KEY }} 22 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | golangci: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/setup-go@v6 15 | with: 16 | go-version: '1.25' 17 | - uses: actions/checkout@v5 18 | - name: golangci-lint 19 | uses: golangci/golangci-lint-action@v8 20 | with: 21 | version: v2.5.0 22 | args: --timeout 5m 23 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2021, Splunk Inc. All Rights Reserved. 2 | 3 | This project contains software developed by Splunk Inc. 4 | https://splunk.com 5 | 6 | This project is licensed to you under the Apache License, Version 2.0 (the "License"). 7 | You may not use this project except in compliance with the License. 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | project_name: vault-plugin-secrets-gitlab 2 | before: 3 | hooks: 4 | - go mod download 5 | builds: 6 | - env: 7 | - CGO_ENABLED=0 8 | goos: 9 | - linux 10 | - darwin 11 | - windows 12 | goarch: 13 | - amd64 14 | - arm64 15 | archives: 16 | - name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}_{{.Arch}}" 17 | format: tar.gz 18 | format_overrides: 19 | - goos: windows 20 | format: zip 21 | checksum: 22 | name_template: 'sha256-checksums.txt' 23 | snapshot: 24 | name_template: "{{ .Tag }}-next" 25 | changelog: 26 | sort: asc 27 | filters: 28 | exclude: 29 | - '^docs:' 30 | - '^test:' 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v5 14 | with: 15 | fetch-depth: 0 16 | - name: Set up Go 17 | uses: actions/setup-go@v6 18 | with: 19 | go-version: '1.25' 20 | - name: Run GoReleaser 21 | uses: goreleaser/goreleaser-action@v6 22 | with: 23 | # either 'goreleaser' (default) or 'goreleaser-pro' 24 | distribution: goreleaser 25 | version: latest 26 | args: release --clean 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | permissions: 30 | contents: "write" 31 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | linters: 4 | settings: 5 | cyclop: 6 | max-complexity: 13 7 | gocyclo: 8 | min-complexity: 15 9 | wsl_v5: 10 | allow-first-in-block: true 11 | allow-whole-block: false 12 | branch-max-lines: 2 13 | 14 | default: all 15 | disable: 16 | - gochecknoglobals 17 | - gochecknoinits 18 | - lll 19 | - depguard 20 | - wsl 21 | - durationcheck 22 | - wrapcheck 23 | - varnamelen 24 | - tagalign 25 | - tagliatelle 26 | - nilnil 27 | - nilerr 28 | - mnd 29 | - exhaustruct 30 | - err113 31 | - paralleltest 32 | exclusions: 33 | rules: 34 | - linters: 35 | - dupl 36 | - errcheck 37 | - goconst 38 | - gocyclo 39 | - testpackage 40 | path: _test\.go 41 | 42 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v5 15 | with: 16 | fetch-depth: 0 17 | - uses: actions/cache@v4 18 | with: 19 | path: | 20 | ~/.cache/go-build 21 | ~/go/pkg/mod 22 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 23 | restore-keys: | 24 | ${{ runner.os }}-go- 25 | - name: Set Go Version 26 | run: echo "GO_VERSION=$(grep "go 1." go.mod | cut -d " " -f 2)" >> $GITHUB_ENV 27 | - name: Set up Go 28 | uses: actions/setup-go@v6 29 | with: 30 | go-version: ${{ env.GO_VERSION }} 31 | - name: Test 32 | run: make test 33 | - name: Codecov 34 | uses: codecov/codecov-action@v5 35 | with: 36 | directory: coverage/unit/ 37 | flags: unittests 38 | -------------------------------------------------------------------------------- /plugin/const.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitlabtoken 16 | 17 | import gitlab "gitlab.com/gitlab-org/api/client-go" 18 | 19 | // PAT stands for Project Access Token. 20 | type PAT = gitlab.ProjectAccessToken 21 | 22 | const ( 23 | pathPatternConfig = "config" 24 | pathPatternToken = "token" 25 | pathPatternRoles = "roles" 26 | 27 | //nolint:godot 28 | // accessLevelGuest = 10 29 | // accessLevelReporter = 20 30 | // accessLevelDeveloper = 30 31 | // accessLevelMaintainer = 40 32 | ) 33 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | - cron: '34 19 * * 6' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | security-events: write 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | language: [ 'go' ] 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v5 24 | - uses: actions/cache@v4 25 | with: 26 | path: | 27 | ~/.cache/go-build 28 | ~/go/pkg/mod 29 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 30 | restore-keys: | 31 | ${{ runner.os }}-go- 32 | - name: Set Go Version 33 | run: echo "GO_VERSION=$(grep "go 1." go.mod | cut -d " " -f 2)" >> $GITHUB_ENV 34 | - name: Set up Go 35 | uses: actions/setup-go@v6 36 | with: 37 | go-version: ${{ env.GO_VERSION }} 38 | - name: Initialize CodeQL 39 | uses: github/codeql-action/init@v2 40 | with: 41 | languages: ${{ matrix.language }} 42 | - name: Build 43 | run: make build-linux 44 | - name: Perform CodeQL Analysis 45 | uses: github/codeql-action/analyze@v3 46 | -------------------------------------------------------------------------------- /scripts/setup_dev_vault.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euox pipefail 4 | 5 | 6 | : ${GITLAB_URL:?unset} 7 | 8 | export VAULT_ADDR="http://localhost:8200" 9 | export VAULT_TOKEN=root 10 | 11 | setup_vault() { 12 | plugin=vault-plugin-secrets-gitlab 13 | existing=$(vault secrets list -format json | jq -r '."gitlab/"') 14 | if [ "$existing" == "null" ]; then 15 | 16 | # in CI, current container bind mount is private, preventing nested bind mounts 17 | # instead, copy plugin in to vault container and reload 18 | vault plugin list secret | grep -q gitlab 19 | if [ $? -ne 0 ]; then 20 | echo "Plugin missing from dev plugin dir /vault/plugins... registering manually." 21 | sha=$(shasum -a 256 plugins/$plugin | cut -d' ' -f1) 22 | # if plugin is missing, it is assumed this is a CI environment and vault is running in a container 23 | docker cp plugins/$plugin vault:/vault/plugins 24 | vault plugin register -sha256=$sha secret $plugin 25 | fi 26 | 27 | echo "Enabling vault gitlab plugin..." 28 | vault secrets enable -path=gitlab $plugin 29 | 30 | else 31 | echo 32 | echo "Plugin enabled on path 'gitlab/':" 33 | echo "$existing" | jq 34 | fi 35 | 36 | vault write gitlab/config base_url=$GITLAB_URL token=$GITLAB_TOKEN 37 | } 38 | 39 | setup_vault >&2 40 | 41 | # eval output for local use 42 | echo export VAULT_ADDR=\"$VAULT_ADDR\"\; 43 | echo export VAULT_TOKEN=\"$VAULT_TOKEN\"\; 44 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package main 15 | 16 | import ( 17 | "log" 18 | "os" 19 | 20 | "github.com/hashicorp/vault/api" 21 | 22 | "github.com/hashicorp/vault/sdk/plugin" 23 | gitlabtoken "github.com/splunk/vault-plugin-secrets-gitlab/plugin" 24 | ) 25 | 26 | var ( 27 | version = "dev" 28 | commit = "none" 29 | date = "unknown" 30 | ) 31 | 32 | func main() { 33 | apiClientMeta := &api.PluginAPIClientMeta{} 34 | flags := apiClientMeta.FlagSet() 35 | _ = flags.Parse(os.Args[1:]) 36 | 37 | tlsConfig := apiClientMeta.GetTLSConfig() 38 | tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig) 39 | 40 | log.Printf("vault-plugin-secrets-gitlab %s, commit %s, built at %s\n", version, commit, date) 41 | 42 | err := plugin.Serve(&plugin.ServeOpts{ 43 | BackendFactoryFunc: gitlabtoken.Factory, 44 | TLSProviderFunc: tlsProviderFunc, 45 | }) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/backlogs.md: -------------------------------------------------------------------------------- 1 | # Backlogs 2 | 3 | ## Replace Gitlab Library 4 | 5 | For speed of development, I reused library. This library is actively maintained by community. However, with the far more active development on Gitlab side, library sometimes sits behind. Also since this plugin only uses limited API endpoints, we can possibly maintain our gitlab clinet. 6 | 7 | ## Feasibility with Gitlab's Native Vault Support 8 | 9 | This plugin is not initially created with a mindset of compatibility with Gitlab's native vault support for static credentials. 10 | 11 | ## Granular Control on Token Expiry 12 | 13 | Gitlab currently doesn't have granular control on token expiry. A token is expired at midnight UTC of a chosen day. We should have a shorter-lived token like 15 mins in case a user failed to revoke a token after use. 14 | 15 | Gitlab issue to have [granular control on token expiry] 16 | 17 | ## Pipeline 18 | 19 | Setup CICD pipeline in gitlab and do the following at least. 20 | 21 | - lint 22 | - unit test 23 | - OSS scan 24 | - SAST scan 25 | - binary build and publish 26 | 27 | For comprehensive CI, 28 | 29 | - DAST scan 30 | - acceptance testing 31 | 32 | ## Acceptance Testing 33 | 34 | Running test against real servers doesn't seem good idea. Create an isolated environment by spinning up vault and gitlab in docker in CI. Then, run full suite of testing there. *Self-hosted GitLab has project access token available from free version* 35 | 36 | [granular control on token expiry]: https://gitlab.com/gitlab-org/gitlab/-/issues/335535 37 | -------------------------------------------------------------------------------- /plugin/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitlabtoken 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "strconv" 21 | 22 | "github.com/hashicorp/go-multierror" 23 | ) 24 | 25 | func validateScopes(scopes []string) error { 26 | var err *multierror.Error 27 | 28 | for _, scope := range scopes { 29 | switch scope { 30 | case "api", "read_api", 31 | "read_registry", "write_registry", 32 | "read_repository", "write_repository": 33 | continue 34 | default: 35 | err = multierror.Append(err, fmt.Errorf("scope '%s' is not allowed", scope)) 36 | } 37 | } 38 | 39 | return err.ErrorOrNil() 40 | } 41 | 42 | func envOrDefault(key, d string) string { 43 | env := os.Getenv(key) 44 | if env == "" { 45 | return d 46 | } 47 | 48 | return env 49 | } 50 | 51 | func envAsInt(key string, def int) int { 52 | v := envOrDefault(key, "") 53 | 54 | val, err := strconv.Atoi(v) 55 | if err == nil { 56 | return val 57 | } 58 | 59 | return def 60 | } 61 | -------------------------------------------------------------------------------- /plugin/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitlabtoken 16 | 17 | import ( 18 | "context" 19 | "time" 20 | 21 | "github.com/hashicorp/vault/sdk/logical" 22 | ) 23 | 24 | // ConfigStorageEntry structure represents the config as it is stored within vault. 25 | type ConfigStorageEntry struct { 26 | BaseURL string `json:"base_url" structs:"base_url" mapstructure:"base_url"` 27 | Token string `json:"token" structs:"token" mapstructure:"token"` 28 | MaxTTL time.Duration `json:"max_ttl" structs:"max_ttl" mapstructure:"max_ttl"` 29 | AllowOwnerLevel bool `json:"allow_owner_level" structs:"allow_owner_level" mapstructure:"allow_owner_level"` 30 | } 31 | 32 | func getConfig(ctx context.Context, s logical.Storage) (*ConfigStorageEntry, error) { 33 | var config ConfigStorageEntry 34 | 35 | configRaw, err := s.Get(ctx, pathPatternConfig) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | if configRaw == nil { 41 | return nil, nil 42 | } 43 | 44 | err = configRaw.DecodeJSON(&config) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return &config, err 50 | } 51 | -------------------------------------------------------------------------------- /plugin/util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitlabtoken 16 | 17 | import ( 18 | "errors" 19 | "testing" 20 | 21 | "github.com/hashicorp/go-multierror" 22 | "github.com/stretchr/testify/assert" 23 | "github.com/stretchr/testify/require" 24 | ) 25 | 26 | func TestValidateScopes(t *testing.T) { 27 | t.Parallel() 28 | 29 | t.Run("valid_scopes", func(t *testing.T) { 30 | t.Parallel() 31 | 32 | validScopes := []string{"api", "read_api", 33 | "read_registry", "write_registry", 34 | "read_repository", "write_repository"} 35 | err := validateScopes(validScopes) 36 | require.NoError(t, err, "not expecting error: %s", err) 37 | }) 38 | 39 | t.Run("invalid_scopes", func(t *testing.T) { 40 | t.Parallel() 41 | 42 | invalidScopes := []string{"something", "invalid"} 43 | err := validateScopes(invalidScopes) 44 | require.Error(t, err, "expecting error") 45 | 46 | var merr *multierror.Error 47 | if errors.As(err, &merr) { 48 | assert.Len(t, merr.Errors, 2, "expecting %d errors, got %s", 2, len(merr.Errors)) 49 | } 50 | 51 | assert.Contains(t, err.Error(), "scope 'something' is not allowed") 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /plugin/backend_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitlabtoken 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | 21 | "github.com/hashicorp/vault/sdk/logical" 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | // getTestBackend returns the mocked out backend for testing. 26 | // 27 | //nolint:ireturn 28 | func getTestBackend(t *testing.T, mockGitlab bool) (logical.Backend, logical.Storage) { 29 | t.Helper() 30 | 31 | config := logical.TestBackendConfig() 32 | config.StorageView = &logical.InmemStorage{} 33 | 34 | b, err := Factory(context.Background(), config) 35 | require.NoError(t, err, "unable to create backend") 36 | 37 | if mockGitlab { 38 | gb, _ := b.(*GitlabBackend) 39 | gb.client = &mockGitlabClient{} 40 | } 41 | 42 | return b, config.StorageView 43 | } 44 | 45 | //nolint:ireturn 46 | func newGitlabAccEnv(t *testing.T) (*logical.Request, logical.Backend) { 47 | t.Helper() 48 | 49 | backend, storage := getTestBackend(t, false) 50 | 51 | conf := map[string]interface{}{ 52 | "base_url": envOrDefault("GITLAB_URL", "http://localhost"), 53 | "token": envOrDefault("GITLAB_TOKEN", "BogusToken"), 54 | } 55 | 56 | testConfigUpdate(t, backend, storage, conf) 57 | 58 | req := &logical.Request{ 59 | Storage: storage, 60 | } 61 | 62 | return req, backend 63 | } 64 | -------------------------------------------------------------------------------- /docs/design-principles.md: -------------------------------------------------------------------------------- 1 | # Gitlab Secrets Engine 2 | 3 | The Gitlab Vault secrets plugin dynamically generates gitlab project access token based on passed parameters. This enables users to gain access to Gitlab projects without needing to create or manage project access tokens manually. 4 | 5 | ## Design Principles 6 | 7 | This plugin supports two ways to generate a token in `/token` path 8 | 9 | 1. At root of `/token` path, a user requests a token by passing parameters. 10 | 2. (WIP): A user predefines roles with parameters. Then, a user can request a role's token at `/token/:` 11 | 12 | Parameters are same from Gitlab's [Project Access Token API] 13 | 14 | path `/token` 15 | 16 | - Create/Update: generate a project access token with given parameters 17 | 18 | path `/roles/:` 19 | 20 | - Create/Update: create/update vault resource with given parameters. This won't do anything against Gitlab API 21 | - Delete: delete vault resource 22 | - Get: return stored parameters for the role 23 | - List: list all roles 24 | 25 | path `/token/:` 26 | 27 | - Create/Update: generate a project access token with stored parameters for the role 28 | 29 | ## Things to Note 30 | 31 | ### Access Control 32 | 33 | There are 2 kinds of access control in this plugins. 34 | 35 | 1. permissions attaches to the configured token 36 | 1. Vault resource access control - path access and capabilities 37 | 38 | Root `/token` path can be used to request a project access token for any projects and any scopes as long as the configured token to generate access tokens have necessary permissions in these projects. 2nd kind of access token can't limit parameters to pass. 39 | 40 | With that being said, it's better to use **roles**, which predefines a project and scopes; then, requesting a project access token for a role. You can further limit access to path via 2nd kind of access control imposed by Vault 41 | 42 | [Project Access Token API]: https://docs.gitlab.com/ee/api/resource_access_tokens.html 43 | -------------------------------------------------------------------------------- /plugin/gitlab_client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitlabtoken 16 | 17 | import ( 18 | "testing" 19 | "time" 20 | 21 | "github.com/stretchr/testify/assert" 22 | "github.com/stretchr/testify/require" 23 | ) 24 | 25 | func TestNewClientFail(t *testing.T) { 26 | t.Parallel() 27 | t.Run("no config", func(t *testing.T) { 28 | t.Parallel() 29 | 30 | c, err := NewClient(nil) 31 | require.Error(t, err, "nil config should thrown an error when retrieving Gitlab client") 32 | assert.Nil(t, c, "NewClient should return nil client on error") 33 | }) 34 | 35 | t.Run("empty config", func(t *testing.T) { 36 | t.Parallel() 37 | 38 | config := &ConfigStorageEntry{} 39 | c, err := NewClient(config) 40 | require.Error(t, err, "NewClient should return an error if config is missing auth") 41 | assert.Nil(t, c, "NewClient should return nil client on error") 42 | }) 43 | } 44 | 45 | func TestValid(t *testing.T) { 46 | t.Parallel() 47 | 48 | tests := []struct { 49 | name string 50 | client *GitlabClient 51 | asserter assert.BoolAssertionFunc 52 | }{ 53 | { 54 | name: "valid", 55 | client: &GitlabClient{ 56 | expiration: time.Now().Add(clientTTL), 57 | }, 58 | asserter: assert.True, 59 | }, 60 | { 61 | name: "expired", 62 | client: &GitlabClient{ 63 | expiration: time.Now().Add(-1 * time.Minute), 64 | }, 65 | asserter: assert.False, 66 | }, 67 | } 68 | 69 | for _, test := range tests { 70 | t.Run(test.name, func(t *testing.T) { 71 | t.Parallel() 72 | test.asserter(t, test.client.Valid()) 73 | }) 74 | } 75 | } 76 | 77 | type mockGitlabClient struct{} 78 | 79 | var _ Client = &mockGitlabClient{} 80 | 81 | func (ac *mockGitlabClient) Valid() bool { 82 | return true 83 | } 84 | 85 | // func (ac *mockGitlabClient) ListProjectAccessToken(id int) ([]*PAT, error) { 86 | // return nil, nil 87 | // } 88 | func (ac *mockGitlabClient) CreateProjectAccessToken(_ *BaseTokenStorageEntry, _ *time.Time) (*PAT, error) { 89 | return nil, nil 90 | } 91 | 92 | // func (ac *mockGitlabClient) RevokeProjectAccessToken(tokenStorage *BaseTokenStorageEntry) error { 93 | // return nil 94 | // } 95 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME?=vault-plugin-secrets-gitlab 2 | 3 | .DEFAULT_GOAL := all 4 | all: get build lint test 5 | 6 | get: 7 | go get ./... 8 | 9 | build: 10 | go build -v -o plugins/$(NAME) 11 | 12 | build-linux: 13 | @GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o plugins/$(NAME) 14 | 15 | 16 | lint: .tools/golangci-lint 17 | .tools/golangci-lint --version 18 | .tools/golangci-lint run -v 19 | 20 | test: 21 | mkdir -p coverage/unit 22 | go test -short -parallel=10 -v -covermode=count -cover ./... $(TESTARGS) -args -test.gocoverdir="$(shell pwd)/coverage/unit" 23 | 24 | # acc-test: tools 25 | # mkdir -p coverage/int 26 | # @(eval $$(./scripts/init_dev.sh) && go test -parallel=10 -v -covermode=count -cover ./... -run=TestAcc -args -test.gocoverdir="$(shell pwd)/coverage/int") 27 | 28 | report: .tools/gocover-cobertura 29 | mkdir -p coverage 30 | # go tool covdata textfmt -i ./coverage/unit,./coverage/int -o coverage/profile 31 | go tool covdata textfmt -i ./coverage/unit -o coverage/profile 32 | go tool cover -func=coverage/profile 33 | go tool cover -html=coverage/profile -o coverage/coverage.html 34 | .tools/gocover-cobertura < coverage/profile > coverage/coverage.xml 35 | 36 | vault-only: build 37 | vault server -log-level=debug -dev -dev-root-token-id=root -dev-plugin-dir=./plugins 38 | 39 | dev: tools build-linux 40 | @./scripts/init_dev.sh 41 | 42 | clean-dev: 43 | @cd scripts && docker-compose down 44 | 45 | clean-all: clean-dev 46 | @rm -rf .tools coverage*.* plugins 47 | 48 | tools: .tools .tools/docker-compose .tools/gocover-cobertura .tools/golangci-lint .tools/jq .tools/vault 49 | 50 | .tools: 51 | @mkdir -p .tools 52 | 53 | .tools/docker-compose: DOCKER_COMPOSE_VERSION = 2.40.0 54 | .tools/docker-compose: DOCKER_COMPOSE_BINARY = "docker-compose-$(shell uname -s)-$(shell uname -m)" 55 | .tools/docker-compose: 56 | curl -so .tools/docker-compose -L "https://github.com/docker/compose/releases/download/$(DOCKER_COMPOSE_VERSION)/$(DOCKER_COMPOSE_BINARY)" 57 | @chmod +x .tools/docker-compose 58 | 59 | .tools/gocover-cobertura: 60 | export GOBIN=$(shell pwd)/.tools; go install github.com/boumenot/gocover-cobertura@v1.4.0 61 | 62 | .tools/golangci-lint: 63 | export GOBIN=$(shell pwd)/.tools; go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0 64 | 65 | .tools/jq: JQ_VERSION = 1.8.1 66 | .tools/jq: JQ_PLATFORM = $(patsubst darwin,osx-amd,$(shell uname -s | tr A-Z a-z)) 67 | .tools/jq: 68 | curl -so .tools/jq -sSL https://github.com/stedolan/jq/releases/download/jq-$(JQ_VERSION)/jq-$(JQ_PLATFORM)64 69 | @chmod +x .tools/jq 70 | 71 | .tools/vault: VAULT_VERSION = 1.19.5 72 | .tools/vault: VAULT_PLATFORM = $(shell uname -s | tr A-Z a-z) 73 | .tools/vault: 74 | curl -so .tools/vault.zip -sSL https://releases.hashicorp.com/vault/$(VAULT_VERSION)/vault_$(VAULT_VERSION)_$(VAULT_PLATFORM)_amd64.zip 75 | (cd .tools && unzip -o vault.zip && rm vault.zip) 76 | 77 | .PHONY: all get build build-linux publish lint test test-artacc test-vaultacc report vault-only dev clean-dev clean-all tools 78 | -------------------------------------------------------------------------------- /docs/gitlab-api-specs.md: -------------------------------------------------------------------------------- 1 | # Gitlab API Specs 2 | 3 | Gitlab project access token API docs can be found [here][pat doc]. 4 | 5 | ## API behavior 6 | 7 | ### Creating a Token 8 | 9 | #### Create a token with same name that exists in a project 10 | 11 | when you create a token with same name that already exists in a project, it creates another token. Name field is actually obsolete in terms of uniqueness. Every call is POST and it creates a new token regardless of name provided. There's currently no update(PUT) API for existing project access tokens. 12 | 13 | ```bash 14 | ➜ curl --header "PRIVATE-TOKEN: MYTOKEN" https://my.gitlab.com/api/v4/projects/1/access_tokens/ -XPOST --header "Content-Type:application/json" \ 15 | --data '{ "name":"test_token", "scopes":["api"] }' 16 | {"id":10,"name":"test_token","revoked":false,"created_at":"2021-01-01T00:00:0.000Z","scopes":["api"],"user_id":10,"active":true,"expires_at":null,"token":"XXX"} 17 | ➜ curl --header "PRIVATE-TOKEN: MYTOKEN" https://my.gitlab.com/api/v4/projects/1/access_tokens/ -XPOST --header "Content-Type:application/json" \ 18 | --data '{ "name":"test_token", "scopes":["api"] }' 19 | {"id":11,"name":"test_token","revoked":false,"created_at":"2021-0101T00:00:1.000Z","scopes":["api"],"user_id":11,"active":true,"expires_at":null,"token":"YYY"} 20 | ``` 21 | 22 | ### Create a token in a project where its parent group disables creation of project access token 23 | 24 | when a parent group disables `Allow project access token creation` like [this image](./disabled-pat-setting-in-group.md). (You can visit thsi in groups settings > genera > Permissions) 25 | 26 | ```bash 27 | ➜ curl --header "PRIVATE-TOKEN: MYTOKEN" https://my.gitlab.com/api/v4/projects/1/access_tokens/ -XPOST --header "Content-Type:application/json" \ 28 | --data '{ "name":"test_token", "scopes":["api"] }' 29 | {"message":"400 Bad request - User does not have permission to create project access token"} 30 | ``` 31 | 32 | ### Create a token with scope that's not available for the project 33 | 34 | If a project/group/instance doesn't enable certain scopes such as container registry, it gets 400 35 | 36 | ```bash 37 | ➜ curl --header "PRIVATE-TOKEN: MYTOKEN" https://my.gitlab.com/api/v4/projects/1/access_tokens/ -XPOST --header "Content-Type:application/json" --data '{ "name":"test_token_developer", "scopes":["api", "read_repository","read_registry"], "access_level": 40 }' 38 | {"message":"400 Bad request - Scopes can only contain available scopes"}% 39 | ``` 40 | 41 | ### Revoking a Token 42 | 43 | #### Revoking a token that has been revoked 44 | 45 | ```bash 46 | ➜ curl --header "PRIVATE-TOKEN: MYTOKEN" https://my.gitlab.com/api/v4/projects/1/access_tokens/1 -XDELETE 47 | {"message":"404 Could not find project access token with token_id: 1 Not Found"} 48 | ``` 49 | 50 | #### Revoking a token in another project 51 | 52 | Say, a token is created in project 1. What happens if we try to delete the generated token in another project, say project 2 53 | 54 | [pat doc]: https://docs.gitlab.com/ee/api/resource_access_tokens.html 55 | -------------------------------------------------------------------------------- /plugin/gitlab_client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitlabtoken 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "time" 21 | 22 | gitlab "gitlab.com/gitlab-org/api/client-go" 23 | ) 24 | 25 | const ( 26 | clientTTL = 30 * time.Minute 27 | ) 28 | 29 | // Client makes API calls to GitLab. 30 | type Client interface { 31 | // ListProjectAccessToken(int) ([]*PAT, error) 32 | CreateProjectAccessToken(e *BaseTokenStorageEntry, t *time.Time) (*PAT, error) 33 | // RevokeProjectAccessToken(*BaseTokenStorageEntry) error 34 | Valid() bool 35 | } 36 | 37 | // GitlabClient calls the GitLab API and implements the provided Client interface. 38 | type GitlabClient struct { 39 | client *gitlab.Client 40 | expiration time.Time 41 | } 42 | 43 | var _ Client = &GitlabClient{} 44 | 45 | // NewClient returns a new GitLab Client and any error if occurs. 46 | func NewClient(config *ConfigStorageEntry) (*GitlabClient, error) { 47 | if config == nil { 48 | return nil, errors.New("gitlab backend configuration has not been set up") 49 | } 50 | 51 | gc := &GitlabClient{ 52 | expiration: time.Now().Add(clientTTL), 53 | } 54 | 55 | opt := gitlab.WithBaseURL(config.BaseURL) 56 | if config.Token == "" { 57 | return nil, errors.New("token isn't configured") 58 | } 59 | 60 | c, err := gitlab.NewClient(config.Token, opt) 61 | if err != nil { 62 | return nil, fmt.Errorf("failed to create Gitlab client iwht endpoint %s: %w", config.BaseURL, err) 63 | } 64 | 65 | gc.client = c 66 | 67 | return gc, nil 68 | } 69 | 70 | // Valid returns true if the client is not expired. 71 | func (gc *GitlabClient) Valid() bool { 72 | return gc != nil && time.Now().Before(gc.expiration) 73 | } 74 | 75 | // CreateProjectAccessToken returns a new Project Access Token and/or any error. 76 | func (gc *GitlabClient) CreateProjectAccessToken(tokenStorage *BaseTokenStorageEntry, expiresAt *time.Time) (*PAT, error) { 77 | opt := gitlab.CreateProjectAccessTokenOptions{ 78 | Name: &tokenStorage.Name, 79 | Scopes: &tokenStorage.Scopes, 80 | } 81 | if expiresAt != nil { 82 | expiration := gitlab.ISOTime(*expiresAt) 83 | opt.ExpiresAt = &expiration 84 | } 85 | 86 | if tokenStorage.AccessLevel != 0 { 87 | opt.AccessLevel = (*gitlab.AccessLevelValue)(&tokenStorage.AccessLevel) 88 | } 89 | 90 | pat, _, err := gc.client.ProjectAccessTokens.CreateProjectAccessToken(tokenStorage.ID, &opt) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | return pat, nil 96 | } 97 | 98 | // func (gc *gitlabClient) RevokeProjectAccessToken(tokenStorage *BaseTokenStorageEntry) error { 99 | // return nil 100 | // } 101 | -------------------------------------------------------------------------------- /plugin/backend.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitlabtoken 16 | 17 | import ( 18 | "context" 19 | "strings" 20 | "sync" 21 | 22 | "github.com/hashicorp/vault/sdk/framework" 23 | "github.com/hashicorp/vault/sdk/helper/locksutil" 24 | "github.com/hashicorp/vault/sdk/logical" 25 | ) 26 | 27 | // GitlabBackend is the backend for Gitlab plugin. 28 | type GitlabBackend struct { 29 | *framework.Backend 30 | 31 | view logical.Storage 32 | client Client 33 | lock sync.RWMutex 34 | roleLocks []*locksutil.LockEntry 35 | } 36 | 37 | func (b *GitlabBackend) getClient(ctx context.Context, s logical.Storage) (Client, error) { //nolint:ireturn 38 | b.lock.RLock() 39 | unlockFunc := b.lock.RUnlock 40 | 41 | defer func() { unlockFunc() }() 42 | 43 | if b.client != nil && b.client.Valid() { 44 | return b.client, nil 45 | } 46 | 47 | b.lock.RUnlock() 48 | b.lock.Lock() 49 | unlockFunc = b.lock.Unlock 50 | 51 | if b.client != nil && b.client.Valid() { 52 | return b.client, nil 53 | } 54 | 55 | config, err := getConfig(ctx, s) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | c, err := NewClient(config) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | b.client = c 66 | 67 | return c, nil 68 | } 69 | func (b *GitlabBackend) reset() { 70 | b.lock.Lock() 71 | defer b.lock.Unlock() 72 | 73 | b.client = nil 74 | } 75 | func (b *GitlabBackend) invalidate(_ context.Context, key string) { 76 | if key == pathPatternConfig { 77 | b.reset() 78 | } 79 | } 80 | 81 | // Factory is the factory for the backend. 82 | func Factory(ctx context.Context, c *logical.BackendConfig) (logical.Backend, error) { //nolint:ireturn 83 | b := Backend(c) 84 | 85 | err := b.Setup(ctx, c) 86 | if err != nil { 87 | return nil, err 88 | } 89 | 90 | return b, nil 91 | } 92 | 93 | // Backend exports the function to create backend and configure. 94 | func Backend(conf *logical.BackendConfig) *GitlabBackend { 95 | backend := &GitlabBackend{ 96 | view: conf.StorageView, 97 | roleLocks: locksutil.CreateLocks(), 98 | } 99 | 100 | backend.Backend = &framework.Backend{ 101 | BackendType: logical.TypeLogical, 102 | Help: strings.TrimSpace(backendHelp), 103 | Paths: framework.PathAppend( 104 | pathConfig(backend), 105 | pathToken(backend), 106 | pathRole(backend), 107 | pathRoleList(backend), 108 | pathRoleToken(backend), 109 | ), 110 | Invalidate: backend.invalidate, 111 | } 112 | 113 | return backend 114 | } 115 | 116 | const backendHelp = ` 117 | The Gitlab token engine dynamically generates Gitlab project access token 118 | based on user defined permission targets. This enables users to gain access to 119 | Gitlab resources without needing to create or manage a static project access token. 120 | 121 | After mounting this secrets engine, you can configure the credentials using the 122 | "config/" endpoints. You can generate project access tokens using the "token/" endpoints. 123 | ` 124 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances 31 | of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others’ private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Project maintainers are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Project maintainers have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for 49 | moderation decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail 56 | address, posting via an official social media account, or acting as an 57 | appointed representative at an online or offline event. Representation of a 58 | project may be further defined and clarified by project maintainers. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported by contacting the project team. All 64 | complaints will be reviewed and investigated and will result in a response that 65 | is deemed necessary and appropriate to the circumstances. The project team is 66 | obligated to maintain confidentiality with regard to the reporter of an incident. 67 | Further details of specific enforcement policies may be posted separately. 68 | 69 | Project maintainers who do not follow or enforce the Code of Conduct in good 70 | faith may face temporary or permanent repercussions as determined by other 71 | members of the project's leadership. 72 | 73 | ## Attribution 74 | 75 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 76 | available at 77 | 78 | [homepage]: https://www.contributor-covenant.org/ 79 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project! Whether it's a bug report, new feature, question, or additional documentation, we greatly value feedback and contributions from our community. Read through this document before submitting any issues or pull requests to ensure we have all the necessary information to effectively respond to your bug report or contribution. 4 | 5 | In addition to this document, please review our [Code of Conduct](CODE_OF_CONDUCT.md). For any code of conduct questions or comments please leave an issue. 6 | 7 | If you would like to contribute to this project, see [Contributions to Splunk] for more information and we ask you to sign Contributor License Agreement(CLA). 8 | 9 | ## Reporting Bugs/Feature Requests 10 | 11 | We welcome you to use the [GitHub issue tracker] to report bugs or suggest features. When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 12 | 13 | - A reproducible test case or series of steps 14 | - The version of this plugin 15 | - The version of Vault Server 16 | - The version of Gitlab Server 17 | - Any modifications you've made relevant to the bug 18 | - Anything unusual about your environment or deployment 19 | - Any known workarounds 20 | 21 | When filing an issue, please do *NOT* include: 22 | 23 | - Internal identifiers such as JIRA tickets 24 | - Any sensitive information related to your environment, users, etc. 25 | 26 | ## Contributing via Merge Requests 27 | 28 | Contributions via Merge Requests (MRs) are much appreciated. Before sending us a merge request, please ensure that: 29 | 30 | 1. You are working against the latest source on the `main` branch. 31 | 2. You check existing open, and recently merged, merge requests to make sure 32 | someone else hasn't addressed the problem already. 33 | 3. You open an issue to discuss any significant work - we would hate for your 34 | time to be wasted. 35 | 4. You submit MRs that are easy to review and ideally less 500 lines of code. 36 | Multiple MRs can be submitted for larger contributions. 37 | 38 | To send us a merge request, please: 39 | 40 | 1. Fork the project. 41 | 2. Modify the source; please ensure a single change per MR. If you also 42 | reformat all the code, it will be hard for us to focus on your change. 43 | 3. Ensure local tests pass and add new tests related to the contribution. 44 | 4. Commit to your fork using clear commit messages. 45 | 5. Send us a merge request, answering any default questions in the merge request 46 | interface. 47 | 6. Pay attention to any automated CI failures reported in the merge request, and 48 | stay involved in the conversation. 49 | 50 | GitHub provides additional documentation on [forking a project](https://docs.github.com/en/get-started/quickstart/fork-a-repo) and [creating a pull request](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests). 51 | 52 | ## Finding contributions to work on 53 | 54 | Looking at the existing issues is a great way to find something to contribute 55 | on. As our projects, by default, use the default GitHub issue labels 56 | (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at 57 | any 'help wanted' issues is a great place to start. 58 | 59 | ## Licensing 60 | 61 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to 62 | confirm the licensing of your contribution. 63 | 64 | [GitHub issue tracker]: https://github.com/splunk/vault-plugin-secrets-gitlab/issues 65 | [Contributions to Splunk]: https://www.splunk.com/en_us/form/contributions.html 66 | -------------------------------------------------------------------------------- /plugin/path_token_role.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitlabtoken 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "time" 22 | 23 | "github.com/hashicorp/vault/sdk/framework" 24 | "github.com/hashicorp/vault/sdk/logical" 25 | ) 26 | 27 | var roleTokenSchema = map[string]*framework.FieldSchema{ 28 | "role_name": { 29 | Type: framework.TypeString, 30 | Description: "Role name", 31 | }, 32 | } 33 | 34 | func (b *GitlabBackend) pathRoleTokenCreate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 35 | gc, err := b.getClient(ctx, req.Storage) 36 | if err != nil { 37 | return logical.ErrorResponse("failed to obtain gitlab client - %s", err.Error()), nil 38 | } 39 | 40 | roleName, ok := data.Get("role_name").(string) 41 | if !ok { 42 | return nil, errors.New("string type assertion failed for data field 'role_name'") 43 | } 44 | 45 | // get the role by name 46 | role, err := getRoleEntry(ctx, req.Storage, roleName) 47 | if role == nil || err != nil { 48 | return logical.ErrorResponse(fmt.Sprintf("Role name '%s' not recognised", roleName)), nil 49 | } 50 | 51 | expiresAt := time.Now().UTC().Add(role.TokenTTL) 52 | b.Logger().Debug("generating access token for a role", "role_name", role.RoleName, "expires_at", expiresAt) 53 | 54 | pat, err := gc.CreateProjectAccessToken(&role.BaseTokenStorage, &expiresAt) 55 | if err != nil { 56 | return logical.ErrorResponse("Failed to create a token - " + err.Error()), nil 57 | } 58 | 59 | return &logical.Response{Data: tokenDetails(pat)}, nil 60 | } 61 | 62 | // Set up the paths for the roles within vault. 63 | func pathRoleToken(b *GitlabBackend) []*framework.Path { 64 | paths := []*framework.Path{ 65 | { 66 | Pattern: fmt.Sprintf("%s/%s", pathPatternToken, framework.GenericNameRegex("role_name")), 67 | Fields: roleTokenSchema, 68 | ExistenceCheck: b.pathTokenExistenceCheck(), 69 | Operations: map[logical.Operation]framework.OperationHandler{ 70 | logical.CreateOperation: &framework.PathOperation{ 71 | 72 | Callback: b.pathRoleTokenCreate, 73 | Summary: "Create a project access token based on a predefined role", 74 | Examples: roleTokenExamples, 75 | }, 76 | logical.UpdateOperation: &framework.PathOperation{ 77 | Callback: b.pathRoleTokenCreate, 78 | }, 79 | }, 80 | HelpSynopsis: pathRoleTokenHelpSyn, 81 | HelpDescription: pathRoleTokenHelpDesc, 82 | }, 83 | } 84 | 85 | return paths 86 | } 87 | 88 | //nolint:gosec 89 | const pathRoleTokenHelpSyn = `Generate a project access token for a given project based on a predefined role` 90 | const pathRoleTokenHelpDesc = ` 91 | This path allows you to generate a project access token based on a predefined role. You must create a role beforehand in /roles/ path, 92 | whose parameters are used to generate a project access token. 93 | ` 94 | 95 | var roleTokenExamples = []framework.RequestExample{ 96 | { 97 | Description: "Create a project access token based on a predefined role", 98 | Data: map[string]interface{}{ 99 | "role_name": "MyRole", 100 | }, 101 | }, 102 | } 103 | -------------------------------------------------------------------------------- /plugin/path_token_role_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitlabtoken 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "testing" 21 | 22 | "github.com/hashicorp/vault/sdk/logical" 23 | "github.com/stretchr/testify/assert" 24 | "github.com/stretchr/testify/require" 25 | gitlab "gitlab.com/gitlab-org/api/client-go" 26 | ) 27 | 28 | //nolint:funlen 29 | func TestAccRoleToken(t *testing.T) { 30 | t.Parallel() 31 | 32 | if testing.Short() { 33 | t.Skip("skipping integration test (short)") 34 | } 35 | 36 | req, backend := newGitlabAccEnv(t) 37 | 38 | ID := envAsInt("GITLAB_PROJECT_ID", 1) 39 | 40 | t.Run("successfully create", func(t *testing.T) { 41 | t.Parallel() 42 | 43 | data := map[string]interface{}{ 44 | "id": ID, 45 | "name": "vault-role-test", 46 | "scopes": []string{"read_api"}, 47 | } 48 | roleName := "successful" 49 | mustRoleCreate(t, backend, req.Storage, roleName, data) 50 | resp, err := testIssueRoleToken(t, backend, req, roleName, nil) 51 | require.NoError(t, err) 52 | require.False(t, resp.IsError()) 53 | 54 | assert.NotEmpty(t, resp.Data["token"], "no token returned") 55 | assert.NotEmpty(t, resp.Data["id"], "no id returned") 56 | assert.NotEmpty(t, resp.Data["access_level"], "no access_level returned") 57 | assert.NotEmpty(t, resp.Data["expires_at"], "default is 1d for expires_at") 58 | 59 | // check for default value 60 | assert.Equal(t, gitlab.AccessLevelValue(40), resp.Data["access_level"]) 61 | }) 62 | 63 | t.Run("successfully create token for role with access level", func(t *testing.T) { 64 | t.Parallel() 65 | 66 | data := map[string]interface{}{ 67 | "id": ID, 68 | "name": "vault-role-test-access-level", 69 | "access_level": 30, 70 | "scopes": []string{"read_api"}, 71 | } 72 | roleName := "successful-access-level" 73 | mustRoleCreate(t, backend, req.Storage, roleName, data) 74 | resp, err := testIssueRoleToken(t, backend, req, roleName, nil) 75 | require.NoError(t, err) 76 | require.False(t, resp.IsError()) 77 | 78 | assert.NotEmpty(t, resp.Data["token"], "no token returned") 79 | assert.NotEmpty(t, resp.Data["id"], "no id returned") 80 | assert.NotEmpty(t, resp.Data["access_level"], "no access_level returned") 81 | assert.NotEmpty(t, resp.Data["expires_at"], "default is 1d for expires_at") 82 | 83 | assert.Equal(t, gitlab.AccessLevelValue(30), resp.Data["access_level"]) 84 | }) 85 | 86 | t.Run("non-existing role", func(t *testing.T) { 87 | t.Parallel() 88 | 89 | resp, err := testIssueRoleToken(t, backend, req, "non-existing", nil) 90 | require.NoError(t, err) 91 | require.True(t, resp.IsError()) 92 | }) 93 | } 94 | 95 | // Create the token given a role name. 96 | func testIssueRoleToken(t *testing.T, b logical.Backend, req *logical.Request, roleName string, data map[string]interface{}) (*logical.Response, error) { 97 | t.Helper() 98 | 99 | req.Operation = logical.CreateOperation 100 | req.Path = fmt.Sprintf("%s/%s", pathPatternToken, roleName) 101 | req.Data = data 102 | 103 | resp, err := b.HandleRequest(context.Background(), req) 104 | 105 | return resp, err 106 | } 107 | -------------------------------------------------------------------------------- /plugin/role.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitlabtoken 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "time" 22 | 23 | "github.com/hashicorp/go-multierror" 24 | "github.com/hashicorp/vault/sdk/framework" 25 | "github.com/hashicorp/vault/sdk/helper/locksutil" 26 | "github.com/hashicorp/vault/sdk/logical" 27 | ) 28 | 29 | // RoleStorageEntry represents the role metadata stored in vault. 30 | type RoleStorageEntry struct { 31 | // `json:"" structs:"" mapstructure:""` 32 | RoleName string `json:"role_name" structs:"role_name" mapstructure:"role_name"` 33 | // The TTL for your token 34 | TokenTTL time.Duration `json:"token_ttl" structs:"token_ttl" mapstructure:"token_ttl"` 35 | BaseTokenStorage BaseTokenStorageEntry 36 | } 37 | 38 | func (role *RoleStorageEntry) assertValid(maxTTL time.Duration, allowOwnerLevel bool) error { 39 | var err *multierror.Error 40 | 41 | e := role.BaseTokenStorage.assertValid(allowOwnerLevel) 42 | if e != nil { 43 | err = multierror.Append(err, e) 44 | } 45 | 46 | if maxTTL > time.Duration(0) { 47 | if role.TokenTTL > maxTTL { 48 | errMsg := fmt.Sprintf("Requested token ttl '%v' exceeds configured maximum ttl of '%v's. ", 49 | role.TokenTTL, int64(maxTTL/time.Second)) 50 | err = multierror.Append(err, errors.New(errMsg)) 51 | } 52 | } 53 | 54 | return err.ErrorOrNil() 55 | } 56 | 57 | func (role *RoleStorageEntry) retrieve(data *framework.FieldData) { 58 | role.BaseTokenStorage.retrieve(data) 59 | 60 | ttlRaw, ok := data.GetOk("token_ttl") 61 | 62 | ttl, _ := ttlRaw.(int) 63 | if ok && ttl > 0 { 64 | role.TokenTTL = time.Duration(ttl) * time.Second 65 | } else if role.TokenTTL == time.Duration(0) { 66 | tokenTTL, _ := roleSchema["token_ttl"].Default.(int) 67 | role.TokenTTL = time.Duration(tokenTTL) * time.Second 68 | } 69 | } 70 | 71 | // save saves a role to storage. 72 | func (role *RoleStorageEntry) save(ctx context.Context, storage logical.Storage) error { 73 | entry, err := logical.StorageEntryJSON(fmt.Sprintf("%s/%s", pathPatternRoles, role.RoleName), role) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | return storage.Put(ctx, entry) 79 | } 80 | 81 | // Get or create the basic lock for the role name. 82 | func (b *GitlabBackend) roleLock(roleName string) *locksutil.LockEntry { 83 | return locksutil.LockForKey(b.roleLocks, roleName) 84 | } 85 | 86 | // deleteRoleEntry will remove the role with specified name from storage. 87 | func deleteRoleEntry(ctx context.Context, storage logical.Storage, roleName string) error { 88 | if roleName == "" { 89 | return errors.New("missing role name") 90 | } 91 | 92 | return storage.Delete(ctx, fmt.Sprintf("%s/%s", pathPatternRoles, roleName)) 93 | } 94 | 95 | // getRoleEntry fetches a role from the storage. 96 | func getRoleEntry(ctx context.Context, storage logical.Storage, roleName string) (*RoleStorageEntry, error) { 97 | var result RoleStorageEntry 98 | 99 | entry, err := storage.Get(ctx, fmt.Sprintf("%s/%s", pathPatternRoles, roleName)) 100 | if err != nil { 101 | return nil, err 102 | } else if entry == nil { 103 | return nil, nil 104 | } 105 | 106 | err = entry.DecodeJSON(&result) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | return &result, nil 112 | } 113 | 114 | // listRoleEntries gets all the roles. 115 | func listRoleEntries(ctx context.Context, storage logical.Storage) ([]string, error) { 116 | roles, err := storage.List(ctx, pathPatternRoles+"/") 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | return roles, nil 122 | } 123 | -------------------------------------------------------------------------------- /plugin/token.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitlabtoken 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "time" 21 | 22 | "github.com/hashicorp/go-multierror" 23 | "github.com/hashicorp/vault/sdk/framework" 24 | ) 25 | 26 | var errAccessLevelInvalid = errors.New("invalid access level") 27 | var errAccessLevelNotPermitted = errors.New("access level not permitted") 28 | 29 | // TokenStorageEntry represents the token metadata stored in vault. 30 | type TokenStorageEntry struct { 31 | BaseTokenStorage BaseTokenStorageEntry 32 | ExpiresAt *time.Time `json:"expires_at" structs:"expires_at" mapstructure:"expires_at,omitempty"` 33 | } 34 | 35 | // BaseTokenStorageEntry represents the base token metadata stored in vault. 36 | type BaseTokenStorageEntry struct { 37 | // `json:"" structs:"" mapstructure:""` 38 | ID int `json:"id" structs:"id" mapstructure:"id"` 39 | Name string `json:"name" structs:"name" mapstructure:"name"` 40 | Scopes []string `json:"scopes" structs:"scopes" mapstructure:"scopes"` 41 | AccessLevel int `json:"access_level" structs:"access_level" mapstructure:"access_level,omitempty"` 42 | } 43 | 44 | func (tokenStorage *TokenStorageEntry) assertValid(maxTTL time.Duration, allowOwnerLevel bool) error { 45 | var err *multierror.Error 46 | 47 | e := tokenStorage.BaseTokenStorage.assertValid(allowOwnerLevel) 48 | if e != nil { 49 | err = multierror.Append(err, e) 50 | } 51 | 52 | if maxTTL > time.Duration(0) && tokenStorage.ExpiresAt != nil { 53 | maxExpiresAt := time.Now().UTC().Add(maxTTL) 54 | if maxExpiresAt.Before(*tokenStorage.ExpiresAt) { 55 | errMsg := fmt.Sprintf("Requested expires_at '%v' exceeds configured maximum ttl of '%v's. Expires at or before '%v'", 56 | *tokenStorage.ExpiresAt, int64(maxTTL/time.Second), maxExpiresAt) 57 | err = multierror.Append(err, errors.New(errMsg)) 58 | } 59 | } 60 | 61 | return err.ErrorOrNil() 62 | } 63 | 64 | func (baseTokenStorage *BaseTokenStorageEntry) assertValid(allowOwnerLevel bool) error { 65 | var err *multierror.Error 66 | if baseTokenStorage.ID <= 0 { 67 | err = multierror.Append(err, errors.New("id is empty or invalid")) 68 | } 69 | 70 | if baseTokenStorage.Name == "" { 71 | err = multierror.Append(err, errors.New("name is empty")) 72 | } 73 | 74 | if len(baseTokenStorage.Scopes) == 0 { 75 | err = multierror.Append(err, errors.New("scopes are empty")) 76 | } else { 77 | e := validateScopes(baseTokenStorage.Scopes) 78 | if e != nil { 79 | err = multierror.Append(err, e) 80 | } 81 | } 82 | 83 | // check validity of access level. allowed values are: 84 | // 0(zero value), 10, 20, 30, 40 and 50 (when allowOwnerLevel flag is set) 85 | maxAllowedAccessLevel := 40 86 | if allowOwnerLevel { 87 | maxAllowedAccessLevel = 50 88 | } 89 | 90 | requestedAccessLevel := baseTokenStorage.AccessLevel 91 | 92 | if requestedAccessLevel%10 != 0 { 93 | err = multierror.Append(err, errAccessLevelInvalid) 94 | } else if requestedAccessLevel > maxAllowedAccessLevel || requestedAccessLevel < 0 { 95 | err = multierror.Append(err, errAccessLevelNotPermitted) 96 | } 97 | 98 | return err.ErrorOrNil() 99 | } 100 | 101 | func (tokenStorage *TokenStorageEntry) retrieve(data *framework.FieldData) { 102 | tokenStorage.BaseTokenStorage.retrieve(data) 103 | 104 | if expiresAtRaw, ok := data.GetOk("expires_at"); ok { 105 | t, _ := expiresAtRaw.(time.Time) 106 | tokenStorage.ExpiresAt = &t 107 | } 108 | } 109 | 110 | func (baseTokenStorage *BaseTokenStorageEntry) retrieve(data *framework.FieldData) { 111 | if idRaw, ok := data.GetOk("id"); ok { 112 | baseTokenStorage.ID, _ = idRaw.(int) 113 | } 114 | 115 | if nameRaw, ok := data.GetOk("name"); ok { 116 | baseTokenStorage.Name, _ = nameRaw.(string) 117 | } 118 | 119 | if scopesRaw, ok := data.GetOk("scopes"); ok { 120 | baseTokenStorage.Scopes, _ = scopesRaw.([]string) 121 | } 122 | 123 | if accessLevelRaw, ok := data.GetOk("access_level"); ok { 124 | baseTokenStorage.AccessLevel, _ = accessLevelRaw.(int) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /plugin/path_token_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitlabtoken 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "testing" 21 | "time" 22 | 23 | "github.com/hashicorp/vault/sdk/logical" 24 | "github.com/stretchr/testify/assert" 25 | "github.com/stretchr/testify/require" 26 | gitlab "gitlab.com/gitlab-org/api/client-go" 27 | ) 28 | 29 | //nolint:funlen 30 | func TestAccToken(t *testing.T) { 31 | t.Parallel() 32 | 33 | if testing.Short() { 34 | t.Skip("skipping integration test (short)") 35 | } 36 | 37 | req, backend := newGitlabAccEnv(t) 38 | 39 | ID := envAsInt("GITLAB_PROJECT_ID", 1) 40 | 41 | t.Run("successfully create", func(t *testing.T) { 42 | t.Parallel() 43 | 44 | d := map[string]interface{}{ 45 | "id": ID, 46 | "name": "vault-test", 47 | "scopes": []string{"read_api"}, 48 | } 49 | resp, err := testIssueToken(t, backend, req, d) 50 | require.NoError(t, err) 51 | fmt.Println(resp.Error()) //nolint:forbidigo 52 | require.False(t, resp.IsError()) 53 | 54 | assert.NotEmpty(t, resp.Data["token"], "no token returned") 55 | assert.NotEmpty(t, resp.Data["id"], "no id returned") 56 | assert.Empty(t, resp.Data["expires_at"], "default is never(nil) for expires_at") 57 | }) 58 | 59 | t.Run("successfully create with expiration", func(t *testing.T) { 60 | t.Parallel() 61 | 62 | e := time.Now().Add(time.Hour * 24) 63 | d := map[string]interface{}{ 64 | "id": ID, 65 | "name": "vault-test-expires", 66 | "scopes": []string{"read_api"}, 67 | "expires_at": e.Unix(), 68 | } 69 | resp, err := testIssueToken(t, backend, req, d) 70 | require.NoError(t, err) 71 | require.False(t, resp.IsError()) 72 | 73 | assert.NotEmpty(t, resp.Data["token"], "no token returned") 74 | assert.NotEmpty(t, resp.Data["id"], "no id returned") 75 | expiresAt, _ := resp.Data["expires_at"].(time.Time) 76 | assert.Contains(t, expiresAt, e.Format("2006-01-02")) 77 | }) 78 | 79 | t.Run("successfully create with access level", func(t *testing.T) { 80 | t.Parallel() 81 | 82 | e := time.Now().Add(time.Hour * 24) 83 | d := map[string]interface{}{ 84 | "id": ID, 85 | "name": "vault-test-access-level", 86 | "scopes": []string{"read_api"}, 87 | "access_level": 30, 88 | "expires_at": e.Unix(), 89 | } 90 | resp, err := testIssueToken(t, backend, req, d) 91 | require.NoError(t, err) 92 | require.False(t, resp.IsError()) 93 | 94 | assert.NotEmpty(t, resp.Data["token"], "no token returned") 95 | assert.NotEmpty(t, resp.Data["id"], "no id returned") 96 | assert.NotEmpty(t, resp.Data["access_level"], "no access_level returned") 97 | expiresAt, _ := resp.Data["expires_at"].(time.Time) 98 | assert.Contains(t, expiresAt.String(), e.Format("2006-01-02")) 99 | 100 | assert.Equal(t, gitlab.AccessLevelValue(30), resp.Data["access_level"]) 101 | }) 102 | 103 | t.Run("validation failure", func(t *testing.T) { 104 | t.Parallel() 105 | 106 | d := map[string]interface{}{ 107 | "id": -1, 108 | } 109 | resp, err := testIssueToken(t, backend, req, d) 110 | require.NoError(t, err) 111 | require.True(t, resp.IsError()) 112 | 113 | require.Contains(t, resp.Data["error"], "id is empty or invalid") 114 | require.Contains(t, resp.Data["error"], "name is empty") 115 | require.Contains(t, resp.Data["error"], "scopes are empty") 116 | }) 117 | 118 | t.Run("exceeding max token lifetime", func(t *testing.T) { 119 | t.Parallel() 120 | 121 | conf := map[string]interface{}{ 122 | "max_ttl": fmt.Sprintf("%dh", 7*24), // 7 days 123 | } 124 | 125 | testConfigUpdate(t, backend, req.Storage, conf) 126 | 127 | e := time.Now().Add(time.Hour * 14 * 24) 128 | d := map[string]interface{}{ 129 | "id": ID, 130 | "name": "vault-test-exceeding-lifetime", 131 | "scopes": []string{"read_api"}, 132 | "expires_at": e.Unix(), 133 | } 134 | resp, err := testIssueToken(t, backend, req, d) 135 | require.NoError(t, err) 136 | require.True(t, resp.IsError()) 137 | }) 138 | } 139 | 140 | // Create the token given the parameters. 141 | func testIssueToken(t *testing.T, b logical.Backend, req *logical.Request, data map[string]interface{}) (*logical.Response, error) { 142 | t.Helper() 143 | 144 | req.Operation = logical.CreateOperation 145 | req.Path = pathPatternToken 146 | req.Data = data 147 | 148 | resp, err := b.HandleRequest(context.Background(), req) 149 | 150 | return resp, err 151 | } 152 | -------------------------------------------------------------------------------- /plugin/path_token.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitlabtoken 16 | 17 | import ( 18 | "context" 19 | "time" 20 | 21 | "github.com/hashicorp/vault/sdk/framework" 22 | "github.com/hashicorp/vault/sdk/logical" 23 | ) 24 | 25 | // Schema for the token. This will map the fields coming in from the 26 | // vault request field map. 27 | var accessTokenSchema = map[string]*framework.FieldSchema{ 28 | "id": { 29 | Type: framework.TypeInt, 30 | Description: "Project ID to create a project access token for", 31 | }, 32 | "name": { 33 | Type: framework.TypeString, 34 | Description: "The name of the project access token", 35 | }, 36 | "scopes": { 37 | Type: framework.TypeCommaStringSlice, 38 | Description: "List of scopes", 39 | }, 40 | "expires_at": { 41 | Type: framework.TypeTime, 42 | Description: "The token expires at midnight UTC on that date", 43 | }, 44 | "access_level": { 45 | Type: framework.TypeInt, 46 | Description: "access level of project access token", 47 | }, 48 | } 49 | 50 | func tokenDetails(pat *PAT) map[string]interface{} { 51 | d := map[string]interface{}{ 52 | "token": pat.Token, 53 | "id": pat.ID, 54 | "name": pat.Name, 55 | "scopes": pat.Scopes, 56 | "access_level": pat.AccessLevel, 57 | } 58 | 59 | if pat.ExpiresAt != nil { 60 | d["expires_at"] = time.Time(*pat.ExpiresAt) 61 | } 62 | 63 | return d 64 | } 65 | 66 | func (b *GitlabBackend) pathTokenCreate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 67 | gc, err := b.getClient(ctx, req.Storage) 68 | if err != nil { 69 | return logical.ErrorResponse("failed to obtain gitlab client - %s", err.Error()), nil 70 | } 71 | 72 | var tokenStorage TokenStorageEntry 73 | tokenStorage.retrieve(data) 74 | 75 | config, err := getConfig(ctx, req.Storage) 76 | if err != nil { 77 | return logical.ErrorResponse("failed to obtain GitLab config - %s", err.Error()), nil 78 | } 79 | 80 | if config == nil { 81 | return logical.ErrorResponse("GitLab backend configuration has not been set up"), nil 82 | } 83 | 84 | err = tokenStorage.assertValid(config.MaxTTL, config.AllowOwnerLevel) 85 | if err != nil { 86 | return logical.ErrorResponse("Failed to validate - " + err.Error()), nil 87 | } 88 | 89 | b.Logger().Debug("generating access token", "id", tokenStorage.BaseTokenStorage.ID, 90 | "name", tokenStorage.BaseTokenStorage.Name, "scopes", tokenStorage.BaseTokenStorage.Scopes) 91 | 92 | pat, err := gc.CreateProjectAccessToken(&tokenStorage.BaseTokenStorage, tokenStorage.ExpiresAt) 93 | if err != nil { 94 | return logical.ErrorResponse("Failed to create a token - " + err.Error()), nil 95 | } 96 | 97 | return &logical.Response{Data: tokenDetails(pat)}, nil 98 | } 99 | 100 | // There is a correctness check that verifies there is an ExistenceFunc for all 101 | // the paths that have a CreateOperation, so we must define a stub one to pass 102 | // that if needed. 103 | func (b *GitlabBackend) pathTokenExistenceCheck() framework.ExistenceFunc { 104 | return func(_ context.Context, _ *logical.Request, _ *framework.FieldData) (bool, error) { 105 | return false, nil 106 | } 107 | } 108 | 109 | // Set up the paths for the roles within vault. 110 | func pathToken(b *GitlabBackend) []*framework.Path { 111 | paths := []*framework.Path{ 112 | { 113 | Pattern: pathPatternToken, 114 | Fields: accessTokenSchema, 115 | ExistenceCheck: b.pathTokenExistenceCheck(), 116 | Operations: map[logical.Operation]framework.OperationHandler{ 117 | logical.CreateOperation: &framework.PathOperation{ 118 | 119 | Callback: b.pathTokenCreate, 120 | Summary: "Create a project access token", 121 | Examples: tokenExamples, 122 | }, 123 | logical.UpdateOperation: &framework.PathOperation{ 124 | Callback: b.pathTokenCreate, 125 | }, 126 | }, 127 | HelpSynopsis: pathTokenHelpSyn, 128 | HelpDescription: pathTokenHelpDesc, 129 | }, 130 | } 131 | 132 | return paths 133 | } 134 | 135 | //nolint:gosec 136 | const pathTokenHelpSyn = `Generate a project access token for a given project with token name, scopes.` 137 | const pathTokenHelpDesc = ` 138 | This path allows you to generate a project access token. You must supply a project id to generate a token for, a name, which 139 | will be used as a name field in Gitlab, and scopes for the generated project access token. 140 | ` 141 | 142 | var tokenExamples = []framework.RequestExample{ 143 | { 144 | Description: "Create a project access token", 145 | Data: map[string]interface{}{ 146 | "id": 1, 147 | "name": "MyProjectAccessToken", 148 | "scopes": []string{"read_api", "read_repository"}, 149 | }, 150 | }, 151 | } 152 | -------------------------------------------------------------------------------- /plugin/path_config_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | package gitlabtoken 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/hashicorp/vault/sdk/logical" 22 | "github.com/stretchr/testify/assert" 23 | "github.com/stretchr/testify/require" 24 | ) 25 | 26 | //nolint:funlen 27 | func TestConfig(t *testing.T) { 28 | t.Parallel() 29 | 30 | t.Run("successful", func(t *testing.T) { 31 | t.Parallel() 32 | 33 | backend, reqStorage := getTestBackend(t, true) 34 | 35 | testConfigRead(t, backend, reqStorage, nil) 36 | 37 | conf := map[string]interface{}{ 38 | "base_url": "https://my.gitlab.com", 39 | "token": "mytoken", 40 | } 41 | 42 | testConfigUpdate(t, backend, reqStorage, conf, NoTTLWarning("max_ttl")) 43 | 44 | expected := map[string]interface{}{ 45 | "base_url": "https://my.gitlab.com", 46 | "max_ttl": int64(0), 47 | "allow_owner_level": false, 48 | } 49 | 50 | testConfigRead(t, backend, reqStorage, expected) 51 | 52 | conf["base_url"] = "https://another.gitlab.com" 53 | testConfigUpdate(t, backend, reqStorage, conf) 54 | 55 | expected["base_url"] = "https://another.gitlab.com" 56 | testConfigRead(t, backend, reqStorage, expected) 57 | }) 58 | 59 | t.Run("max ttl", func(t *testing.T) { 60 | t.Parallel() 61 | 62 | backend, reqStorage := getTestBackend(t, true) 63 | 64 | testConfigRead(t, backend, reqStorage, nil) 65 | 66 | conf := map[string]interface{}{ 67 | "base_url": "https://my.gitlab.com", 68 | "token": "mytoken", 69 | "max_ttl": fmt.Sprintf("%dh", 30*24), 70 | } 71 | 72 | testConfigUpdate(t, backend, reqStorage, conf) 73 | 74 | expected := map[string]interface{}{ 75 | "base_url": "https://my.gitlab.com", 76 | "max_ttl": int64(30 * 24 * 3600), 77 | "allow_owner_level": false, 78 | } 79 | 80 | testConfigRead(t, backend, reqStorage, expected) 81 | 82 | // Try seconds 83 | conf["max_ttl"] = fmt.Sprintf("%ds", 7*24*3600) 84 | testConfigUpdate(t, backend, reqStorage, conf) 85 | 86 | expected["max_ttl"] = int64(7 * 24 * 3600) 87 | testConfigRead(t, backend, reqStorage, expected) 88 | 89 | // Try less than 24 hours 90 | conf["max_ttl"] = fmt.Sprintf("%ds", 12*3600) 91 | testConfigUpdate(t, backend, reqStorage, conf, LT24HourTTLWarning("max_ttl")) 92 | 93 | testConfigRead(t, backend, reqStorage, expected) 94 | }) 95 | 96 | t.Run("allow_owner_level", func(t *testing.T) { 97 | t.Parallel() 98 | 99 | backend, reqStorage := getTestBackend(t, true) 100 | 101 | testConfigRead(t, backend, reqStorage, nil) 102 | 103 | // Validating allow_owner_token set to true 104 | 105 | conf := map[string]interface{}{ 106 | "base_url": "https://my.gitlab.com", 107 | "token": "mytoken", 108 | "max_ttl": fmt.Sprintf("%dh", 30*24), 109 | "allow_owner_level": true, 110 | } 111 | 112 | testConfigUpdate(t, backend, reqStorage, conf) 113 | 114 | expected := map[string]interface{}{ 115 | "base_url": "https://my.gitlab.com", 116 | "max_ttl": int64(30 * 24 * 3600), 117 | "allow_owner_level": true, 118 | } 119 | 120 | testConfigRead(t, backend, reqStorage, expected) 121 | 122 | // Validating allow_owner_token set to false 123 | 124 | conf["allow_owner_level"] = false 125 | expected["allow_owner_level"] = false 126 | 127 | testConfigUpdate(t, backend, reqStorage, conf) 128 | testConfigRead(t, backend, reqStorage, expected) 129 | }) 130 | } 131 | 132 | func testConfigUpdate(t *testing.T, b logical.Backend, s logical.Storage, d map[string]interface{}, warnings ...string) { 133 | t.Helper() 134 | 135 | resp, err := b.HandleRequest(context.Background(), &logical.Request{ 136 | Operation: logical.UpdateOperation, 137 | Path: pathPatternConfig, 138 | Data: d, 139 | Storage: s, 140 | }) 141 | require.NoError(t, err) 142 | require.False(t, resp.IsError()) 143 | 144 | for _, warning := range warnings { 145 | require.Contains(t, resp.Warnings, warning, "it should expect a warning", 146 | "expected_warning", warning, "actual_warnings", resp.Warnings) 147 | } 148 | } 149 | 150 | func testConfigRead(t *testing.T, b logical.Backend, s logical.Storage, expected map[string]interface{}) { 151 | t.Helper() 152 | 153 | resp, err := b.HandleRequest(context.Background(), &logical.Request{ 154 | Operation: logical.ReadOperation, 155 | Path: pathPatternConfig, 156 | Storage: s, 157 | }) 158 | 159 | require.NoError(t, err) 160 | 161 | if resp == nil && expected == nil { 162 | return 163 | } 164 | 165 | require.False(t, resp.IsError()) 166 | assert.Len(t, resp.Data, len(expected), "read data mismatch") 167 | assert.Equal(t, expected, resp.Data, "expected %v, actual: %v", expected, resp.Data) 168 | 169 | if t.Failed() { 170 | t.FailNow() 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/splunk/vault-plugin-secrets-gitlab 2 | 3 | go 1.25.0 4 | 5 | require ( 6 | github.com/hashicorp/go-multierror v1.1.1 7 | github.com/hashicorp/vault/api v1.22.0 8 | github.com/hashicorp/vault/sdk v0.20.0 9 | github.com/mitchellh/mapstructure v1.5.0 10 | github.com/stretchr/testify v1.11.1 11 | gitlab.com/gitlab-org/api/client-go v0.157.0 12 | ) 13 | 14 | require ( 15 | cloud.google.com/go/auth v0.17.0 // indirect 16 | cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect 17 | cloud.google.com/go/cloudsqlconn v1.18.1 // indirect 18 | cloud.google.com/go/compute/metadata v0.9.0 // indirect 19 | github.com/Microsoft/go-winio v0.6.2 // indirect 20 | github.com/armon/go-metrics v0.4.1 // indirect 21 | github.com/armon/go-radix v1.0.0 // indirect 22 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 23 | github.com/containerd/errdefs v1.0.0 // indirect 24 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 25 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 26 | github.com/distribution/reference v0.6.0 // indirect 27 | github.com/docker/docker v28.5.1+incompatible // indirect 28 | github.com/docker/go-connections v0.6.0 // indirect 29 | github.com/docker/go-units v0.5.0 // indirect 30 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 31 | github.com/fatih/color v1.18.0 // indirect 32 | github.com/felixge/httpsnoop v1.0.4 // indirect 33 | github.com/go-jose/go-jose/v4 v4.1.3 // indirect 34 | github.com/go-logr/logr v1.4.3 // indirect 35 | github.com/go-logr/stdr v1.2.2 // indirect 36 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 37 | github.com/golang/protobuf v1.5.4 // indirect 38 | github.com/golang/snappy v1.0.0 // indirect 39 | github.com/google/certificate-transparency-go v1.3.2 // indirect 40 | github.com/google/go-querystring v1.1.0 // indirect 41 | github.com/google/s2a-go v0.1.9 // indirect 42 | github.com/google/uuid v1.6.0 // indirect 43 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect 44 | github.com/googleapis/gax-go/v2 v2.15.0 // indirect 45 | github.com/hashicorp/errwrap v1.1.0 // indirect 46 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 47 | github.com/hashicorp/go-hclog v1.6.3 // indirect 48 | github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6 // indirect 49 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 50 | github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.1 // indirect 51 | github.com/hashicorp/go-kms-wrapping/v2 v2.0.19 // indirect 52 | github.com/hashicorp/go-metrics v0.5.4 // indirect 53 | github.com/hashicorp/go-plugin v1.7.0 // indirect 54 | github.com/hashicorp/go-retryablehttp v0.7.8 // indirect 55 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 56 | github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1 // indirect 57 | github.com/hashicorp/go-secure-stdlib/mlock v0.1.3 // indirect 58 | github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect 59 | github.com/hashicorp/go-secure-stdlib/permitpool v1.0.0 // indirect 60 | github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2 // indirect 61 | github.com/hashicorp/go-secure-stdlib/regexp v1.0.0 // indirect 62 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 63 | github.com/hashicorp/go-sockaddr v1.0.7 // indirect 64 | github.com/hashicorp/go-uuid v1.0.3 // indirect 65 | github.com/hashicorp/go-version v1.7.0 // indirect 66 | github.com/hashicorp/golang-lru v1.0.2 // indirect 67 | github.com/hashicorp/hcl v1.0.1-vault-7 // indirect 68 | github.com/hashicorp/yamux v0.1.2 // indirect 69 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 70 | github.com/jackc/pgconn v1.14.3 // indirect 71 | github.com/jackc/pgio v1.0.0 // indirect 72 | github.com/jackc/pgpassfile v1.0.0 // indirect 73 | github.com/jackc/pgproto3/v2 v2.3.3 // indirect 74 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 75 | github.com/jackc/pgtype v1.14.4 // indirect 76 | github.com/jackc/pgx/v4 v4.18.3 // indirect 77 | github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 // indirect 78 | github.com/mattn/go-colorable v0.1.14 // indirect 79 | github.com/mattn/go-isatty v0.0.20 // indirect 80 | github.com/mitchellh/copystructure v1.2.0 // indirect 81 | github.com/mitchellh/go-homedir v1.1.0 // indirect 82 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 83 | github.com/moby/docker-image-spec v1.3.1 // indirect 84 | github.com/oklog/run v1.2.0 // indirect 85 | github.com/opencontainers/go-digest v1.0.0 // indirect 86 | github.com/opencontainers/image-spec v1.1.1 // indirect 87 | github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 // indirect 88 | github.com/pierrec/lz4 v2.6.1+incompatible // indirect 89 | github.com/pkg/errors v0.9.1 // indirect 90 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 91 | github.com/robfig/cron/v3 v3.0.1 // indirect 92 | github.com/ryanuber/go-glob v1.0.0 // indirect 93 | github.com/sasha-s/go-deadlock v0.3.6 // indirect 94 | go.opencensus.io v0.24.0 // indirect 95 | go.opentelemetry.io/auto/sdk v1.2.1 // indirect 96 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect 97 | go.opentelemetry.io/otel v1.38.0 // indirect 98 | go.opentelemetry.io/otel/metric v1.38.0 // indirect 99 | go.opentelemetry.io/otel/trace v1.38.0 // indirect 100 | go.uber.org/atomic v1.11.0 // indirect 101 | golang.org/x/crypto v0.43.0 // indirect 102 | golang.org/x/exp v0.0.0-20251017212417-90e834f514db // indirect 103 | golang.org/x/net v0.46.0 // indirect 104 | golang.org/x/oauth2 v0.32.0 // indirect 105 | golang.org/x/sys v0.37.0 // indirect 106 | golang.org/x/text v0.30.0 // indirect 107 | golang.org/x/time v0.14.0 // indirect 108 | google.golang.org/api v0.252.0 // indirect 109 | google.golang.org/genproto/googleapis/rpc v0.0.0-20251020155222-88f65dc88635 // indirect 110 | google.golang.org/grpc v1.76.0 // indirect 111 | google.golang.org/protobuf v1.36.10 // indirect 112 | gopkg.in/yaml.v3 v3.0.1 // indirect 113 | ) 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vault Plugin for Gitlab Project Access Token 2 | 3 | [![build-status-badge]][actions-page] 4 | [![go-report-card-badge]][go-report-card] 5 | [![codecov-badge]][codecov] 6 | ![go-version-badge] 7 | 8 | This is a backend plugin to be used with Vault. This plugin generates [Gitlab Project Access Tokens][pat] 9 | 10 | - [Requirements](#requirements) 11 | - [Getting Started](#getting-started) 12 | - [Usage](#usage) 13 | - [Design Principles](#design-principles) 14 | - [Development](#development) 15 | - [Contribution](#contribution) 16 | - [License](#license) 17 | 18 | ## Requirements 19 | 20 | - Gitlab instance with **13.10** or later for API compatibility 21 | - You need **14.1** or later to have access level 22 | - Self-managed instances on Free and above. Or, GitLab SaaS Premium and above 23 | - a token of a user with maintainer or higher permission in a project 24 | 25 | - Lifting API rate limit for the user whose token will be used in this plugin to generate/revoke project access tokens. Admin of self-hosted can check [this doc][lift rate limit] to allow specific users to bypass authenticated request rate limiting. For SaaS Gitlab, I have not confirmed how to lift API limit yet. 26 | 27 | ## Getting Started 28 | 29 | This is a [Vault plugin] meant to work with Vault. This guide assumes you have already installed 30 | Vault and have a basic understanding of how Vault works. 31 | 32 | Otherwise, first read [how to get started with Vault][vault-getting-started]. 33 | 34 | To learn specifically about how plugins work, see documentation on [Vault 35 | plugins][vault plugin]. 36 | 37 | ### Usage 38 | 39 | ```sh 40 | # Please mount a plugin, then you can enable a secret 41 | $ vault secrets enable -path=gitlab vault-plugin-secrets-gitlab 42 | Success! Enabled the vault-plugin-secrets-gitlab secrets engine at: gitlab/ 43 | 44 | # configure the /config backend. You must supply a token which can generate project access tokens 45 | $ vault write gitlab/config base_url="https://gitlab.example.com" token=$GITLAB_TOKEN 46 | 47 | # see supported paths 48 | $ vault path-help gitlab/ 49 | $ vault path-help gitlab/config 50 | 51 | # generate an ephemeral gitlab token 52 | $ vault write gitlab/token id=1 name=ci-token scopes=api,write_repository 53 | Key Value 54 | --- ----- 55 | id 12345 56 | name ci-token 57 | scopes [api write_repository] 58 | token REDACTED_TOKEN 59 | 60 | # create a role 61 | $ vault write gitlab/roles/ci-role id=1 name=project1-role scopes=read_api,read_repository 62 | Key Value 63 | --- ----- 64 | role_name ci-role 65 | id 1 66 | name project1-role 67 | scopes [read_api read_repository] 68 | token_ttl 86400s 69 | 70 | # generate an ephemeral gitlab token for ci-role 71 | $ vault write gitlab/token/ci-role 72 | Key Value 73 | --- ----- 74 | id 12346 75 | name project1-role 76 | scopes [read_api read_repository] 77 | token REDACTED_TOKEN 78 | expires_at 2021-09-13 79 | ``` 80 | 81 | ## Design Principles 82 | 83 | The Gitlab Vault secrets plugin dynamically generates gitlab project access token based on passed parameters. This enables users to gain access to Gitlab projects without needing to create or manage project access tokens manually. 84 | 85 | You can find [detail design principles](docs/design-principles.md) 86 | 87 | ## Development 88 | 89 | ## Full dev environment 90 | 91 | To be coming... 92 | 93 | TODO: spin up a gitlab instance in docker 94 | 95 | ## Developing with an existing Gitlab instance 96 | 97 | Requirements: 98 | 99 | - vault 100 | 101 | ```sh 102 | # Build binary in plugins directory, and spin up dev vault 103 | make vault-only 104 | 105 | # In New Terminal 106 | export VAULT_ADDR=http://localhost:8200 107 | export GITLAB_URL="https://gitlab.example.com" 108 | export GITLAB_TOKEN=TOKEN 109 | 110 | 111 | # enable secrets backend and configuration 112 | ./scripts/setup_dev_vault.sh 113 | ``` 114 | 115 | You can then issue a project access following above usage. 116 | 117 | ### Tests 118 | 119 | ```sh 120 | # run unit tests 121 | make test 122 | 123 | # run subset of tests 124 | make test TESTARGS='-run=TestConfig' 125 | 126 | # run acceptance tests (uses Vault and Gitlab Docker containers against the compiled plugin) 127 | make acc-test 128 | 129 | # generate a code coverage report 130 | make report 131 | open coverage.html 132 | 133 | ``` 134 | 135 | ## Contribution 136 | 137 | This plugin was initially created as Hackathon project to enahance ephemeral credential suite. Another example is [vault-plugin-secrets-artifactory]. Contribution in a form of `issue`, `merge request` and donation will always be welcome. 138 | 139 | Please refer [CONTRIBUTING.md](CONTRIBUTING.md) and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) before contributing. 140 | 141 | ## License 142 | 143 | [Apache Software License version 2.0](LICENSE) 144 | 145 | [pat]: https://docs.gitlab.com/ee/user/project/settings/project_access_tokens.html 146 | [lift rate limit]: https://docs.gitlab.com/ee/user/admin_area/settings/user_and_ip_rate_limits.html#allow-specific-users-to-bypass-authenticated-request-rate-limiting 147 | [vault-plugin-secrets-artifactory]: https://github.com/splunk/vault-plugin-secrets-artifactory 148 | [vault plugin]:https://www.vaultproject.io/docs/internals/plugins.html 149 | [vault-getting-started]:https://www.vaultproject.io/intro/getting-started/install.html 150 | [actions-page]:https://github.com/splunk/vault-plugin-secrets-gitlab/actions 151 | [build-status-badge]:https://github.com/splunk/vault-plugin-secrets-gitlab/workflows/test.yml/badge.svg 152 | [codecov]:https://codecov.io/gh/splunk/vault-plugin-secrets-gitlab 153 | [codecov-badge]:https://codecov.io/gh/splunk/vault-plugin-secrets-gitlab/branch/main/graph/badge.svg 154 | [go-report-card]:https://goreportcard.com/report/github.com/splunk/vault-plugin-secrets-gitlab 155 | [go-report-card-badge]:https://goreportcard.com/badge/github.com/splunk/vault-plugin-secrets-gitlab 156 | [go-version-badge]:https://img.shields.io/github/go-mod/go-version/splunk/vault-plugin-secrets-gitlab 157 | -------------------------------------------------------------------------------- /plugin/path_config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitlabtoken 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "time" 22 | 23 | "github.com/hashicorp/vault/sdk/framework" 24 | "github.com/hashicorp/vault/sdk/logical" 25 | ) 26 | 27 | // NoTTLWarning returns a warning message for missing TTL for the provided ttl flag name. 28 | func NoTTLWarning(s string) string { 29 | return s + "is not set. Token can be generated with expiration 'never'" 30 | } 31 | 32 | // LT24HourTTLWarning returns a warning message for the provided TTL flag name if the TTL is < 24 hrs. 33 | func LT24HourTTLWarning(s string) string { 34 | return fmt.Sprintf("%[1]s is set with less than 24 hours. With current token expiry limitation, this %[1]s is ignored", s) 35 | } 36 | 37 | // Schema for the configuring Gitlab token plugin, this will map the fields coming in from the 38 | // vault request field map. 39 | var configSchema = map[string]*framework.FieldSchema{ 40 | "base_url": { 41 | Type: framework.TypeString, 42 | Description: `gitlab base url`, 43 | Default: "https://gitlab.com", 44 | }, 45 | "token": { 46 | Type: framework.TypeString, 47 | Description: `gitlab token that has permissions to generate project access tokens`, 48 | }, 49 | "max_ttl": { 50 | Type: framework.TypeDurationSecond, 51 | Description: `Maximum lifetime a generated token will be valid for. If <= 0, will use system default(0, never expire)`, 52 | Default: 0, 53 | }, 54 | "allow_owner_level": { 55 | Type: framework.TypeBool, 56 | Description: "allow to create roles with owner level access", 57 | Required: false, 58 | Default: false, 59 | }, 60 | } 61 | 62 | func configDetail(config *ConfigStorageEntry) map[string]interface{} { 63 | return map[string]interface{}{ 64 | "base_url": config.BaseURL, 65 | "max_ttl": int64(config.MaxTTL / time.Second), 66 | "allow_owner_level": config.AllowOwnerLevel, 67 | } 68 | } 69 | 70 | func (b *GitlabBackend) pathConfigRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { 71 | config, err := getConfig(ctx, req.Storage) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | if config == nil { 77 | return nil, nil 78 | } 79 | 80 | return &logical.Response{ 81 | Data: configDetail(config), 82 | }, nil 83 | } 84 | 85 | //nolint:gocyclo,cyclop,funlen 86 | func (b *GitlabBackend) pathConfigWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 87 | warnings := []string{} 88 | 89 | config, err := getConfig(ctx, req.Storage) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | if config == nil { 95 | config = &ConfigStorageEntry{} 96 | } 97 | 98 | baseURL, ok := data.GetOk("base_url") 99 | if ok { 100 | var valid bool 101 | 102 | config.BaseURL, valid = baseURL.(string) 103 | if !valid { 104 | return nil, errors.New("string type assertion failed for data field 'base_url'") 105 | } 106 | } else if config.BaseURL == "" { 107 | config.BaseURL, ok = configSchema["base_url"].Default.(string) 108 | if !ok { 109 | return nil, errors.New("string type assertion failed for data field 'base_url'") 110 | } 111 | } 112 | 113 | if token, ok := data.GetOk("token"); ok { 114 | config.Token, ok = token.(string) 115 | if !ok { 116 | return nil, errors.New("string type assertion failed for data field 'token'") 117 | } 118 | } 119 | 120 | maxTTLRaw, ok := data.GetOk("max_ttl") 121 | if ok { 122 | maxTTL, valid := maxTTLRaw.(int) 123 | if !valid { 124 | return nil, errors.New("int type assertion failed for data field 'max_ttl'") 125 | } 126 | 127 | // Until Gitlab implements granular token expiry. 128 | // bounce anything less than 24 hours 129 | if maxTTL > 0 && maxTTL < (24*3600) { 130 | warnings = append(warnings, LT24HourTTLWarning("max_ttl")) 131 | } else if maxTTL > 0 { 132 | config.MaxTTL = time.Duration(maxTTL) * time.Second 133 | } 134 | } 135 | 136 | if config.MaxTTL == 0 { 137 | warnings = append(warnings, NoTTLWarning("max_ttl")) 138 | } 139 | 140 | allowOwnerLevel, ok := data.GetOk("allow_owner_level") 141 | if ok { 142 | var valid bool 143 | 144 | config.AllowOwnerLevel, valid = allowOwnerLevel.(bool) 145 | if !valid { 146 | return nil, errors.New("bool type assertion failed for data field 'allow_owner_level'") 147 | } 148 | } 149 | 150 | // maxTTLRaw, ok := data.GetOk("max_ttl") 151 | // if ok && maxTTLRaw.(int) > 0 { 152 | // config.MaxTTL = time.Duration(maxTTLRaw.(int)) * time.Second 153 | // } else if config.MaxTTL == time.Duration(0) { 154 | // config.MaxTTL = time.Duration(configSchema["max_ttl"].Default.(int)) * time.Second 155 | // } 156 | 157 | entry, err := logical.StorageEntryJSON(pathPatternConfig, config) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | err = req.Storage.Put(ctx, entry) 163 | if err != nil { 164 | return nil, err 165 | } 166 | 167 | return &logical.Response{ 168 | Data: configDetail(config), 169 | Warnings: warnings, 170 | }, nil 171 | } 172 | 173 | func pathConfig(b *GitlabBackend) []*framework.Path { 174 | paths := []*framework.Path{ 175 | { 176 | Pattern: pathPatternConfig, 177 | Fields: configSchema, 178 | 179 | Operations: map[logical.Operation]framework.OperationHandler{ 180 | logical.ReadOperation: &framework.PathOperation{ 181 | Callback: b.pathConfigRead, 182 | }, 183 | logical.UpdateOperation: &framework.PathOperation{ 184 | Callback: b.pathConfigWrite, 185 | Examples: configExamples, 186 | }, 187 | }, 188 | 189 | HelpSynopsis: pathConfigHelpSyn, 190 | HelpDescription: pathConfigHelpDesc, 191 | }, 192 | } 193 | 194 | return paths 195 | } 196 | 197 | const pathConfigHelpSyn = ` 198 | Configure the Gitlab backend. 199 | ` 200 | 201 | const pathConfigHelpDesc = ` 202 | The Gitlab backend requires credentials for creating a project access token. 203 | This endpoint is used to configure those credentials as well as default values 204 | for the backend in general. 205 | ` 206 | 207 | var configExamples = []framework.RequestExample{ 208 | { 209 | Description: "Create/update backend configuration", 210 | Data: map[string]interface{}{ 211 | "base_url": "https://my.gitlab.com", 212 | "token": "MyPersonalAccessToken", 213 | "max_ttl": "168h", 214 | "allow_owner_level": true, 215 | }, 216 | }, 217 | } 218 | -------------------------------------------------------------------------------- /plugin/path_role.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitlabtoken 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "time" 22 | 23 | "github.com/hashicorp/vault/sdk/framework" 24 | "github.com/hashicorp/vault/sdk/logical" 25 | ) 26 | 27 | // Schema for the role, this will map the fields coming in from the 28 | // vault request field map. 29 | var roleSchema = map[string]*framework.FieldSchema{ 30 | "role_name": { 31 | Type: framework.TypeString, 32 | Description: "Role name", 33 | }, 34 | "id": { 35 | Type: framework.TypeInt, 36 | Description: "Project ID to create a project access token for", 37 | }, 38 | "name": { 39 | Type: framework.TypeString, 40 | Description: "The name of the project access token", 41 | }, 42 | "scopes": { 43 | Type: framework.TypeCommaStringSlice, 44 | Description: "List of scopes", 45 | }, 46 | "token_ttl": { 47 | Type: framework.TypeDurationSecond, 48 | Description: "The TTL of the token", 49 | Default: 24 * 3600, // 24 hours, until it hits midnight UTC 50 | }, 51 | "access_level": { 52 | Type: framework.TypeInt, 53 | Description: "access level of project access token", 54 | }, 55 | } 56 | 57 | func roleDetail(role *RoleStorageEntry) map[string]interface{} { 58 | return map[string]interface{}{ 59 | "role_name": role.RoleName, 60 | "id": role.BaseTokenStorage.ID, 61 | "name": role.BaseTokenStorage.Name, 62 | "scopes": role.BaseTokenStorage.Scopes, 63 | "access_level": role.BaseTokenStorage.AccessLevel, 64 | "token_ttl": int64(role.TokenTTL / time.Second), 65 | } 66 | } 67 | 68 | func (b *GitlabBackend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 69 | warnings := []string{} 70 | 71 | roleName, ok := data.Get("role_name").(string) 72 | if !ok { 73 | return nil, errors.New("string type assertion failed for data field 'role_name'") 74 | } 75 | 76 | if roleName == "" { 77 | return logical.ErrorResponse("Role name not supplied"), nil 78 | } 79 | 80 | lock := b.roleLock(roleName) 81 | 82 | lock.RLock() 83 | defer lock.RUnlock() 84 | 85 | role, err := getRoleEntry(ctx, req.Storage, roleName) 86 | if err != nil { 87 | return logical.ErrorResponse("Error reading role"), nil 88 | } 89 | 90 | if role == nil { 91 | role = &RoleStorageEntry{ 92 | RoleName: roleName, 93 | } 94 | } 95 | 96 | role.retrieve(data) 97 | 98 | config, err := getConfig(ctx, req.Storage) 99 | if err != nil { 100 | return logical.ErrorResponse("failed to obtain GitLab config - %s", err.Error()), nil 101 | } 102 | 103 | if config == nil { 104 | return logical.ErrorResponse("GitLab backend configuration has not been set up"), nil 105 | } 106 | 107 | err = role.assertValid(config.MaxTTL, config.AllowOwnerLevel) 108 | if err != nil { 109 | return logical.ErrorResponse("Failed to validate - " + err.Error()), nil 110 | } 111 | 112 | if role.TokenTTL == 0 { 113 | warnings = append(warnings, NoTTLWarning("token_ttl")) 114 | } 115 | 116 | err = role.save(ctx, req.Storage) 117 | if err != nil { 118 | return logical.ErrorResponse(err.Error()), nil 119 | } 120 | 121 | b.Logger().Debug("successfully create role", "role_name", roleName, "id", role.BaseTokenStorage.ID, 122 | "name", role.BaseTokenStorage.Name, "scopes", role.BaseTokenStorage.Scopes) 123 | 124 | return &logical.Response{ 125 | Data: roleDetail(role), 126 | Warnings: warnings, 127 | }, nil 128 | } 129 | 130 | func (b *GitlabBackend) pathRoleRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 131 | roleName, ok := data.Get("role_name").(string) 132 | if !ok { 133 | return nil, errors.New("string type assertion failed for data field 'role_name'") 134 | } 135 | 136 | role, err := getRoleEntry(ctx, req.Storage, roleName) 137 | if err != nil { 138 | return logical.ErrorResponse("Error reading role"), err 139 | } 140 | 141 | if role == nil { 142 | return nil, nil 143 | } 144 | 145 | return &logical.Response{ 146 | Data: roleDetail(role), 147 | }, nil 148 | } 149 | 150 | func (b *GitlabBackend) pathRoleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 151 | roleName, ok := data.Get("role_name").(string) 152 | if !ok { 153 | return nil, errors.New("string type assertion failed for data field 'role_name'") 154 | } 155 | 156 | if roleName == "" { 157 | return logical.ErrorResponse("Unable to remove, missing role name"), nil 158 | } 159 | 160 | lock := b.roleLock(roleName) 161 | 162 | lock.RLock() 163 | defer lock.RUnlock() 164 | 165 | // get the role to make sure it exists and to get the role id 166 | role, err := getRoleEntry(ctx, req.Storage, roleName) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | if role == nil { 172 | return nil, nil 173 | } 174 | 175 | err = deleteRoleEntry(ctx, req.Storage, roleName) 176 | if err != nil { 177 | return logical.ErrorResponse("Unable to remove role " + roleName), err 178 | } 179 | 180 | b.Logger().Debug("successfully deleted role", "role_name", roleName) 181 | 182 | return nil, nil 183 | } 184 | 185 | func (b *GitlabBackend) pathRoleList(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { 186 | roles, err := listRoleEntries(ctx, req.Storage) 187 | if err != nil { 188 | return logical.ErrorResponse("Error listing roles"), err 189 | } 190 | 191 | return logical.ListResponse(roles), nil 192 | } 193 | 194 | func (b *GitlabBackend) pathRoleExistenceCheck(fieldName string) framework.ExistenceFunc { 195 | return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) { 196 | roleName, ok := data.Get(fieldName).(string) 197 | if !ok { 198 | return false, fmt.Errorf("string type assertion failed for data field %q", fieldName) 199 | } 200 | 201 | role, err := getRoleEntry(ctx, req.Storage, roleName) 202 | if err != nil { 203 | return false, err 204 | } 205 | 206 | return role != nil, nil 207 | } 208 | } 209 | 210 | // Set up the paths for the roles within vault. 211 | func pathRole(b *GitlabBackend) []*framework.Path { 212 | paths := []*framework.Path{ 213 | { 214 | Pattern: fmt.Sprintf("%s/%s", pathPatternRoles, framework.GenericNameRegex("role_name")), 215 | Fields: roleSchema, 216 | ExistenceCheck: b.pathRoleExistenceCheck("role_name"), 217 | Operations: map[logical.Operation]framework.OperationHandler{ 218 | logical.CreateOperation: &framework.PathOperation{ 219 | Callback: b.pathRoleCreateUpdate, 220 | Summary: "Create a role", 221 | Examples: roleExamples, 222 | }, 223 | logical.UpdateOperation: &framework.PathOperation{ 224 | Callback: b.pathRoleCreateUpdate, 225 | }, 226 | logical.ReadOperation: &framework.PathOperation{ 227 | Callback: b.pathRoleRead, 228 | }, 229 | logical.DeleteOperation: &framework.PathOperation{ 230 | Callback: b.pathRoleDelete, 231 | }, 232 | }, 233 | HelpSynopsis: pathRoleHelpSyn, 234 | HelpDescription: pathRoleHelpDesc, 235 | }, 236 | } 237 | 238 | return paths 239 | } 240 | 241 | func pathRoleList(b *GitlabBackend) []*framework.Path { 242 | // Paths for listing role sets 243 | paths := []*framework.Path{ 244 | { 245 | Pattern: pathPatternRoles + "?/?", 246 | Callbacks: map[logical.Operation]framework.OperationFunc{ 247 | logical.ListOperation: b.pathRoleList, 248 | }, 249 | HelpSynopsis: pathListRoleHelpSyn, 250 | }, 251 | } 252 | 253 | return paths 254 | } 255 | 256 | const pathRoleHelpSyn = `Create a role with parameters that are used to generate a project access token.` 257 | const pathRoleHelpDesc = ` 258 | This path allows you to create a role whose parameters will be used to generate a project access token. 259 | You must supply a project id to generate a token for, a name, which will be used as a name field in Gitlab, 260 | and scopes for the generated project access token. 261 | ` 262 | 263 | var roleExamples = []framework.RequestExample{ 264 | { 265 | Description: "Create/update a role", 266 | Data: map[string]interface{}{ 267 | "role_name": "MyProject1ReadRole", 268 | "id": 1, 269 | "name": "MyProjectAccessToken", 270 | "scopes": []string{"read_api", "read_repository"}, 271 | }, 272 | }, 273 | } 274 | 275 | const pathListRoleHelpSyn = `List existing roles.` 276 | -------------------------------------------------------------------------------- /plugin/path_role_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Splunk Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gitlabtoken 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "testing" 21 | 22 | "github.com/hashicorp/vault/sdk/logical" 23 | "github.com/mitchellh/mapstructure" 24 | "github.com/stretchr/testify/assert" 25 | "github.com/stretchr/testify/require" 26 | ) 27 | 28 | //nolint:funlen 29 | func TestPathRole(t *testing.T) { 30 | a := assert.New(t) 31 | backend, storage := getTestBackend(t, false) 32 | 33 | conf := map[string]interface{}{ 34 | "base_url": "http://randomhost", 35 | "token": "gibberish", 36 | } 37 | testConfigUpdate(t, backend, storage, conf) 38 | 39 | data := map[string]interface{}{ 40 | "id": 1, 41 | "name": "role-test", 42 | "scopes": []string{"api", "read_repository"}, 43 | "access_level": 30, 44 | } 45 | 46 | t.Run("successful", func(t *testing.T) { 47 | roleName := "successful" 48 | resp, err := testRoleRead(t, backend, storage, roleName) 49 | require.NoError(t, err, "non-existing role should not return error") 50 | require.Nil(t, resp, "non-existing role should return nil response") 51 | 52 | mustRoleCreate(t, backend, storage, roleName, data) 53 | 54 | resp, err = testRoleRead(t, backend, storage, roleName) 55 | require.NoError(t, err, "existing role should not return error") 56 | require.False(t, resp.IsError()) 57 | 58 | a.Equal(roleName, resp.Data["role_name"]) 59 | a.Equal("role-test", resp.Data["name"]) 60 | a.Equal(1, resp.Data["id"]) 61 | a.Equal([]string{"api", "read_repository"}, resp.Data["scopes"]) 62 | a.Equal(30, resp.Data["access_level"]) 63 | 64 | mustRoleDelete(t, backend, storage, roleName) 65 | }) 66 | 67 | t.Run("successful with ttl", func(t *testing.T) { 68 | conf["max_ttl"] = fmt.Sprintf("%dh", 7*24) 69 | testConfigUpdate(t, backend, storage, conf) 70 | 71 | roleName := "successful-ttl" 72 | data["token_ttl"] = 3 * 24 * 3600 73 | mustRoleCreate(t, backend, storage, roleName, data) 74 | 75 | resp, err := testRoleRead(t, backend, storage, roleName) 76 | require.NoError(t, err, "existing role should not return error") 77 | require.False(t, resp.IsError()) 78 | 79 | a.Equal(int64(3*24*3600), resp.Data["token_ttl"]) 80 | 81 | mustRoleDelete(t, backend, storage, roleName) 82 | }) 83 | 84 | t.Run("delete non-existing", func(t *testing.T) { 85 | roleName := "non-existing" 86 | resp, err := testRoleDelete(t, backend, storage, roleName) 87 | require.NoError(t, err, "non-existing role should not return error") 88 | require.Nil(t, resp) 89 | }) 90 | 91 | t.Run("validation failure", func(t *testing.T) { 92 | roleName := "validation-failure" 93 | d := map[string]interface{}{ 94 | "id": -1, 95 | "token_ttl": fmt.Sprintf("%dh", 30*24), 96 | "access_level": 31, 97 | } 98 | resp, err := testRoleCreate(t, backend, storage, roleName, d) 99 | require.NoError(t, err) 100 | require.True(t, resp.IsError()) 101 | 102 | require.Contains(t, resp.Data["error"], "id is empty or invalid") 103 | require.Contains(t, resp.Data["error"], "name is empty") 104 | require.Contains(t, resp.Data["error"], "scopes are empty") 105 | require.Contains(t, resp.Data["error"], "exceeds configured maximum ttl") 106 | require.Contains(t, resp.Data["error"], "invalid access level") 107 | }) 108 | 109 | t.Run("validation of not allowed access level", func(t *testing.T) { 110 | roleName := "validation-not-allowed-access-level" 111 | d := map[string]interface{}{ 112 | "id": 1, 113 | "name": "role-test-not-allowed-access-level", 114 | "scopes": []string{"api", "read_repository"}, 115 | "access_level": 50, 116 | } 117 | resp, err := testRoleCreate(t, backend, storage, roleName, d) 118 | require.NoError(t, err) 119 | require.True(t, resp.IsError()) 120 | 121 | require.Contains(t, resp.Data["error"], "access level not permitted") 122 | }) 123 | } 124 | 125 | func TestPathRoleWithAllowOwnerAccessLevel(t *testing.T) { 126 | a := assert.New(t) 127 | backend, storage := getTestBackend(t, false) 128 | 129 | conf := map[string]interface{}{ 130 | "base_url": "http://randomhost", 131 | "token": "gibberish", 132 | "allow_owner_level": true, 133 | } 134 | testConfigUpdate(t, backend, storage, conf) 135 | 136 | data := map[string]interface{}{ 137 | "id": 1, 138 | "name": "role-test", 139 | "scopes": []string{"api", "read_repository"}, 140 | "access_level": 50, 141 | } 142 | 143 | t.Run("successful", func(t *testing.T) { 144 | roleName := "successful" 145 | resp, err := testRoleRead(t, backend, storage, roleName) 146 | require.NoError(t, err, "non-existing role should not return error") 147 | require.Nil(t, resp, "non-existing role should return nil response") 148 | 149 | mustRoleCreate(t, backend, storage, roleName, data) 150 | 151 | resp, err = testRoleRead(t, backend, storage, roleName) 152 | require.NoError(t, err, "existing role should not return error") 153 | require.False(t, resp.IsError()) 154 | 155 | a.Equal(roleName, resp.Data["role_name"]) 156 | a.Equal("role-test", resp.Data["name"]) 157 | a.Equal(1, resp.Data["id"]) 158 | a.Equal([]string{"api", "read_repository"}, resp.Data["scopes"]) 159 | a.Equal(50, resp.Data["access_level"]) 160 | 161 | mustRoleDelete(t, backend, storage, roleName) 162 | }) 163 | } 164 | 165 | func TestPathRoleList(t *testing.T) { 166 | t.Parallel() 167 | 168 | a := assert.New(t) 169 | 170 | backend, storage := getTestBackend(t, false) 171 | conf := map[string]interface{}{ 172 | "base_url": "http://randomhost", 173 | "token": "gibberish", 174 | } 175 | testConfigUpdate(t, backend, storage, conf) 176 | 177 | data := map[string]interface{}{ 178 | "id": 1, 179 | "name": "role-test", 180 | "scopes": []string{"api", "read_repository"}, 181 | } 182 | 183 | var listResp map[string]interface{} 184 | 185 | resp, err := testRoleList(t, backend, storage) 186 | require.NoError(t, err) 187 | require.False(t, resp.IsError()) 188 | 189 | err = mapstructure.Decode(resp.Data, &listResp) 190 | require.NoError(t, err) 191 | require.False(t, resp.IsError()) 192 | require.Empty(t, resp.Data, "no role to list should return nil data") 193 | 194 | roleName1 := "test_list_role1" 195 | roleName2 := "test_list_role2" 196 | 197 | mustRoleCreate(t, backend, storage, roleName1, data) 198 | mustRoleCreate(t, backend, storage, roleName2, data) 199 | 200 | resp, err = testRoleList(t, backend, storage) 201 | require.NoError(t, err) 202 | require.False(t, resp.IsError()) 203 | err = mapstructure.Decode(resp.Data, &listResp) 204 | require.NoError(t, err) 205 | 206 | returnedRoles, _ := listResp["keys"].([]string) 207 | a.Len(returnedRoles, 2, "incorrect number of roles") 208 | a.Equal(roleName1, returnedRoles[0], "incorrect path set") 209 | a.Equal(roleName2, returnedRoles[1], "incorrect path set") 210 | 211 | mustRoleDelete(t, backend, storage, roleName2) 212 | resp, err = testRoleList(t, backend, storage) 213 | require.NoError(t, err) 214 | require.False(t, resp.IsError()) 215 | err = mapstructure.Decode(resp.Data, &listResp) 216 | require.NoError(t, err) 217 | 218 | returnedRoles, _ = listResp["keys"].([]string) 219 | a.Len(returnedRoles, 1, "incorrect number of roles") 220 | a.Equal(roleName1, returnedRoles[0], "incorrect path set") 221 | } 222 | 223 | func testRoleCreate(t *testing.T, b logical.Backend, s logical.Storage, roleName string, data map[string]interface{}) (*logical.Response, error) { 224 | t.Helper() 225 | 226 | resp, err := b.HandleRequest(context.Background(), &logical.Request{ 227 | Operation: logical.CreateOperation, 228 | Path: fmt.Sprintf("%s/%s", pathPatternRoles, roleName), 229 | Data: data, 230 | Storage: s, 231 | }) 232 | 233 | return resp, err 234 | } 235 | 236 | func mustRoleCreate(t *testing.T, b logical.Backend, s logical.Storage, roleName string, data map[string]interface{}) { 237 | t.Helper() 238 | resp, err := testRoleCreate(t, b, s, roleName, data) 239 | require.NoError(t, err) 240 | require.False(t, resp.IsError()) 241 | } 242 | 243 | func testRoleRead(t *testing.T, b logical.Backend, s logical.Storage, roleName string) (*logical.Response, error) { 244 | t.Helper() 245 | 246 | data := map[string]interface{}{ 247 | "role_name": roleName, 248 | } 249 | 250 | resp, err := b.HandleRequest(context.Background(), &logical.Request{ 251 | Operation: logical.ReadOperation, 252 | Path: fmt.Sprintf("%s/%s", pathPatternRoles, roleName), 253 | Data: data, 254 | Storage: s, 255 | }) 256 | 257 | return resp, err 258 | } 259 | 260 | func testRoleDelete(t *testing.T, b logical.Backend, s logical.Storage, roleName string) (*logical.Response, error) { 261 | t.Helper() 262 | 263 | data := map[string]interface{}{ 264 | "role_name": roleName, 265 | } 266 | 267 | resp, err := b.HandleRequest(context.Background(), &logical.Request{ 268 | Operation: logical.DeleteOperation, 269 | Path: fmt.Sprintf("%s/%s", pathPatternRoles, roleName), 270 | Data: data, 271 | Storage: s, 272 | }) 273 | 274 | return resp, err 275 | } 276 | 277 | func mustRoleDelete(t *testing.T, b logical.Backend, s logical.Storage, roleName string) { 278 | t.Helper() 279 | 280 | resp, err := testRoleDelete(t, b, s, roleName) 281 | require.NoError(t, err) 282 | require.Nil(t, resp) 283 | } 284 | 285 | func testRoleList(t *testing.T, b logical.Backend, s logical.Storage) (*logical.Response, error) { 286 | t.Helper() 287 | 288 | resp, err := b.HandleRequest(context.Background(), &logical.Request{ 289 | Operation: logical.ListOperation, 290 | Path: pathPatternRoles, 291 | Storage: s, 292 | }) 293 | 294 | return resp, err 295 | } 296 | -------------------------------------------------------------------------------- /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 2021 Splunk Inc. 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 | 203 | ======================================================================= 204 | Vault Plugin Secrets for Gitlab: 205 | 206 | Vault Plugin Secrets for Gitlab project contains subcomponents with 207 | separate copyright notices and license terms. Your use of the source 208 | code for the these subcomponents is subject to the terms and conditions 209 | of the following licenses. 210 | 211 | ======================================================================== 212 | Apache License 2.0 213 | ======================================================================== 214 | The following components are provided under the Apache License 2.0. See project link for details. 215 | 216 | (Apache License 2.0) go-gitlab (https://gitlab.com/gitlab-org/api/client-go/-/blob/main/LICENSE) 217 | 218 | ======================================================================== 219 | MIT licenses 220 | ======================================================================== 221 | The following components are provided under the MIT License. See project link for details. 222 | 223 | (MIT License) go-glob (https://github.com/ryanuber/go-glob/blob/master/LICENSE) 224 | (MIT License) go-homedir (https://github.com/mitchellh/go-homedir/blob/master/LICENSE) 225 | (MIT License) mapstructure (https://github.com/mitchellh/mapstructure/blob/master/LICENSE) 226 | (MIT License) objx (https://github.com/stretchr/objx/blob/master/LICENSE) 227 | (MIT License) testify (https://github.com/stretchr/testify/blob/master/LICENSE) 228 | 229 | ======================================================================== 230 | BSD 3-Clause licenses 231 | ======================================================================== 232 | The following components are provided under the BSD 3-Clause. See project link for details. 233 | 234 | (BSD 3-Clause) snappy (https://github.com/golang/snappy/blob/master/LICENSE) 235 | (BSD 3-Clause) lz4 (https://github.com/pierrec/lz4/blob/master/LICENSE) 236 | (BSD 3-Clause) difflib (https://github.com/pmezard/go-difflib/blob/master/difflib/LICENSE) 237 | (BSD 3-Clause) go-querystring (https://github.com/google/go-querystring/blob/master/query/LICENSE) 238 | 239 | ======================================================================== 240 | MPL-2.0 licenses 241 | ======================================================================== 242 | The following components are provided under the MPL-2.0. See project link for details. 243 | 244 | (MPL-2.0) go-multierror (https://github.com/hashicorp/go-multierror/blob/master/LICENSE) 245 | (MPL-2.0) errwrap (https://github.com/hashicorp/errwrap/blob/master/LICENSE) 246 | (MPL-2.0) vault/api (https://github.com/hashicorp/vault/blob/master/api/LICENSE) 247 | (MPL-2.0) go-retryablehttp (https://github.com/hashicorp/go-retryablehttp/blob/master/LICENSE) 248 | (MPL-2.0) vault/sdk/helper (https://github.com/hashicorp/vault/blob/master/sdk/helper/LICENSE) 249 | (MPL-2.0) go-sockaddr (https://github.com/hashicorp/go-sockaddr/blob/master/LICENSE) 250 | (MPL-2.0) go-cleanhttp (https://github.com/hashicorp/go-cleanhttp/blob/master/LICENSE) 251 | (MPL-2.0) go-rootcerts (https://github.com/hashicorp/go-rootcerts/blob/master/LICENSE) 252 | (MPL-2.0) hcl (https://github.com/hashicorp/hcl/blob/master/LICENSE) 253 | 254 | ======================================================================== 255 | For the rest: 256 | ======================================================================== 257 | 258 | (ISC) spew (https://github.com/davecgh/go-spew/blob/master/spew/LICENSE) 259 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= 4 | cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= 5 | cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= 6 | cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= 7 | cloud.google.com/go/cloudsqlconn v1.18.1 h1:IIvs7QJ8eqKUUHSon13Joie9oH7/i7MJwNzBLG+FrhM= 8 | cloud.google.com/go/cloudsqlconn v1.18.1/go.mod h1:58bxZZ17Mz5D83ddMT8x6w56yKpcmVXyaOwGWkzGcMw= 9 | cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= 10 | cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= 11 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 12 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 13 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= 14 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 15 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 16 | github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= 17 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 18 | github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= 19 | github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 20 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 21 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 22 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 23 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 24 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 25 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 26 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 27 | github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= 28 | github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= 29 | github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= 30 | github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 31 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 32 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 33 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 34 | github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= 35 | github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= 36 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 37 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 38 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 39 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 40 | github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= 41 | github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= 42 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 43 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 44 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 45 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 46 | github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= 47 | github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 48 | github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= 49 | github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= 50 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 51 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 52 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 53 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 54 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 55 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 56 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 57 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 58 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 59 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 60 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 61 | github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= 62 | github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 63 | github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= 64 | github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= 65 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 66 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 67 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 68 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 69 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 70 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 71 | github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= 72 | github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= 73 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 74 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 75 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 76 | github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 77 | github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= 78 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 79 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 80 | github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= 81 | github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= 82 | github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= 83 | github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= 84 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 85 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 86 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 87 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 88 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 89 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 90 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 91 | github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= 92 | github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 93 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 94 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 95 | github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= 96 | github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= 97 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 98 | github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= 99 | github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 100 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 101 | github.com/gofrs/uuid v4.3.0+incompatible h1:CaSVZxm5B+7o45rtab4jC2G37WGYX1zQfuU2i6DSvnc= 102 | github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 103 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 104 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= 105 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 106 | github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= 107 | github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= 108 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 109 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 110 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 111 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 112 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 113 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 114 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 115 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 116 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 117 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 118 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 119 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 120 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 121 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 122 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 123 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 124 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 125 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 126 | github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= 127 | github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 128 | github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A= 129 | github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs= 130 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 131 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 132 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 133 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 134 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 135 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 136 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 137 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 138 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 139 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 140 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 141 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 142 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 143 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 144 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 145 | github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= 146 | github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 147 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 148 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 149 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 150 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= 151 | github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= 152 | github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= 153 | github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= 154 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= 155 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= 156 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 157 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 158 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 159 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 160 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 161 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 162 | github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= 163 | github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 164 | github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6 h1:kBoJV4Xl5FLtBfnBjDvBxeNSy2IRITSGs73HQsFUEjY= 165 | github.com/hashicorp/go-hmac-drbg v0.0.0-20210916214228-a6e5a68489f6/go.mod h1:y+HSOcOGB48PkUxNyLAiCiY6rEENu+E+Ss4LG8QHwf4= 166 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 167 | github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= 168 | github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 169 | github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.1 h1:KIge4FHZEDb2/xjaWgmBheCTgRL6HV4sgTfDsH876L8= 170 | github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.1/go.mod h1:aHO1EoFD0kBYLBedqxXgalfFT8lrWfP7kpuSoaqGjH0= 171 | github.com/hashicorp/go-kms-wrapping/v2 v2.0.19 h1:FX7HrkfkYomf4SlMrwzOP32FXuFltq34Qy/gXk1Tp5Y= 172 | github.com/hashicorp/go-kms-wrapping/v2 v2.0.19/go.mod h1:wpZygQlPUUGt4Klgg+RlCaq/KRe8XinEzqTf7QmvrNo= 173 | github.com/hashicorp/go-metrics v0.5.4 h1:8mmPiIJkTPPEbAiV97IxdAGNdRdaWwVap1BU6elejKY= 174 | github.com/hashicorp/go-metrics v0.5.4/go.mod h1:CG5yz4NZ/AI/aQt9Ucm/vdBnbh7fvmv4lxZ350i+QQI= 175 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 176 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 177 | github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= 178 | github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= 179 | github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= 180 | github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= 181 | github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= 182 | github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= 183 | github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 184 | github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1 h1:VaLXp47MqD1Y2K6QVrA9RooQiPyCgAbnfeJg44wKuJk= 185 | github.com/hashicorp/go-secure-stdlib/cryptoutil v0.1.1/go.mod h1:hH8rgXHh9fPSDPerG6WzABHsHF+9ZpLhRI1LPk4JZ8c= 186 | github.com/hashicorp/go-secure-stdlib/mlock v0.1.3 h1:kH3Rhiht36xhAfhuHyWJDgdXXEx9IIZhDGRk24CDhzg= 187 | github.com/hashicorp/go-secure-stdlib/mlock v0.1.3/go.mod h1:ov1Q0oEDjC3+A4BwsG2YdKltrmEw8sf9Pau4V9JQ4Vo= 188 | github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= 189 | github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= 190 | github.com/hashicorp/go-secure-stdlib/permitpool v1.0.0 h1:U6y5MXGiDVOOtkWJ6o/tu1TxABnI0yKTQWJr7z6BpNk= 191 | github.com/hashicorp/go-secure-stdlib/permitpool v1.0.0/go.mod h1:ecDb3o+8D4xtP0nTCufJaAVawHavy5M2eZ64Nq/8/LM= 192 | github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2 h1:gCNiM4T5xEc4IpT8vM50CIO+AtElr5kO9l2Rxbq+Sz8= 193 | github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.2/go.mod h1:6ZM4ZdwClyAsiU2uDBmRHCvq0If/03BMbF9U+U7G5pA= 194 | github.com/hashicorp/go-secure-stdlib/regexp v1.0.0 h1:08mz6j5MsCG9sf8tvC8Lhboe/ZMiNg41IPSh6unK5T4= 195 | github.com/hashicorp/go-secure-stdlib/regexp v1.0.0/go.mod h1:n/Gj3sYIEEOYds8uKS55bFf7XiYvWN4e+d+UOA7r/YU= 196 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= 197 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= 198 | github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= 199 | github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= 200 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 201 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 202 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 203 | github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= 204 | github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 205 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 206 | github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= 207 | github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 208 | github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= 209 | github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= 210 | github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0= 211 | github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= 212 | github.com/hashicorp/vault/sdk v0.20.0 h1:a4ulj2gICzw/qH0A4+6o36qAHxkUdcmgpMaSSjqE3dc= 213 | github.com/hashicorp/vault/sdk v0.20.0/go.mod h1:xEjAt/n/2lHBAkYiRPRmvf1d5B6HlisPh2pELlRCosk= 214 | github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= 215 | github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= 216 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 217 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 218 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 219 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 220 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 221 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 222 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 223 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 224 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 225 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 226 | github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= 227 | github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= 228 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 229 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 230 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 231 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 232 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 233 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 234 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 235 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 236 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 237 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 238 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 239 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 240 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 241 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 242 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 243 | github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= 244 | github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 245 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 246 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 247 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 248 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 249 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 250 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 251 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 252 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 253 | github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 254 | github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= 255 | github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= 256 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 257 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 258 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 259 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 260 | github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= 261 | github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= 262 | github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= 263 | github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= 264 | github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= 265 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 266 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 267 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 268 | github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= 269 | github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 270 | github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 271 | github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 272 | github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= 273 | github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= 274 | github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 h1:hgVxRoDDPtQE68PT4LFvNlPz2nBKd3OMlGKIQ69OmR4= 275 | github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531/go.mod h1:fqTUQpVYBvhCNIsMXGl2GE9q6z94DIP6NtFKXCSTVbg= 276 | github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d h1:J8tJzRyiddAFF65YVgxli+TyWBi0f79Sld6rJP6CBcY= 277 | github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d/go.mod h1:b+Q3v8Yrg5o15d71PSUraUzYb+jWl6wQMSBXSGS/hv0= 278 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 279 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 280 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 281 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 282 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 283 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 284 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 285 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 286 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 287 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 288 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 289 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 290 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 291 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 292 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 293 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 294 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 295 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 296 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 297 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 298 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 299 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 300 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 301 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 302 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 303 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 304 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 305 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 306 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 307 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 308 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 309 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 310 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 311 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 312 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 313 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 314 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 315 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 316 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 317 | github.com/microsoft/go-mssqldb v1.9.2 h1:nY8TmFMQOHpm2qVWo6y4I2mAmVdZqlGiMGAYt64Ibbs= 318 | github.com/microsoft/go-mssqldb v1.9.2/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA= 319 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 320 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 321 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 322 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 323 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 324 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 325 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 326 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 327 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 328 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 329 | github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= 330 | github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= 331 | github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= 332 | github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= 333 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 334 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 335 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 336 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 337 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 338 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 339 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 340 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 341 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 342 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 343 | github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E= 344 | github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk= 345 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 346 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 347 | github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 348 | github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 349 | github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= 350 | github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 351 | github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= 352 | github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 h1:QTvNkZ5ylY0PGgA+Lih+GdboMLY/G9SEGLMEGVjTVA4= 353 | github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= 354 | github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM= 355 | github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 356 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 357 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 358 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 359 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 360 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 361 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 362 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 363 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 364 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 365 | github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 366 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 367 | github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 368 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 369 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 370 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 371 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 372 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 373 | github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 374 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 375 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 376 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 377 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 378 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 379 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 380 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 381 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= 382 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 383 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 384 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 385 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 386 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 387 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 388 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 389 | github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= 390 | github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= 391 | github.com/sasha-s/go-deadlock v0.3.6 h1:TR7sfOnZ7x00tWPfD397Peodt57KzMDo+9Ae9rMiUmw= 392 | github.com/sasha-s/go-deadlock v0.3.6/go.mod h1:CUqNyyvMxTyjFqDT7MRg9mb4Dv/btmGTqSR+rky/UXo= 393 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 394 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 395 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 396 | github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= 397 | github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= 398 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 399 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 400 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 401 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 402 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 403 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 404 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 405 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 406 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 407 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 408 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 409 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 410 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 411 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 412 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 413 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 414 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 415 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 416 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 417 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 418 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 419 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 420 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 421 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 422 | github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= 423 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 424 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 425 | gitlab.com/gitlab-org/api/client-go v0.157.0 h1:B+/Ku1ek3V/MInR/SmvL4FOqE0YYx51u7lBVYIHC2ic= 426 | gitlab.com/gitlab-org/api/client-go v0.157.0/go.mod h1:CQVoxjEswJZeXft4Mi+H+OF1MVrpNVF6m4xvlPTQ2J4= 427 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 428 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 429 | go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= 430 | go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= 431 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= 432 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= 433 | go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= 434 | go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= 435 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= 436 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= 437 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8= 438 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8= 439 | go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= 440 | go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= 441 | go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= 442 | go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= 443 | go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= 444 | go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= 445 | go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= 446 | go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= 447 | go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= 448 | go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= 449 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 450 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 451 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 452 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 453 | go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 454 | go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 455 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 456 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 457 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 458 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 459 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 460 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 461 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 462 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 463 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 464 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 465 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 466 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 467 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 468 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 469 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 470 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 471 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 472 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 473 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 474 | golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= 475 | golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= 476 | golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= 477 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 478 | golang.org/x/exp v0.0.0-20251017212417-90e834f514db h1:by6IehL4BH5k3e3SJmcoNbOobMey2SLpAF79iPOEBvw= 479 | golang.org/x/exp v0.0.0-20251017212417-90e834f514db/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= 480 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 481 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 482 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 483 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 484 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 485 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 486 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 487 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 488 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 489 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 490 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 491 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 492 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 493 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 494 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 495 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 496 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 497 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 498 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 499 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 500 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 501 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 502 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 503 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 504 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 505 | golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= 506 | golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= 507 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 508 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 509 | golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY= 510 | golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 511 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 512 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 513 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 514 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 515 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 516 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 517 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 518 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 519 | golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= 520 | golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 521 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 522 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 523 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 524 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 525 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 526 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 527 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 528 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 529 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 530 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 531 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 532 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 533 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 534 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 535 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 536 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 537 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 538 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 539 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 540 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 541 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 542 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 543 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 544 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 545 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 546 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 547 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 548 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 549 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 550 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 551 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 552 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 553 | golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= 554 | golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 555 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 556 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 557 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 558 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 559 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 560 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 561 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 562 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 563 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 564 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 565 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 566 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 567 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 568 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 569 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 570 | golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= 571 | golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= 572 | golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= 573 | golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= 574 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 575 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 576 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 577 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 578 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 579 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 580 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 581 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 582 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 583 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 584 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 585 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 586 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 587 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 588 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 589 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 590 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 591 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 592 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 593 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 594 | gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= 595 | gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 596 | google.golang.org/api v0.252.0 h1:xfKJeAJaMwb8OC9fesr369rjciQ704AjU/psjkKURSI= 597 | google.golang.org/api v0.252.0/go.mod h1:dnHOv81x5RAmumZ7BWLShB/u7JZNeyalImxHmtTHxqw= 598 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 599 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 600 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 601 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 602 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 603 | google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= 604 | google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s= 605 | google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc= 606 | google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M= 607 | google.golang.org/genproto/googleapis/rpc v0.0.0-20251020155222-88f65dc88635 h1:3uycTxukehWrxH4HtPRtn1PDABTU331ViDjyqrUbaog= 608 | google.golang.org/genproto/googleapis/rpc v0.0.0-20251020155222-88f65dc88635/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= 609 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 610 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 611 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 612 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 613 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 614 | google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= 615 | google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= 616 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 617 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 618 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 619 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 620 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 621 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 622 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 623 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 624 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 625 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 626 | google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= 627 | google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 628 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 629 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 630 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 631 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 632 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 633 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 634 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 635 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 636 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 637 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 638 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 639 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 640 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 641 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 642 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 643 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 644 | gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= 645 | gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 646 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 647 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 648 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 649 | --------------------------------------------------------------------------------