├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── go.yml │ ├── golangci-lint.yml │ ├── release.yml │ └── stale.yml ├── .gitignore ├── .goreleaser.yaml ├── .idea ├── modules.xml ├── vault-plugin-secrets-gitlab.iml └── vcs.xml ├── LICENSE ├── README.md ├── backend.go ├── backend_test.go ├── cmd └── vault-plugin-secrets-gitlab │ └── main.go ├── defs.go ├── defs_test.go ├── entry_config.go ├── entry_config_merge_test.go ├── entry_config_update_form_field_data_test.go ├── entry_role.go ├── events.go ├── examples ├── terraform-vault-manages-gitlab-token │ ├── .gitignore │ ├── README.md │ ├── main.tf │ └── versions.tf └── terraform-with-patch-values │ ├── .gitignore │ ├── README.md │ ├── main.tf │ └── versions.tf ├── flags.go ├── flags_test.go ├── gitlab_client.go ├── gitlab_client_test.go ├── gitlab_record_client_test.go ├── gitlab_type.go ├── gitlab_type_test.go ├── gitlab_version_test.go ├── go.mod ├── go.sum ├── golangci.yml ├── helpers_test.go ├── local-env ├── .gitignore ├── README.md ├── backup-volumes.sh ├── docker-compose.yml ├── initial-setup.sh ├── restore-volumes.sh └── tf │ ├── .gitignore │ ├── group_example_with_project_owner.tf │ ├── group_test.tf │ ├── time.tf │ ├── tokens.json │ ├── tokens.tf │ ├── tokens_admin_user.tf │ ├── tokens_normal_user.tf │ ├── user.tf │ ├── vars.tf │ └── versions.tf ├── name_tpl.go ├── name_tpl_rand_string_test.go ├── name_tpl_test.go ├── name_tpl_unix_timestamp_test.go ├── path_config.go ├── path_config_list.go ├── path_config_list_test.go ├── path_config_rotate.go ├── path_config_rotate_test.go ├── path_config_test.go ├── path_config_token_autorotate_test.go ├── path_flags.go ├── path_flags_test.go ├── path_role.go ├── path_role_deploy_tokens_test.go ├── path_role_pipeline_project_trigger_token_test.go ├── path_role_test.go ├── path_role_ttl_test.go ├── path_token_role.go ├── path_token_role_multiple_config_test.go ├── path_token_role_test.go ├── secret_access_tokens.go ├── secret_access_tokens_test.go ├── testdata ├── gitlab-com ├── gitlab-selfhosted ├── local │ ├── TestWithAdminUser_PAT_AdminUser_GitlabRevokesToken.yaml │ ├── TestWithAdminUser_PAT_AdminUser_VaultRevokesToken.yaml │ ├── TestWithGroupDeployToken.yaml │ ├── TestWithNormalUser_GAT.yaml │ ├── TestWithNormalUser_PersonalAT_Fails.yaml │ ├── TestWithNormalUser_ProjectAT.yaml │ ├── TestWithPipelineProjectTriggerAccessToken.yaml │ └── TestWithProjectDeployToken.yaml ├── saas │ └── TestWithGitlabUser_RotateToken.yaml ├── selfhosted │ ├── TestWithServiceAccountGroup.yaml │ ├── TestWithServiceAccountUser.yaml │ ├── TestWithServiceAccountUserFail_dedicated.yaml │ └── TestWithServiceAccountUserFail_saas.yaml ├── tokens.json └── unit │ ├── TestBackend.yaml │ ├── TestGitlabClient_CreateAccessToken_And_Revoke.yaml │ ├── TestGitlabClient_CurrentTokenInfo.yaml │ ├── TestGitlabClient_GetGroupIdByPath.yaml │ ├── TestGitlabClient_GetUserIdByUsername.yaml │ ├── TestGitlabClient_GetUserIdByUsernameDoesNotMatch.yaml │ ├── TestGitlabClient_InvalidToken.yaml │ ├── TestGitlabClient_Metadata.yaml │ ├── TestGitlabClient_RevokeToken_NotFound.yaml │ ├── TestGitlabClient_Revoke_NonExistingTokens.yaml │ ├── TestGitlabClient_RotateCurrentToken.yaml │ ├── TestPathConfigList_empty_list.yaml │ ├── TestPathConfigList_multiple_configs.yaml │ ├── TestPathConfigRotate_initial_config_should_be_empty_fail_with_backend_not_configured.yaml │ ├── TestPathConfig_AutoRotateToken_call_auto_rotate_the_main_token_and_rotate_the_token.yaml │ ├── TestPathConfig_AutoRotateToken_call_auto_rotate_the_main_token_but_the_token_is_still_valid.yaml │ ├── TestPathConfig_AutoRotateToken_no_error_when_auto_rotate_is_disabled_and_config_is_not_set.yaml │ ├── TestPathConfig_AutoRotateToken_no_error_when_auto_rotate_is_disabled_and_config_is_set.yaml │ ├── TestPathConfig_AutoRotate_auto_rotate_before_cannot_be_more_than_the_minimal_value.yaml │ ├── TestPathConfig_AutoRotate_auto_rotate_before_should_be_between_the_min_and_max_value.yaml │ ├── TestPathConfig_AutoRotate_auto_rotate_before_should_be_less_than_the_maximal_limit.yaml │ ├── TestPathConfig_AutoRotate_auto_rotate_before_should_be_more_than_the_minimal_limit.yaml │ ├── TestPathConfig_AutoRotate_auto_rotate_before_should_be_set_to_correct_value.yaml │ ├── TestPathConfig_AutoRotate_auto_rotate_before_should_be_set_to_min_if_not_specified.yaml │ ├── TestPathConfig_AutoRotate_auto_rotate_token_should_be_false_if_not_specified.yaml │ ├── TestPathConfig_deleting_uninitialized_config_should_fail_with_backend_not_configured.yaml │ ├── TestPathConfig_initial_config_should_be_empty_fail_with_backend_not_configured.yaml │ ├── TestPathConfig_invalid_token.yaml │ ├── TestPathConfig_missing_token_from_the_request.yaml │ ├── TestPathConfig_patch_a_config.yaml │ ├── TestPathConfig_patch_a_config_no_backend.yaml │ ├── TestPathConfig_patch_a_config_with_no_storage.yaml │ ├── TestPathConfig_write_read_delete_and_read_config.yaml │ ├── TestPathConfig_write_read_delete_and_read_config_with_show_config_token.yaml │ ├── TestPathRolesDeployTokens_group_deploy_fail_to_create_role_due_to_missing_scopes_and_wrong_access_level.yaml │ ├── TestPathRolesDeployTokens_group_deploy_should_create_role_successfully.yaml │ ├── TestPathRolesDeployTokens_project_deploy_fail_to_create_role_due_to_missing_scopes_and_wrong_access_level.yaml │ ├── TestPathRolesDeployTokens_project_deploy_should_create_role_successfully.yaml │ ├── TestPathRolesList_empty_list.yaml │ ├── TestPathRolesPipelineProjectTrigger_should_fail_if_have_defined_scopes_or_access_level.yaml │ ├── TestPathRolesPipelineProjectTrigger_ttl_is_optional.yaml │ ├── TestPathRolesPipelineProjectTrigger_ttl_is_set.yaml │ ├── TestPathRolesTTL_general_ttl_limits_role_TTL__DefaultAccessTokenMaxPossibleTTL.yaml │ ├── TestPathRolesTTL_general_ttl_limits_ttl__maxTTL.yaml │ ├── TestPathRolesTTL_gitlab_revokes_the_tokens_ttl__24h.yaml │ ├── TestPathRolesTTL_gitlab_revokes_the_tokens_ttl__24h__ttl__DefaultAccessTokenMaxPossibleTTL.yaml │ ├── TestPathRolesTTL_vault_revokes_the_token_ttl__1h.yaml │ ├── TestPathRolesTTL_vault_revokes_the_token_ttl__1h__ttl__DefaultAccessTokenMaxPossibleTTL.yaml │ ├── TestPathRoles_Group_token_scopes_invalid_scopes.yaml │ ├── TestPathRoles_Group_token_scopes_valid_scopes.yaml │ ├── TestPathRoles_Personal_token_scopes_invalid_scopes.yaml │ ├── TestPathRoles_Personal_token_scopes_valid_scopes.yaml │ ├── TestPathRoles_Project_token_scopes_invalid_scopes.yaml │ ├── TestPathRoles_Project_token_scopes_valid_scopes.yaml │ ├── TestPathRoles_access_level_group_no_access_level_defined.yaml │ ├── TestPathRoles_access_level_group_with_access_level_defined.yaml │ ├── TestPathRoles_access_level_personal_no_access_level_defined.yaml │ ├── TestPathRoles_access_level_personal_with_access_level_defined.yaml │ ├── TestPathRoles_access_level_project_no_access_level_defined.yaml │ ├── TestPathRoles_access_level_project_with_access_level_defined.yaml │ ├── TestPathRoles_create_with_missing_parameters.yaml │ ├── TestPathRoles_delete_non_existing_role.yaml │ ├── TestPathRoles_full_flow_check_roles.yaml │ ├── TestPathRoles_invalid_name_template.yaml │ ├── TestPathRoles_update_handler_existence_check.yaml │ ├── TestPathRoles_we_get_error_if_backend_is_not_set_up_during_role_write.yaml │ ├── TestPathTokenRolesMultipleConfigs.yaml │ ├── TestPathTokenRoles_group_access_token.yaml │ ├── TestPathTokenRoles_personal_access_token.yaml │ ├── TestPathTokenRoles_project_access_token.yaml │ ├── TestPathTokenRoles_role_not_found.yaml │ └── TestSecretAccessTokenRevokeToken.yaml ├── token.go ├── token_config.go ├── token_group.go ├── token_group_deploy.go ├── token_group_service_account.go ├── token_personal.go ├── token_pipeline_project_trigger.go ├── token_project.go ├── token_project_deploy.go ├── token_user_service_account.go ├── token_with_scopes.go ├── token_with_scopes_and_access_level.go ├── type_access_level.go ├── type_access_level_test.go ├── type_token_scope.go ├── type_token_scope_test.go ├── type_token_type.go ├── type_token_type_test.go ├── utils.go ├── utils_test.go ├── version.go ├── with_admin_user_pat_gitlab_revokes_token_test.go ├── with_admin_user_pat_vault_revokes_token_test.go ├── with_gitlab_com_user_rotate_token_test.go ├── with_group_deploy_token_test.go ├── with_normal_user_gat_test.go ├── with_normal_user_personal_at_fails_test.go ├── with_normal_user_project_at_test.go ├── with_pipeline_project_trigger_token_test.go ├── with_project_deploy_token_test.go ├── with_service_account_fail_test.go ├── with_service_account_group_test.go └── with_service_account_user_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | groups: 13 | hashicorp: 14 | patterns: 15 | - github.com/hashicorp/go-hclog 16 | - github.com/hashicorp/go-multierror 17 | - github.com/hashicorp/vault/api 18 | - github.com/hashicorp/vault/sdk 19 | update-types: 20 | - minor 21 | - patch 22 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL Analysis" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: '43 9 * * 5' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ 'go' ] 24 | 25 | steps: 26 | - name: Checkout repository 27 | uses: actions/checkout@v4 28 | 29 | # Initializes the CodeQL tools for scanning. 30 | - name: Initialize CodeQL 31 | uses: github/codeql-action/init@v3 32 | with: 33 | languages: ${{ matrix.language }} 34 | 35 | 36 | - name: Autobuild 37 | uses: github/codeql-action/autobuild@v3 38 | 39 | 40 | - name: Perform CodeQL Analysis 41 | uses: github/codeql-action/analyze@v3 42 | with: 43 | category: "/language:${{matrix.language}}" -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version-file: 'go.mod' 19 | cache: true 20 | - name: Display Go version 21 | run: go version 22 | - name: Build 23 | run: go build ./cmd/vault-plugin-secrets-gitlab 24 | - name: Test 25 | run: go test -cover -coverprofile=coverage.out -tags unit,selfhosted,saas,local 26 | env: 27 | GITLAB_SERVICE_ACCOUNT_URL: ${{ vars.GITLAB_SERVICE_ACCOUNT_URL }} 28 | - name: Upload coverage reports to Codecov 29 | uses: codecov/codecov-action@v5 30 | env: 31 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 32 | with: 33 | files: ./coverage.out 34 | flags: unittests -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - "**.go" 9 | pull_request: 10 | branches: 11 | - main 12 | paths: 13 | - "**.go" 14 | 15 | permissions: 16 | contents: read 17 | 18 | jobs: 19 | golangci: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set up Go 24 | uses: actions/setup-go@v5 25 | with: 26 | go-version-file: 'go.mod' 27 | cache: true 28 | - name: Display Go version 29 | run: go version 30 | - name: golangci-lint 31 | uses: golangci/golangci-lint-action@v8 32 | with: 33 | version: latest -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | goreleaser: 13 | outputs: 14 | hashes: ${{ steps.binary.outputs.hashes }} 15 | permissions: 16 | contents: write 17 | packages: write 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | - name: Set up Go 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version-file: 'go.mod' 28 | cache: true 29 | - uses: anchore/sbom-action/download-syft@v0.20.0 30 | - name: Run GoReleaser 31 | uses: goreleaser/goreleaser-action@v6.3.0 32 | with: 33 | version: latest 34 | args: release --clean 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | VERSION_LDFLAGS: ${{ steps.ldflags.outputs.version }} 38 | - name: Generate binary hashes 39 | id: binary 40 | env: 41 | ARTIFACTS: "${{ steps.goreleaser.outputs.artifacts }}" 42 | run: | 43 | set -euo pipefail 44 | 45 | checksum_file=$(echo "$ARTIFACTS" | jq -r '.[] | select (.type=="Checksum") | .path') 46 | echo "hashes=$(cat $checksum_file | base64 -w0)" >> "$GITHUB_OUTPUT" 47 | 48 | # binary-provenance: 49 | # needs: [goreleaser] 50 | # permissions: 51 | # actions: read 52 | # id-token: write 53 | # contents: write 54 | # uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.10.0 55 | # with: 56 | # base64-subjects: "${{ needs.goreleaser.outputs.hashes }}" 57 | # upload-assets: true 58 | # 59 | # verification-with-slsa-verifier: 60 | # needs: [goreleaser, binary-provenance] 61 | # runs-on: ubuntu-latest 62 | # permissions: read-all 63 | # steps: 64 | # - name: Install the verifier 65 | # uses: slsa-framework/slsa-verifier/actions/installer@v2.5.1 66 | # - name: Download assets 67 | # env: 68 | # GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | # PROVENANCE: "${{ needs.binary-provenance.outputs.provenance-name }}" 70 | # run: | 71 | # set -euo pipefail 72 | # gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "*.tar.gz" 73 | # gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "*.zip" 74 | # gh -R "$GITHUB_REPOSITORY" release download "$GITHUB_REF_NAME" -p "$PROVENANCE" 75 | # - name: Verify assets 76 | # env: 77 | # CHECKSUMS: ${{ needs.goreleaser.outputs.hashes }} 78 | # PROVENANCE: "${{ needs.binary-provenance.outputs.provenance-name }}" 79 | # run: | 80 | # set -euo pipefail 81 | # checksums=$(echo "$CHECKSUMS" | base64 -d) 82 | # while read -r line; do 83 | # fn=$(echo $line | cut -d ' ' -f2) 84 | # echo "Verifying $fn" 85 | # slsa-verifier verify-artifact --provenance-path "$PROVENANCE" \ 86 | # --source-uri "github.com/$GITHUB_REPOSITORY" \ 87 | # --source-tag "$GITHUB_REF_NAME" \ 88 | # "$fn" 89 | # done <<<"$checksums" -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v9 11 | with: 12 | stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' 13 | stale-pr-message: 'This PR is stale because it has been open 45 days with no activity. Remove stale label or comment or this will be closed in 10 days.' 14 | close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' 15 | close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.' 16 | days-before-issue-stale: 30 17 | days-before-pr-stale: 45 18 | days-before-issue-close: 5 19 | days-before-pr-close: 10 20 | stale-issue-label: 'no-issue-activity' 21 | stale-pr-label: 'no-pr-activity' 22 | exempt-issue-assignees: 'ilijamt' 23 | exempt-pr-assignees: 'ilijamt' 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/**/workspace.xml 2 | .idea/**/tasks.xml 3 | .idea/**/usage.statistics.xml 4 | .idea/**/dictionaries 5 | .idea/**/shelf 6 | .idea/**/aws.xml 7 | .idea/**/contentModel.xml 8 | .idea/**/dataSources/ 9 | .idea/**/dataSources.ids 10 | .idea/**/dataSources.local.xml 11 | .idea/**/sqlDataSources.xml 12 | .idea/**/dynamic.xml 13 | .idea/**/uiDesigner.xml 14 | .idea/**/dbnavigator.xml 15 | .idea/**/gradle.xml 16 | .idea/**/libraries 17 | cmake-build-*/ 18 | .idea/**/mongoSettings.xml 19 | *.iws 20 | out/ 21 | .idea_modules/ 22 | atlassian-ide-plugin.xml 23 | .idea/replstate.xml 24 | .idea/sonarlint/ 25 | com_crashlytics_export_strings.xml 26 | crashlytics.properties 27 | crashlytics-build.properties 28 | fabric.properties 29 | .idea/httpRequests 30 | .idea/caches/build_file_checksums.ser 31 | *.exe 32 | *.exe~ 33 | *.dll 34 | *.so 35 | *.dylib 36 | *.test 37 | *.out 38 | go.work 39 | tmp/ 40 | bin/ 41 | /coverage.html 42 | /coverage.out 43 | .envrc -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | builds: 3 | - env: 4 | - CGO_ENABLED=0 5 | main: ./cmd/vault-plugin-secrets-gitlab/main.go 6 | mod_timestamp: '{{ .CommitTimestamp }}' 7 | flags: 8 | - -trimpath 9 | ldflags: 10 | - '-s -w' 11 | - "-X 'github.com/ilijamt/vault-plugin-secrets-gitlab.Version=v{{ .Version }}'" 12 | - "-X 'github.com/ilijamt/vault-plugin-secrets-gitlab.FullCommit={{ .FullCommit }}'" 13 | - "-X 'github.com/ilijamt/vault-plugin-secrets-gitlab.BuildDate={{ .Date }}'" 14 | goos: 15 | - windows 16 | - linux 17 | - darwin 18 | - illumos 19 | goarch: 20 | - amd64 21 | - '386' 22 | - arm 23 | - arm64 24 | ignore: 25 | - goos: darwin 26 | goarch: '386' 27 | binary: '{{ .ProjectName }}_v{{ .Version }}' 28 | archives: 29 | - formats: [ 'tar.gz' ] 30 | name_template: >- 31 | {{ .ProjectName }}_ 32 | {{- .Os }}_ 33 | {{- if eq .Arch "amd64" }}x86_64 34 | {{- else if eq .Arch "386" }}i386 35 | {{- else }}{{ .Arch }}{{ end }} 36 | {{- if .Arm }}v{{ .Arm }}{{ end }} 37 | format_overrides: 38 | - goos: windows 39 | formats: [ 'zip' ] 40 | report_sizes: true 41 | sboms: 42 | - artifacts: archive 43 | checksum: 44 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 45 | algorithm: sha256 46 | changelog: 47 | sort: asc 48 | use: github 49 | filters: 50 | exclude: 51 | - '^docs:' 52 | - '^test:' 53 | - "merge conflict" 54 | - Merge pull request 55 | - Merge remote-tracking branch 56 | - Merge branch 57 | - go mod tidy 58 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vault-plugin-secrets-gitlab.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ilija Matoski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /backend_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 12 | ) 13 | 14 | func TestBackend(t *testing.T) { 15 | var err error 16 | var b *gitlab.Backend 17 | ctx := getCtxGitlabClient(t, "unit") 18 | b, _, err = getBackend(ctx) 19 | require.NoError(t, err) 20 | require.NotNil(t, b) 21 | require.Nil(t, b.GetClient(gitlab.DefaultConfigName)) 22 | b.SetClient(newInMemoryClient(true), gitlab.DefaultConfigName) 23 | require.NotNil(t, b.GetClient(gitlab.DefaultConfigName)) 24 | b.Invalidate(ctx, fmt.Sprintf("%s/%s", gitlab.PathConfigStorage, gitlab.DefaultConfigName)) 25 | require.Nil(t, b.GetClient(gitlab.DefaultConfigName)) 26 | b.SetClient(newInMemoryClient(true), gitlab.DefaultConfigName) 27 | require.NotNil(t, b.GetClient(gitlab.DefaultConfigName)) 28 | require.EqualValues(t, gitlab.Version, b.PluginVersion().Version) 29 | } 30 | -------------------------------------------------------------------------------- /cmd/vault-plugin-secrets-gitlab/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/hashicorp/go-hclog" 7 | "github.com/hashicorp/vault/api" 8 | "github.com/hashicorp/vault/sdk/plugin" 9 | 10 | gat "github.com/ilijamt/vault-plugin-secrets-gitlab" 11 | ) 12 | 13 | var ( 14 | logger = hclog.New(&hclog.LoggerOptions{}) 15 | ) 16 | 17 | func main() { 18 | apiClientMeta := &api.PluginAPIClientMeta{} 19 | flags := apiClientMeta.FlagSet() 20 | pf := &gat.Flags{} 21 | pf.FlagSet(flags) 22 | 23 | fatalIfError(flags.Parse(os.Args[1:])) 24 | 25 | tlsConfig := apiClientMeta.GetTLSConfig() 26 | tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig) 27 | 28 | fatalIfError(plugin.ServeMultiplex(&plugin.ServeOpts{ 29 | BackendFactoryFunc: gat.Factory(*pf), 30 | TLSProviderFunc: tlsProviderFunc, 31 | Logger: logger, 32 | })) 33 | } 34 | 35 | func fatalIfError(err error) { 36 | if err != nil { 37 | logger.Error("plugin shutting down", "error", err) 38 | os.Exit(1) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /defs.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | var ( 11 | ErrNilValue = errors.New("nil value") 12 | ErrInvalidValue = errors.New("invalid value") 13 | ErrFieldRequired = errors.New("required field") 14 | ErrFieldInvalidValue = errors.New("invalid value for field") 15 | ErrBackendNotConfigured = errors.New("backend not configured") 16 | ) 17 | 18 | type contextKey string 19 | 20 | const ( 21 | DefaultConfigFieldAccessTokenMaxTTL = 7 * 24 * time.Hour 22 | DefaultConfigFieldAccessTokenRotate = DefaultAutoRotateBeforeMinTTL 23 | DefaultRoleFieldAccessTokenMaxTTL = 24 * time.Hour 24 | DefaultAccessTokenMinTTL = 24 * time.Hour 25 | DefaultAccessTokenMaxPossibleTTL = 365 * 24 * time.Hour 26 | DefaultAutoRotateBeforeMinTTL = 24 * time.Hour 27 | DefaultAutoRotateBeforeMaxTTL = 730 * time.Hour 28 | ctxKeyHttpClient = contextKey("vpsg-ctx-key-http-client") 29 | ctxKeyGitlabClient = contextKey("vpsg-ctx-key-gitlab-client") 30 | ctxKeyTimeNow = contextKey("vpsg-ctx-key-time-now") 31 | DefaultConfigName = "default" 32 | ) 33 | 34 | func WithStaticTime(ctx context.Context, t time.Time) context.Context { 35 | return context.WithValue(ctx, ctxKeyTimeNow, t) 36 | } 37 | 38 | func TimeFromContext(ctx context.Context) time.Time { 39 | t, ok := ctx.Value(ctxKeyTimeNow).(time.Time) 40 | if !ok { 41 | return time.Now() 42 | } 43 | return t 44 | } 45 | 46 | func HttpClientNewContext(ctx context.Context, httpClient *http.Client) context.Context { 47 | return context.WithValue(ctx, ctxKeyHttpClient, httpClient) 48 | } 49 | 50 | func HttpClientFromContext(ctx context.Context) (*http.Client, bool) { 51 | u, ok := ctx.Value(ctxKeyHttpClient).(*http.Client) 52 | if !ok { 53 | u = nil 54 | } 55 | return u, ok 56 | } 57 | 58 | func ClientNewContext(ctx context.Context, client Client) context.Context { 59 | return context.WithValue(ctx, ctxKeyGitlabClient, client) 60 | } 61 | 62 | func ClientFromContext(ctx context.Context) (Client, bool) { 63 | u, ok := ctx.Value(ctxKeyGitlabClient).(Client) 64 | if !ok { 65 | u = nil 66 | } 67 | return u, ok 68 | } 69 | -------------------------------------------------------------------------------- /defs_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/stretchr/testify/require" 9 | 10 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 11 | ) 12 | 13 | func TestEmptyGitlabClientFromContext(t *testing.T) { 14 | c, ok := gitlab.ClientFromContext(t.Context()) 15 | require.False(t, ok) 16 | require.Nil(t, c) 17 | } 18 | 19 | func TestEmptyHttpClientFromContext(t *testing.T) { 20 | c, ok := gitlab.HttpClientFromContext(t.Context()) 21 | require.False(t, ok) 22 | require.Nil(t, c) 23 | } 24 | -------------------------------------------------------------------------------- /entry_config_update_form_field_data_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/hashicorp/go-multierror" 9 | "github.com/hashicorp/vault/sdk/framework" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | 13 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 14 | ) 15 | 16 | func TestEntryConfigUpdateFromFieldData(t *testing.T) { 17 | t.Run("nil data", func(t *testing.T) { 18 | e := new(gitlab.EntryConfig) 19 | _, err := e.UpdateFromFieldData(nil) 20 | require.ErrorIs(t, err, gitlab.ErrNilValue) 21 | }) 22 | 23 | var tests = []struct { 24 | name string 25 | raw map[string]interface{} 26 | expectedConfig *gitlab.EntryConfig 27 | warnings []string 28 | err bool 29 | errMap map[string]int 30 | }{ 31 | { 32 | name: "no data should fail", 33 | raw: map[string]interface{}{}, 34 | err: true, 35 | warnings: []string{"auto_rotate_token not specified setting to 24h0m0s"}, 36 | errMap: map[string]int{ 37 | gitlab.ErrFieldRequired.Error(): 3, 38 | }, 39 | }, 40 | { 41 | name: "empty token and invalid type", 42 | raw: map[string]interface{}{ 43 | "base_url": "https://gitlab.com", 44 | "type": "type", 45 | }, 46 | expectedConfig: &gitlab.EntryConfig{AutoRotateBefore: gitlab.DefaultAutoRotateBeforeMinTTL, BaseURL: "https://gitlab.com"}, 47 | warnings: []string{"auto_rotate_token not specified setting to 24h0m0s"}, 48 | err: true, 49 | errMap: map[string]int{ 50 | gitlab.ErrFieldRequired.Error(): 1, 51 | gitlab.ErrUnknownType.Error(): 1, 52 | }, 53 | }, 54 | { 55 | name: "unconvertible data type", 56 | expectedConfig: &gitlab.EntryConfig{}, 57 | raw: map[string]interface{}{ 58 | "token": struct{}{}, 59 | }, 60 | err: true, 61 | errMap: map[string]int{}, 62 | }, 63 | { 64 | name: "valid config", 65 | expectedConfig: &gitlab.EntryConfig{ 66 | Token: "token", 67 | Type: gitlab.TypeSelfManaged, 68 | AutoRotateToken: false, 69 | AutoRotateBefore: gitlab.DefaultAutoRotateBeforeMinTTL, 70 | BaseURL: "https://gitlab.com", 71 | }, 72 | warnings: []string{"auto_rotate_token not specified setting to 24h0m0s"}, 73 | raw: map[string]interface{}{ 74 | "token": "token", 75 | "type": gitlab.TypeSelfManaged.String(), 76 | "base_url": "https://gitlab.com", 77 | }, 78 | }, 79 | } 80 | 81 | for _, test := range tests { 82 | t.Run(test.name, func(t *testing.T) { 83 | e := new(gitlab.EntryConfig) 84 | assert.Empty(t, e) 85 | warnings, err := e.UpdateFromFieldData(&framework.FieldData{Raw: test.raw, Schema: gitlab.FieldSchemaConfig}) 86 | assert.Equal(t, test.warnings, warnings) 87 | if test.expectedConfig == nil { 88 | test.expectedConfig = &gitlab.EntryConfig{AutoRotateBefore: gitlab.DefaultAutoRotateBeforeMinTTL} 89 | } 90 | assert.EqualValues(t, test.expectedConfig, e) 91 | if test.err { 92 | assert.Error(t, err) 93 | if len(test.errMap) > 0 { 94 | assert.Equal(t, countErrByName(err.(*multierror.Error)), test.errMap) 95 | } 96 | } else { 97 | assert.NoError(t, err) 98 | } 99 | }) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /entry_role.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/hashicorp/vault/sdk/logical" 9 | ) 10 | 11 | type EntryRole struct { 12 | RoleName string `json:"role_name" structs:"role_name" mapstructure:"role_name"` 13 | TTL time.Duration `json:"ttl" structs:"ttl" mapstructure:"ttl"` 14 | Path string `json:"path" structs:"path" mapstructure:"path"` 15 | Name string `json:"name" structs:"name" mapstructure:"name"` 16 | Scopes []string `json:"scopes" structs:"scopes" mapstructure:"scopes"` 17 | AccessLevel AccessLevel `json:"access_level" structs:"access_level" mapstructure:"access_level,omitempty"` 18 | TokenType TokenType `json:"token_type" structs:"token_type" mapstructure:"token_type"` 19 | GitlabRevokesTokens bool `json:"gitlab_revokes_token" structs:"gitlab_revokes_token" mapstructure:"gitlab_revokes_token"` 20 | ConfigName string `json:"config_name" structs:"config_name" mapstructure:"config_name"` 21 | } 22 | 23 | func (e EntryRole) LogicalResponseData() map[string]any { 24 | return map[string]any{ 25 | "role_name": e.RoleName, 26 | "path": e.Path, 27 | "name": e.Name, 28 | "scopes": e.Scopes, 29 | "access_level": e.AccessLevel.String(), 30 | "ttl": int64(e.TTL / time.Second), 31 | "token_type": e.TokenType.String(), 32 | "gitlab_revokes_token": e.GitlabRevokesTokens, 33 | "config_name": e.ConfigName, 34 | } 35 | } 36 | 37 | func getRole(ctx context.Context, name string, s logical.Storage) (role *EntryRole, err error) { 38 | var entry *logical.StorageEntry 39 | if entry, err = s.Get(ctx, fmt.Sprintf("%s/%s", PathRoleStorage, name)); err == nil { 40 | if entry == nil { 41 | return nil, nil 42 | } 43 | role = new(EntryRole) 44 | _ = entry.DecodeJSON(role) 45 | } 46 | return role, err 47 | 48 | } 49 | -------------------------------------------------------------------------------- /events.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/hashicorp/vault/sdk/framework" 9 | "github.com/hashicorp/vault/sdk/logical" 10 | "google.golang.org/protobuf/types/known/structpb" 11 | ) 12 | 13 | func event(ctx context.Context, b *framework.Backend, eventType string, metadata map[string]string) { 14 | var err error 15 | var ev *logical.EventData 16 | if ev, err = logical.NewEvent(); err == nil { 17 | var metadataBytes []byte 18 | metadataBytes, _ = json.Marshal(metadata) 19 | ev.Metadata = &structpb.Struct{} 20 | _ = ev.Metadata.UnmarshalJSON(metadataBytes) 21 | _ = b.SendEvent(ctx, logical.EventType(fmt.Sprintf("%s/%s", operationPrefixGitlabAccessTokens, eventType)), ev) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /examples/terraform-vault-manages-gitlab-token/.gitignore: -------------------------------------------------------------------------------- 1 | /.terraform 2 | /.terraform.lock.hcl 3 | /.envrc 4 | /plan 5 | /terraform.tfstate* -------------------------------------------------------------------------------- /examples/terraform-vault-manages-gitlab-token/README.md: -------------------------------------------------------------------------------- 1 | Terraform with Patch Values 2 | --------------------------- 3 | 4 | ```shell 5 | export TF_VAR_gitlab_base_url="http://localhost:8080" 6 | export TF_VAR_gitlab_token="glpat-secret-random-token" 7 | export VAULT_ADDR=http://127.0.0.1:8200 8 | export VAULT_TOKEN=root 9 | ``` 10 | 11 | ```shell 12 | ❯ terraform plan -out plan 13 | 14 | Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: 15 | + create 16 | 17 | Terraform will perform the following actions: 18 | 19 | # vault_generic_endpoint.mount_default_config will be created 20 | + resource "vault_generic_endpoint" "mount_default_config" { 21 | + data_json = (sensitive value) 22 | + disable_delete = true 23 | + disable_read = false 24 | + id = (known after apply) 25 | + ignore_absent_fields = true 26 | + path = "gitlab/config/default" 27 | + write_data = (known after apply) 28 | + write_data_json = (known after apply) 29 | + write_fields = [ 30 | + "base_url", 31 | + "auto_rotate_token", 32 | + "auto_rotate_before", 33 | + "type", 34 | + "scopes", 35 | ] 36 | } 37 | 38 | Plan: 1 to add, 0 to change, 0 to destroy. 39 | 40 | ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── 41 | 42 | Saved the plan to: plan 43 | 44 | To perform exactly these actions, run the following command to apply: 45 | terraform apply "plan" 46 | ❯ terraform apply plan 47 | vault_generic_endpoint.mount_default_config: Creating... 48 | vault_generic_endpoint.mount_default_config: Creation complete after 0s [id=gitlab/config/default] 49 | 50 | Apply complete! Resources: 1 added, 0 changed, 0 destroyed. 51 | ``` 52 | 53 | After that we have a configuration endpoint in Vault 54 | 55 | ```shell 56 | ❯ vault list gitlab/config 57 | Keys 58 | ---- 59 | default 60 | 61 | ❯ vault read gitlab/config/default 62 | Key Value 63 | --- ----- 64 | auto_rotate_before 48h0m0s 65 | auto_rotate_token true 66 | base_url http://localhost:8080 67 | name default 68 | scopes api, read_api, read_user, sudo, admin_mode, create_runner, k8s_proxy, read_repository, write_repository, ai_features, read_service_ping 69 | token_created_at 2024-07-11T18:53:26Z 70 | token_expires_at 2025-07-11T00:00:00Z 71 | token_id 1 72 | token_sha1_hash 9441e6e07d77a2d5601ab5d7cac5868d358d885c 73 | type self-managed 74 | ``` 75 | -------------------------------------------------------------------------------- /examples/terraform-vault-manages-gitlab-token/main.tf: -------------------------------------------------------------------------------- 1 | variable "gitlab_base_url" { 2 | description = "GitLab base URL, eg. https://gitlab.com" 3 | type = string 4 | } 5 | 6 | variable "gitlab_token" { 7 | description = "GitLab Token" 8 | type = string 9 | sensitive = true 10 | } 11 | 12 | variable "gitlab_type" { 13 | description = "GitLab Type can be saas, self-managed or dedicated" 14 | type = string 15 | default = "self-managed" 16 | } 17 | 18 | variable "gitlab_auto_rotate_token" { 19 | type = bool 20 | default = true 21 | } 22 | 23 | variable "gitlab_auto_rotate_before" { 24 | type = string 25 | default = "48h" 26 | } 27 | 28 | locals { 29 | vault_config_default_data = { 30 | token = var.gitlab_token 31 | base_url = var.gitlab_base_url 32 | auto_rotate_token = var.gitlab_auto_rotate_token 33 | auto_rotate_before = var.gitlab_auto_rotate_before 34 | type = var.gitlab_type 35 | } 36 | } 37 | 38 | resource "vault_generic_endpoint" "mount_default_config" { 39 | path = "gitlab/config/default" 40 | disable_delete = true 41 | ignore_absent_fields = true 42 | 43 | write_fields = [ 44 | "base_url", 45 | "auto_rotate_token", 46 | "auto_rotate_before", 47 | "type", 48 | "scopes", 49 | ] 50 | 51 | data_json = jsonencode(local.vault_config_default_data) 52 | 53 | lifecycle { 54 | ignore_changes = [ 55 | data_json 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/terraform-vault-manages-gitlab-token/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | vault = {} 4 | null = {} 5 | } 6 | } 7 | 8 | -------------------------------------------------------------------------------- /examples/terraform-with-patch-values/.gitignore: -------------------------------------------------------------------------------- 1 | /.terraform 2 | /.terraform.lock.hcl 3 | /.envrc 4 | /plan 5 | /terraform.tfstate* -------------------------------------------------------------------------------- /examples/terraform-with-patch-values/main.tf: -------------------------------------------------------------------------------- 1 | variable "gitlab_base_url" { 2 | description = "GitLab base URL, eg. https://gitlab.com" 3 | type = string 4 | } 5 | 6 | variable "gitlab_token" { 7 | description = "GitLab Token" 8 | type = string 9 | sensitive = true 10 | } 11 | 12 | variable "gitlab_type" { 13 | description = "GitLab Type can be saas, self-managed or dedicated" 14 | type = string 15 | default = "self-managed" 16 | } 17 | 18 | variable "gitlab_auto_rotate_token" { 19 | type = bool 20 | default = true 21 | } 22 | 23 | variable "gitlab_auto_rotate_before" { 24 | type = string 25 | default = "48h" 26 | } 27 | 28 | locals { 29 | vault_config_default_data = { 30 | token = var.gitlab_token 31 | base_url = var.gitlab_base_url 32 | auto_rotate_token = var.gitlab_auto_rotate_token 33 | auto_rotate_before = var.gitlab_auto_rotate_before 34 | type = var.gitlab_type 35 | } 36 | 37 | vault_config_default_patch_data = { 38 | for k, v in local.vault_config_default_data : k => v if k != "token" 39 | } 40 | } 41 | 42 | resource "vault_generic_endpoint" "mount_default_config" { 43 | path = "gitlab/config/default" 44 | disable_delete = true 45 | ignore_absent_fields = true 46 | 47 | write_fields = [ 48 | "base_url", 49 | "auto_rotate_token", 50 | "auto_rotate_before", 51 | "type", 52 | "scopes", 53 | ] 54 | 55 | data_json = jsonencode(local.vault_config_default_data) 56 | 57 | lifecycle { 58 | ignore_changes = [ 59 | data_json 60 | ] 61 | } 62 | } 63 | 64 | resource "null_resource" "mount_default_config_patch" { 65 | for_each = local.vault_config_default_patch_data 66 | triggers = { (each.key) = each.value } 67 | 68 | provisioner "local-exec" { 69 | command = </dev/null 71 | EOT 72 | interpreter = ["bash", "-c"] 73 | } 74 | 75 | depends_on = [ 76 | vault_generic_endpoint.mount_default_config, 77 | ] 78 | } -------------------------------------------------------------------------------- /examples/terraform-with-patch-values/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | vault = {} 4 | null = {} 5 | } 6 | } 7 | 8 | -------------------------------------------------------------------------------- /flags.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "flag" 5 | ) 6 | 7 | type Flags struct { 8 | ShowConfigToken bool `json:"show_config_token" mapstructure:"show_config_token"` 9 | AllowRuntimeFlagsChange bool `json:"allow_runtime_flags_change" mapstructure:"allow_runtime_flags_change"` 10 | } 11 | 12 | // FlagSet returns the flag set for configuring the TLS connection 13 | func (f *Flags) FlagSet(fs *flag.FlagSet) *flag.FlagSet { 14 | fs.BoolVar(&f.ShowConfigToken, "show-config-token", false, "Display the token value when reading it's config the configuration endpoint.") 15 | fs.BoolVar(&f.AllowRuntimeFlagsChange, "allow-runtime-flags-change", false, "Allows you to change the flags dynamically at runtime.") 16 | return fs 17 | } 18 | -------------------------------------------------------------------------------- /flags_test.go: -------------------------------------------------------------------------------- 1 | package gitlab_test 2 | 3 | import ( 4 | "flag" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 10 | ) 11 | 12 | func TestFlags_FlagSet(t *testing.T) { 13 | fs := flag.NewFlagSet("test", flag.ContinueOnError) 14 | 15 | flags := &gitlab.Flags{} 16 | flags.FlagSet(fs) 17 | 18 | assert.False(t, flags.ShowConfigToken) 19 | assert.False(t, flags.AllowRuntimeFlagsChange) 20 | assert.NoError(t, fs.Parse([]string{"-show-config-token", "-allow-runtime-flags-change"})) 21 | assert.True(t, flags.ShowConfigToken) 22 | assert.True(t, flags.AllowRuntimeFlagsChange) 23 | } 24 | -------------------------------------------------------------------------------- /gitlab_record_client_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit || saas || selfhosted || local 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "cmp" 7 | "fmt" 8 | "net/http" 9 | "os" 10 | "testing" 11 | 12 | "gopkg.in/dnaeon/go-vcr.v4/pkg/cassette" 13 | "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder" 14 | ) 15 | 16 | func getClient(t *testing.T, target string) (client *http.Client, u string) { 17 | t.Helper() 18 | 19 | filename := fmt.Sprintf("testdata/%s/%s", target, sanitizePath(t.Name())) 20 | r, err := recorder.New(filename, 21 | []recorder.Option{ 22 | recorder.WithSkipRequestLatency(false), 23 | recorder.WithMode(recorder.ModeRecordOnce), 24 | recorder.WithMatcher( 25 | cassette.NewDefaultMatcher( 26 | cassette.WithIgnoreUserAgent(), 27 | cassette.WithIgnoreAuthorization(), 28 | cassette.WithIgnoreHeaders( 29 | "Private-Token", 30 | ), 31 | ), 32 | ), 33 | recorder.WithHook(func(i *cassette.Interaction) error { 34 | i.Request.Headers.Set("Private-Token", "REPLACED-TOKEN") 35 | return nil 36 | }, recorder.BeforeSaveHook), 37 | }..., 38 | ) 39 | if err != nil { 40 | t.Fatalf("could not create recorder: %s", err) 41 | } 42 | 43 | t.Cleanup(func() { 44 | if err := r.Stop(); err != nil { 45 | t.Errorf("could not close recorder: %s", err) 46 | } 47 | }) 48 | 49 | u = cmp.Or(os.Getenv("GITLAB_URL"), "http://localhost:8080/") 50 | return r.GetDefaultClient(), u 51 | } 52 | -------------------------------------------------------------------------------- /gitlab_type.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "slices" 7 | ) 8 | 9 | type Type string 10 | 11 | const ( 12 | TypeSaaS Type = "saas" 13 | TypeDedicated Type = "dedicated" 14 | TypeSelfManaged Type = "self-managed" 15 | TypeUnknown = Type("") 16 | ) 17 | 18 | var ( 19 | ErrUnknownType = errors.New("unknown gitlab type") 20 | 21 | validGitlabTypes = []string{ 22 | TypeSaaS.String(), 23 | TypeSelfManaged.String(), 24 | TypeDedicated.String(), 25 | } 26 | ) 27 | 28 | func (i Type) String() string { 29 | return string(i) 30 | } 31 | 32 | func (i Type) Value() string { 33 | return i.String() 34 | } 35 | 36 | func TypeParse(value string) (Type, error) { 37 | if slices.Contains(validGitlabTypes, value) { 38 | return Type(value), nil 39 | } 40 | return TypeUnknown, fmt.Errorf("failed to parse '%s': %w", value, ErrUnknownType) 41 | } 42 | -------------------------------------------------------------------------------- /gitlab_type_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 11 | ) 12 | 13 | func TestType(t *testing.T) { 14 | var tests = []struct { 15 | expected gitlab.Type 16 | input string 17 | err bool 18 | }{ 19 | { 20 | expected: gitlab.TypeSaaS, 21 | input: gitlab.TypeSaaS.String(), 22 | err: false, 23 | }, 24 | { 25 | expected: gitlab.TypeSelfManaged, 26 | input: gitlab.TypeSelfManaged.String(), 27 | err: false, 28 | }, 29 | { 30 | expected: gitlab.TypeDedicated, 31 | input: gitlab.TypeDedicated.String(), 32 | err: false, 33 | }, 34 | { 35 | expected: gitlab.TypeUnknown, 36 | input: gitlab.TypeUnknown.String(), 37 | err: true, 38 | }, 39 | } 40 | 41 | for _, test := range tests { 42 | t.Logf("assert parse(%s) = %s (err: %v)", test.input, test.expected, test.err) 43 | val, err := gitlab.TypeParse(test.input) 44 | if test.err { 45 | assert.ErrorIs(t, err, gitlab.ErrUnknownType) 46 | } else { 47 | assert.NoError(t, err) 48 | assert.EqualValues(t, test.expected, val) 49 | assert.EqualValues(t, test.expected.Value(), test.expected.String()) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /gitlab_version_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit || saas || selfhosted || local 2 | 3 | package gitlab_test 4 | 5 | const ( 6 | gitlabVersion = "16.11.6" 7 | ) 8 | -------------------------------------------------------------------------------- /golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 3m 3 | 4 | -------------------------------------------------------------------------------- /local-env/.gitignore: -------------------------------------------------------------------------------- 1 | /backup.tar -------------------------------------------------------------------------------- /local-env/README.md: -------------------------------------------------------------------------------- 1 | ## Local Environment 2 | 3 | To run tests against a real GitLab instance, follow the steps below. 4 | 5 | ### Initial Setup 6 | 7 | 1. **Run the setup script:** 8 | 9 | This command will set up a GitLab instance that is fully configured for testing locally. 10 | 11 | ```bash 12 | bash initial-setup.sh 13 | ``` 14 | 15 | **Note:** Setting up the GitLab instance might take some time. After the setup, a complete backup of the PostgreSQL database will be created to facilitate quick restoration if needed. 16 | 17 | ### Restoring the Environment 18 | 19 | If you need to restore the GitLab instance back to its original configuration, use the following command: 20 | 21 | ```bash 22 | bash restore-volumes.sh 23 | ``` 24 | -------------------------------------------------------------------------------- /local-env/backup-volumes.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -x 4 | 5 | rm backup.tar 6 | docker compose stop 7 | docker run --rm --volumes-from vpsg-web-1 -v $(pwd):/backup ubuntu tar cvf /backup/backup.tar /etc/gitlab /var/opt/gitlab/postgresql/ 8 | docker compose up -d -------------------------------------------------------------------------------- /local-env/docker-compose.yml: -------------------------------------------------------------------------------- 1 | name: vpsg 2 | services: 3 | web: 4 | image: 'gitlab/gitlab-ce:17.10.3-ce.0' 5 | environment: 6 | GITLAB_OMNIBUS_CONFIG: | 7 | gitlab_rails['gitlab_shell_ssh_port'] = 2224 8 | gitlab_rails['initial_root_password'] = "Iem3oe_lohy1" 9 | ports: 10 | - '8080:80' 11 | - '8443:443' 12 | - '2224:22' 13 | volumes: 14 | - 'gitlab_config:/etc/gitlab' 15 | - 'gitlab_logs:/var/log/gitlab' 16 | - 'gitlab_data:/var/opt/gitlab' 17 | shm_size: 2g 18 | logging: 19 | options: 20 | max-size: "3m" 21 | max-file: "1" 22 | 23 | volumes: 24 | gitlab_config: 25 | gitlab_logs: 26 | gitlab_data: 27 | -------------------------------------------------------------------------------- /local-env/initial-setup.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | set -x 4 | docker compose kill 5 | docker compose down --remove-orphans --volumes 6 | docker compose up -d --wait 7 | rm -f tf/terraform.tfstate* 8 | docker compose up -d --wait 9 | 10 | fn() { 11 | local cmd=$1 12 | echo "$cmd" 13 | docker exec vpsg-web-1 gitlab-rails runner "$cmd" 14 | } 15 | 16 | is_gitlab_up() { 17 | curl -sSf http://localhost:8080/users/sign_in > /dev/null 2>&1 18 | } 19 | 20 | # Wait for GitLab to be up 21 | until is_gitlab_up; do 22 | echo "Waiting for GitLab to be up..." 23 | sleep 10 24 | done 25 | 26 | fn 'token = User.find_by_username("root").personal_access_tokens.create(name: "Initial token", expires_at: DateTime.now.next_month(6).to_time, scopes: [:api, :read_api, :read_user, :sudo, :admin_mode, :create_runner, :k8s_proxy, :read_repository, :write_repository, :ai_features, :read_service_ping]); token.set_token("glpat-secret-random-token"); token.save!' 27 | 28 | set -eux 29 | 30 | cd tf || exit 31 | terraform init 32 | terraform apply --auto-approve 33 | cat tokens.json | jq . > ../../testdata/tokens.json 34 | 35 | cd .. 36 | bash backup-volumes.sh -------------------------------------------------------------------------------- /local-env/restore-volumes.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker compose kill 4 | docker run --rm --volumes-from vpsg-web-1 -v $(pwd):/backup ubuntu bash -c "rm -rf /etc/gitlab /var/opt/gitlab/postgresql/; cd / && tar xvf /backup/backup.tar" 5 | docker compose up -d -------------------------------------------------------------------------------- /local-env/tf/.gitignore: -------------------------------------------------------------------------------- 1 | /.terraform 2 | /.terraform.lock.hcl 3 | /terraform.* 4 | /plan -------------------------------------------------------------------------------- /local-env/tf/group_example_with_project_owner.tf: -------------------------------------------------------------------------------- 1 | resource "gitlab_group" "group_example" { 2 | name = "example" 3 | path = "example" 4 | description = "An example group" 5 | } 6 | 7 | resource "gitlab_project" "project_example" { 8 | name = "example" 9 | description = "An example project" 10 | namespace_id = gitlab_group.group_example.id 11 | } 12 | 13 | resource "gitlab_group_membership" "normal_user" { 14 | group_id = gitlab_group.group_example.id 15 | user_id = gitlab_user.normal_user.id 16 | access_level = "owner" 17 | } 18 | 19 | -------------------------------------------------------------------------------- /local-env/tf/group_test.tf: -------------------------------------------------------------------------------- 1 | resource "gitlab_group" "test" { 2 | name = "test" 3 | path = "test" 4 | description = "A test group" 5 | } 6 | -------------------------------------------------------------------------------- /local-env/tf/time.tf: -------------------------------------------------------------------------------- 1 | provider "time" {} 2 | 3 | resource "time_offset" "one_year_later" { 4 | offset_days = 364 5 | } 6 | -------------------------------------------------------------------------------- /local-env/tf/tokens.json: -------------------------------------------------------------------------------- 1 | {"admin_user_auto_rotate_token_1":{"created_at":"2025-04-04 18:35:29.407 +0000 UTC","id":"3:4","token":"glpat-QqqjWLooAa55hKFbWE8w"},"admin_user_auto_rotate_token_main_token":{"created_at":"2025-04-04 18:35:29.489 +0000 UTC","id":"3:6","token":"glpat-qi3_APbMtb-SCbUFd-eM"},"admin_user_initial_token":{"created_at":"2025-04-04 18:35:29.419 +0000 UTC","id":"3:5","token":"glpat-va5WDH4vz9bE1KeAR2C2"},"admin_user_root":{"created_at":"2025-04-04 18:35:29.407 +0000 UTC","id":"3:3","token":"glpat-wU8yWBGat-nypZcyf1LL"},"normal_user_initial_token":{"created_at":"2025-04-04 18:35:29.099 +0000 UTC","id":"2:2","token":"glpat-RKEynyYigffJp45zNFD-"}} -------------------------------------------------------------------------------- /local-env/tf/tokens.tf: -------------------------------------------------------------------------------- 1 | resource "local_file" "tokens" { 2 | filename = "${path.module}/tokens.json" 3 | content = jsonencode({ 4 | admin_user_root = { 5 | id = gitlab_personal_access_token.admin_user_root.id 6 | token = gitlab_personal_access_token.admin_user_root.token 7 | created_at = gitlab_personal_access_token.admin_user_root.created_at 8 | } 9 | admin_user_initial_token = { 10 | id = gitlab_personal_access_token.admin_user_initial_token.id 11 | token = gitlab_personal_access_token.admin_user_initial_token.token 12 | created_at = gitlab_personal_access_token.admin_user_initial_token.created_at 13 | } 14 | admin_user_auto_rotate_token_main_token = { 15 | id = gitlab_personal_access_token.admin_user_auto_rotate_token_main_token.id 16 | token = gitlab_personal_access_token.admin_user_auto_rotate_token_main_token.token 17 | created_at = gitlab_personal_access_token.admin_user_auto_rotate_token_main_token.created_at 18 | } 19 | admin_user_auto_rotate_token_1 = { 20 | id = gitlab_personal_access_token.admin_user_auto_rotate_token_1.id 21 | token = gitlab_personal_access_token.admin_user_auto_rotate_token_1.token 22 | created_at = gitlab_personal_access_token.admin_user_auto_rotate_token_1.created_at 23 | } 24 | normal_user_initial_token = { 25 | id = gitlab_personal_access_token.normal_user_initial_token.id 26 | token = gitlab_personal_access_token.normal_user_initial_token.token 27 | created_at = gitlab_personal_access_token.normal_user_initial_token.created_at 28 | } 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /local-env/tf/tokens_admin_user.tf: -------------------------------------------------------------------------------- 1 | resource "gitlab_personal_access_token" "admin_user_root" { 2 | user_id = gitlab_user.admin_user.id 3 | name = "Root token" 4 | expires_at = local.token_expiry_time 5 | 6 | scopes = concat(local.scopes_admin_user, local.scopes_user_token) 7 | } 8 | 9 | resource "gitlab_personal_access_token" "admin_user_initial_token" { 10 | user_id = gitlab_user.admin_user.id 11 | name = "Initial token" 12 | expires_at = local.token_expiry_time 13 | 14 | scopes = local.scopes_user_token 15 | } 16 | 17 | resource "gitlab_personal_access_token" "admin_user_auto_rotate_token_main_token" { 18 | user_id = gitlab_user.admin_user.id 19 | name = "Auto rotate token main token" 20 | expires_at = local.token_expiry_time 21 | 22 | scopes = local.scopes_user_token 23 | } 24 | 25 | 26 | resource "gitlab_personal_access_token" "admin_user_auto_rotate_token_1" { 27 | user_id = gitlab_user.admin_user.id 28 | name = "Auto rotate token 1" 29 | expires_at = local.token_expiry_time 30 | 31 | scopes = local.scopes_user_token 32 | } 33 | -------------------------------------------------------------------------------- /local-env/tf/tokens_normal_user.tf: -------------------------------------------------------------------------------- 1 | resource "gitlab_personal_access_token" "normal_user_initial_token" { 2 | user_id = gitlab_user.normal_user.id 3 | name = "Initial token" 4 | expires_at = local.token_expiry_time 5 | 6 | scopes = local.scopes_user_token 7 | } 8 | -------------------------------------------------------------------------------- /local-env/tf/user.tf: -------------------------------------------------------------------------------- 1 | resource "gitlab_user" "admin_user" { 2 | name = "Admin User" 3 | username = "admin-user" 4 | password = "quaijooMeewieMieM1bi" 5 | email = "admin@local" 6 | is_admin = true 7 | reset_password = false 8 | } 9 | 10 | resource "gitlab_user" "normal_user" { 11 | name = "Normal User" 12 | username = "normal-user" 13 | password = "cashaep4ONgahCae0bae" 14 | email = "normal@local" 15 | is_admin = false 16 | reset_password = false 17 | } 18 | -------------------------------------------------------------------------------- /local-env/tf/vars.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | token_expiry_time = formatdate("YYYY-MM-DD", time_offset.one_year_later.rfc3339) 3 | scopes_admin_user = ["sudo", "admin_mode"] 4 | scopes_user_token = ["api"] 5 | } 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /local-env/tf/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | gitlab = { 4 | source = "gitlabhq/gitlab" 5 | version = "~> 17.10" 6 | } 7 | local = { 8 | source = "hashicorp/local" 9 | version = "~> 2.5" 10 | } 11 | time = { 12 | source = "hashicorp/time" 13 | version = "~> 0.13" 14 | } 15 | } 16 | } 17 | 18 | provider "gitlab" { 19 | base_url = "http://localhost:8080" 20 | insecure = true 21 | token = "glpat-secret-random-token" 22 | } 23 | -------------------------------------------------------------------------------- /name_tpl.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "strings" 7 | "text/template" 8 | "time" 9 | _ "unsafe" 10 | ) 11 | 12 | func yesNoBool(in bool) string { 13 | if in { 14 | return "yes" 15 | } 16 | return "no" 17 | } 18 | func randHexString(bytes int) string { 19 | buf := make([]byte, bytes) 20 | _, _ = rand.Read(buf) 21 | return fmt.Sprintf("%x", buf) 22 | } 23 | 24 | func timeNowFormat(layout string) string { 25 | return time.Now().UTC().Format(layout) 26 | } 27 | 28 | var tplFuncMap = template.FuncMap{ 29 | "randHexString": randHexString, 30 | "stringsJoin": strings.Join, 31 | "yesNoBool": yesNoBool, 32 | "timeNowFormat": timeNowFormat, 33 | } 34 | 35 | func TokenName(role *EntryRole) (name string, err error) { 36 | if role == nil { 37 | return "", fmt.Errorf("role: %w", ErrNilValue) 38 | } 39 | var tpl *template.Template 40 | tpl, err = template.New("name").Funcs(tplFuncMap).Parse(role.Name) 41 | if err != nil { 42 | return "", err 43 | } 44 | buf := new(strings.Builder) 45 | var data = role.LogicalResponseData() 46 | data["unix_timestamp_utc"] = time.Now().UTC().Unix() 47 | delete(data, "name") 48 | err = tpl.Execute(buf, data) 49 | name = buf.String() 50 | return name, err 51 | } 52 | -------------------------------------------------------------------------------- /name_tpl_rand_string_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/require" 10 | 11 | g "github.com/ilijamt/vault-plugin-secrets-gitlab" 12 | ) 13 | 14 | func TestTokenNameGenerator_RandString(t *testing.T) { 15 | val, err := g.TokenName( 16 | &g.EntryRole{ 17 | RoleName: "test", 18 | TTL: time.Hour, 19 | Path: "/path", 20 | Name: "{{ randHexString 8 }}", 21 | Scopes: []string{g.TokenScopeApi.String()}, 22 | AccessLevel: g.AccessLevelNoPermissions, 23 | TokenType: g.TokenTypePersonal, 24 | GitlabRevokesTokens: false, 25 | }, 26 | ) 27 | require.NoError(t, err) 28 | require.NotEmpty(t, val) 29 | require.Len(t, val, 16) 30 | } 31 | -------------------------------------------------------------------------------- /name_tpl_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "fmt" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | 12 | g "github.com/ilijamt/vault-plugin-secrets-gitlab" 13 | ) 14 | 15 | func TestTokenNameGenerator(t *testing.T) { 16 | var tests = []struct { 17 | in *g.EntryRole 18 | outVal string 19 | outErr bool 20 | }{ 21 | {nil, "", true}, 22 | 23 | // invalid template 24 | { 25 | &g.EntryRole{ 26 | RoleName: "test", 27 | TTL: time.Hour, 28 | Path: "/path", 29 | Name: "{{ .role_name", 30 | Scopes: []string{g.TokenScopeApi.String()}, 31 | AccessLevel: g.AccessLevelNoPermissions, 32 | TokenType: g.TokenTypePersonal, 33 | GitlabRevokesTokens: true, 34 | }, 35 | "", 36 | true, 37 | }, 38 | 39 | // combination template 40 | { 41 | &g.EntryRole{ 42 | RoleName: "test", 43 | TTL: time.Hour, 44 | Path: "/path", 45 | Name: "{{ .role_name }}-{{ .token_type }}-access-token-{{ yesNoBool .gitlab_revokes_token }}", 46 | Scopes: []string{g.TokenScopeApi.String()}, 47 | AccessLevel: g.AccessLevelNoPermissions, 48 | TokenType: g.TokenTypePersonal, 49 | GitlabRevokesTokens: true, 50 | }, 51 | "test-personal-access-token-yes", 52 | false, 53 | }, 54 | 55 | // with stringsJoin 56 | { 57 | &g.EntryRole{ 58 | RoleName: "test", 59 | TTL: time.Hour, 60 | Path: "/path", 61 | Name: "{{ .role_name }}-{{ .token_type }}-{{ stringsJoin .scopes \"-\" }}-{{ yesNoBool .gitlab_revokes_token }}", 62 | Scopes: []string{g.TokenScopeApi.String(), g.TokenScopeSudo.String()}, 63 | AccessLevel: g.AccessLevelNoPermissions, 64 | TokenType: g.TokenTypePersonal, 65 | GitlabRevokesTokens: false, 66 | }, 67 | "test-personal-api-sudo-no", 68 | false, 69 | }, 70 | 71 | // with timeNowFormat 72 | { 73 | &g.EntryRole{ 74 | RoleName: "test", 75 | TTL: time.Hour, 76 | Path: "/path", 77 | Name: "{{ .role_name }}-{{ .token_type }}-{{ timeNowFormat \"2006-01\" }}", 78 | Scopes: []string{g.TokenScopeApi.String(), g.TokenScopeSudo.String()}, 79 | AccessLevel: g.AccessLevelNoPermissions, 80 | TokenType: g.TokenTypePersonal, 81 | GitlabRevokesTokens: false, 82 | }, 83 | fmt.Sprintf("test-personal-%d-%02d", time.Now().UTC().Year(), time.Now().UTC().Month()), 84 | false, 85 | }, 86 | } 87 | 88 | for _, tst := range tests { 89 | t.Logf("TokenName(%v)", tst.in) 90 | val, err := g.TokenName(tst.in) 91 | assert.Equal(t, tst.outVal, val) 92 | if tst.outErr { 93 | assert.Error(t, err, tst.outErr) 94 | } else { 95 | assert.NoError(t, err) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /name_tpl_unix_timestamp_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "strconv" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/require" 11 | 12 | g "github.com/ilijamt/vault-plugin-secrets-gitlab" 13 | ) 14 | 15 | func TestTokenNameGenerator_UnixTimeStamp(t *testing.T) { 16 | now := time.Now().UTC().Unix() 17 | val, err := g.TokenName( 18 | &g.EntryRole{ 19 | RoleName: "test", 20 | TTL: time.Hour, 21 | Path: "/path", 22 | Name: "{{ .unix_timestamp_utc }}", 23 | Scopes: []string{g.TokenScopeApi.String()}, 24 | AccessLevel: g.AccessLevelNoPermissions, 25 | TokenType: g.TokenTypePersonal, 26 | GitlabRevokesTokens: false, 27 | }, 28 | ) 29 | require.NoError(t, err) 30 | require.NotEmpty(t, val) 31 | i, err := strconv.ParseInt(val, 10, 64) 32 | require.NoError(t, err) 33 | require.GreaterOrEqual(t, i, now) 34 | } 35 | -------------------------------------------------------------------------------- /path_config_list.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "strings" 8 | 9 | "github.com/hashicorp/vault/sdk/framework" 10 | "github.com/hashicorp/vault/sdk/logical" 11 | ) 12 | 13 | const ( 14 | pathListConfigHelpSyn = `Lists existing configs` 15 | pathListConfigHelpDesc = ` 16 | This path allows you to list all available configurations that have been set up within the GitLab Access Tokens Backend. 17 | These configurations typically include credentials, base URLs, and other settings required for managing access tokens 18 | across different GitLab environments.` 19 | ) 20 | 21 | func pathListConfig(b *Backend) *framework.Path { 22 | return &framework.Path{ 23 | HelpSynopsis: strings.TrimSpace(pathListConfigHelpSyn), 24 | HelpDescription: strings.TrimSpace(pathListConfigHelpDesc), 25 | Pattern: fmt.Sprintf("%s?/?$", PathConfigStorage), 26 | DisplayAttrs: &framework.DisplayAttributes{ 27 | OperationPrefix: operationPrefixGitlabAccessTokens, 28 | OperationSuffix: "config", 29 | }, 30 | Operations: map[logical.Operation]framework.OperationHandler{ 31 | logical.ListOperation: &framework.PathOperation{ 32 | Callback: b.pathConfigList, 33 | DisplayAttrs: &framework.DisplayAttributes{ 34 | OperationVerb: "list", 35 | }, 36 | Responses: map[int][]framework.Response{ 37 | http.StatusOK: {{ 38 | Description: http.StatusText(http.StatusOK), 39 | Fields: map[string]*framework.FieldSchema{ 40 | "config_name": FieldSchemaRoles["config_name"], 41 | }, 42 | }}, 43 | }, 44 | }, 45 | }, 46 | } 47 | } 48 | 49 | func (b *Backend) pathConfigList(ctx context.Context, req *logical.Request, data *framework.FieldData) (lResp *logical.Response, err error) { 50 | var configs []string 51 | configs, err = req.Storage.List(ctx, fmt.Sprintf("%s/", PathConfigStorage)) 52 | lResp = logical.ErrorResponse("Error listing configs") 53 | if err == nil { 54 | lResp = logical.ListResponse(configs) 55 | } 56 | b.Logger().Debug("Available", "configs", configs) 57 | return lResp, err 58 | } 59 | -------------------------------------------------------------------------------- /path_config_list_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "cmp" 7 | "fmt" 8 | "os" 9 | "slices" 10 | "testing" 11 | 12 | "github.com/hashicorp/vault/sdk/logical" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | 16 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 17 | ) 18 | 19 | func TestPathConfigList(t *testing.T) { 20 | t.Run("empty list", func(t *testing.T) { 21 | ctx := getCtxGitlabClient(t, "unit") 22 | var b, l, err = getBackend(ctx) 23 | require.NoError(t, err) 24 | resp, err := b.HandleRequest(ctx, &logical.Request{ 25 | Operation: logical.ListOperation, 26 | Path: gitlab.PathConfigStorage, Storage: l, 27 | }) 28 | require.NoError(t, err) 29 | require.NotNil(t, resp) 30 | require.NoError(t, resp.Error()) 31 | assert.Empty(t, resp.Data) 32 | }) 33 | 34 | t.Run("multiple configs", func(t *testing.T) { 35 | ctx := getCtxGitlabClient(t, "unit") 36 | var b, l, events, err = getBackendWithEventsAndConfigName(ctx, 37 | map[string]any{ 38 | "token": getGitlabToken("admin_user_root").Token, 39 | "base_url": cmp.Or(os.Getenv("GITLAB_URL"), "http://localhost:8080/"), 40 | "type": gitlab.TypeSaaS.String(), 41 | }, 42 | gitlab.DefaultConfigName, 43 | ) 44 | require.NoError(t, err) 45 | require.NotNil(t, events) 46 | require.NotNil(t, b) 47 | require.NotNil(t, l) 48 | 49 | require.NoError(t, 50 | writeBackendConfigWithName(ctx, b, l, 51 | map[string]any{ 52 | "token": getGitlabToken("admin_user_initial_token").Token, 53 | "base_url": cmp.Or(os.Getenv("GITLAB_URL"), "http://localhost:8080/"), 54 | "type": gitlab.TypeSelfManaged.String(), 55 | }, 56 | "admin", 57 | ), 58 | ) 59 | 60 | require.NoError(t, 61 | writeBackendConfigWithName(ctx, b, l, 62 | map[string]any{ 63 | "token": getGitlabToken("normal_user_initial_token").Token, 64 | "base_url": cmp.Or(os.Getenv("GITLAB_URL"), "http://localhost:8080/"), 65 | "type": gitlab.TypeDedicated.String(), 66 | }, 67 | "normal", 68 | ), 69 | ) 70 | 71 | resp, err := b.HandleRequest(ctx, &logical.Request{ 72 | Operation: logical.ListOperation, 73 | Path: gitlab.PathConfigStorage, Storage: l, 74 | }) 75 | require.NoError(t, err) 76 | require.NotNil(t, resp) 77 | require.NoError(t, resp.Error()) 78 | require.NotNil(t, resp.Data["keys"]) 79 | keysResponse := resp.Data["keys"].([]string) 80 | slices.Sort(keysResponse) 81 | keysExpected := []string{gitlab.DefaultConfigName, "admin", "normal"} 82 | slices.Sort(keysExpected) 83 | require.EqualValues(t, keysExpected, keysResponse) 84 | require.Len(t, keysResponse, 3) 85 | 86 | events.expectEvents(t, []expectedEvent{ 87 | {eventType: "gitlab/config-write"}, 88 | {eventType: "gitlab/config-write"}, 89 | {eventType: "gitlab/config-write"}, 90 | }) 91 | 92 | resp, err = b.HandleRequest(ctx, &logical.Request{ 93 | Operation: logical.ReadOperation, 94 | Path: fmt.Sprintf("%s/%s", gitlab.PathConfigStorage, gitlab.DefaultConfigName), Storage: l, 95 | }) 96 | require.NoError(t, err) 97 | require.NotNil(t, resp) 98 | require.NotEmpty(t, resp.Data) 99 | require.EqualValues(t, gitlab.TypeSaaS.String(), resp.Data["type"]) 100 | 101 | resp, err = b.HandleRequest(ctx, &logical.Request{ 102 | Operation: logical.ReadOperation, 103 | Path: fmt.Sprintf("%s/normal", gitlab.PathConfigStorage), Storage: l, 104 | }) 105 | require.NoError(t, err) 106 | require.NotNil(t, resp) 107 | require.NotEmpty(t, resp.Data) 108 | require.EqualValues(t, gitlab.TypeDedicated.String(), resp.Data["type"]) 109 | 110 | resp, err = b.HandleRequest(ctx, &logical.Request{ 111 | Operation: logical.ReadOperation, 112 | Path: fmt.Sprintf("%s/admin", gitlab.PathConfigStorage), Storage: l, 113 | }) 114 | require.NoError(t, err) 115 | require.NotNil(t, resp) 116 | require.NotEmpty(t, resp.Data) 117 | require.EqualValues(t, gitlab.TypeSelfManaged.String(), resp.Data["type"]) 118 | }) 119 | } 120 | -------------------------------------------------------------------------------- /path_config_rotate.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "cmp" 5 | "context" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "time" 10 | 11 | "github.com/hashicorp/vault/sdk/framework" 12 | "github.com/hashicorp/vault/sdk/logical" 13 | ) 14 | 15 | const pathConfigRotateHelpSynopsis = `Rotate the gitlab token for this configuration.` 16 | 17 | const pathConfigRotateHelpDescription = ` 18 | This endpoint allows you to rotate the GitLab token associated with your current configuration. When you invoke this 19 | operation, Vault securely generates a new token and replaces the existing one revealing the new token to you. It 20 | will only reveal it once, after that you will be unable to retrieve it. The newly generated token is securely 21 | stored within Vault's internal storage, ensuring that only Vault has access to it for future use when interacting 22 | with the GitLab API.'` 23 | 24 | func pathConfigTokenRotate(b *Backend) *framework.Path { 25 | return &framework.Path{ 26 | HelpSynopsis: strings.TrimSpace(pathConfigRotateHelpSynopsis), 27 | HelpDescription: strings.TrimSpace(pathConfigRotateHelpDescription), 28 | Pattern: fmt.Sprintf("%s/%s/rotate$", PathConfigStorage, framework.GenericNameRegex("config_name")), 29 | Fields: FieldSchemaConfig, 30 | DisplayAttrs: &framework.DisplayAttributes{ 31 | OperationPrefix: operationPrefixGitlabAccessTokens, 32 | }, 33 | Operations: map[logical.Operation]framework.OperationHandler{ 34 | logical.UpdateOperation: &framework.PathOperation{ 35 | Callback: b.pathConfigTokenRotate, 36 | DisplayAttrs: &framework.DisplayAttributes{OperationVerb: "configure"}, 37 | Summary: "Rotate the main Gitlab Access Token.", 38 | }, 39 | }, 40 | } 41 | } 42 | 43 | func (b *Backend) checkAndRotateConfigToken(ctx context.Context, request *logical.Request, config *EntryConfig) error { 44 | var err error 45 | b.Logger().Debug("Running check and rotate config token") 46 | if time.Until(config.TokenExpiresAt) <= config.AutoRotateBefore { 47 | _, err = b.pathConfigTokenRotate(ctx, request, &framework.FieldData{ 48 | Raw: map[string]interface{}{ 49 | "config_name": cmp.Or(config.Name, TypeConfigDefault), 50 | }, 51 | Schema: FieldSchemaConfig, 52 | }) 53 | } 54 | return err 55 | } 56 | 57 | func (b *Backend) pathConfigTokenRotate(ctx context.Context, request *logical.Request, data *framework.FieldData) (lResp *logical.Response, err error) { 58 | var name = data.Get("config_name").(string) 59 | b.Logger().Debug("Running pathConfigTokenRotate") 60 | var config *EntryConfig 61 | var client Client 62 | 63 | b.lockClientMutex.RLock() 64 | if config, err = getConfig(ctx, request.Storage, name); err != nil { 65 | b.lockClientMutex.RUnlock() 66 | b.Logger().Error("Failed to fetch configuration", "error", err.Error()) 67 | return nil, err 68 | } 69 | b.lockClientMutex.RUnlock() 70 | 71 | if config == nil { 72 | // no configuration yet so we don't need to rotate anything 73 | return logical.ErrorResponse(ErrBackendNotConfigured.Error()), nil 74 | } 75 | 76 | if client, err = b.getClient(ctx, request.Storage, name); err != nil { 77 | return nil, err 78 | } 79 | 80 | var entryToken *TokenConfig 81 | entryToken, _, err = client.RotateCurrentToken(ctx) 82 | if err != nil { 83 | b.Logger().Error("Failed to rotate main token", "err", err) 84 | return nil, err 85 | } 86 | 87 | config.Token = entryToken.Token.Token 88 | config.TokenId = entryToken.TokenID 89 | config.Scopes = entryToken.Scopes 90 | if entryToken.CreatedAt != nil { 91 | config.TokenCreatedAt = *entryToken.CreatedAt 92 | } 93 | if entryToken.ExpiresAt != nil { 94 | config.TokenExpiresAt = *entryToken.ExpiresAt 95 | } 96 | b.lockClientMutex.Lock() 97 | defer b.lockClientMutex.Unlock() 98 | err = saveConfig(ctx, *config, request.Storage) 99 | if err != nil { 100 | b.Logger().Error("failed to store configuration for revocation", "err", err) 101 | return nil, err 102 | } 103 | 104 | lResp = &logical.Response{Data: config.LogicalResponseData(b.flags.ShowConfigToken)} 105 | lResp.Data["token"] = config.Token 106 | event(ctx, b.Backend, "config-token-rotate", map[string]string{ 107 | "path": fmt.Sprintf("%s/%s", PathConfigStorage, name), 108 | "expires_at": entryToken.ExpiresAt.Format(time.RFC3339), 109 | "created_at": entryToken.CreatedAt.Format(time.RFC3339), 110 | "scopes": strings.Join(entryToken.Scopes, ", "), 111 | "token_id": strconv.Itoa(entryToken.TokenID), 112 | "name": entryToken.Name, 113 | "config_name": entryToken.ConfigName, 114 | }) 115 | 116 | b.SetClient(nil, name) 117 | return lResp, err 118 | } 119 | -------------------------------------------------------------------------------- /path_config_rotate_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/hashicorp/vault/sdk/logical" 10 | "github.com/stretchr/testify/require" 11 | 12 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 13 | ) 14 | 15 | func TestPathConfigRotate(t *testing.T) { 16 | t.Run("initial config should be empty fail with backend not configured", func(t *testing.T) { 17 | ctx := getCtxGitlabClient(t, "unit") 18 | b, l, err := getBackend(ctx) 19 | require.NoError(t, err) 20 | resp, err := b.HandleRequest(ctx, &logical.Request{ 21 | Operation: logical.UpdateOperation, 22 | Path: fmt.Sprintf("%s/%s/rotate", gitlab.PathConfigStorage, gitlab.DefaultConfigName), Storage: l, 23 | }) 24 | require.NoError(t, err) 25 | require.NotNil(t, resp) 26 | require.Error(t, resp.Error()) 27 | require.EqualValues(t, resp.Error(), gitlab.ErrBackendNotConfigured) 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /path_flags.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/hashicorp/vault/sdk/framework" 10 | "github.com/hashicorp/vault/sdk/logical" 11 | "github.com/mitchellh/mapstructure" 12 | ) 13 | 14 | const ( 15 | PathConfigFlags = "flags" 16 | ) 17 | 18 | var FieldSchemaFlags = map[string]*framework.FieldSchema{ 19 | "show_config_token": { 20 | Type: framework.TypeBool, 21 | Description: "Should we display the token value for the roles?", 22 | Default: false, 23 | DisplayAttrs: &framework.DisplayAttributes{Name: "Show Config Token"}, 24 | }, 25 | } 26 | 27 | func (b *Backend) pathFlagsRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (lResp *logical.Response, err error) { 28 | b.lockFlagsMutex.RLock() 29 | defer b.lockFlagsMutex.RUnlock() 30 | var flagData map[string]any 31 | err = mapstructure.Decode(b.flags, &flagData) 32 | return &logical.Response{Data: flagData}, err 33 | } 34 | 35 | func (b *Backend) pathFlagsUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (lResp *logical.Response, err error) { 36 | b.lockFlagsMutex.Lock() 37 | defer b.lockFlagsMutex.Unlock() 38 | 39 | var eventData = make(map[string]string) 40 | 41 | if showConfigToken, ok := data.GetOk("show_config_token"); ok { 42 | b.flags.ShowConfigToken = showConfigToken.(bool) 43 | eventData["show_config_token"] = strconv.FormatBool(b.flags.ShowConfigToken) 44 | } 45 | 46 | event(ctx, b.Backend, "flags-write", eventData) 47 | 48 | var flagData map[string]any 49 | err = mapstructure.Decode(b.flags, &flagData) 50 | return &logical.Response{Data: flagData}, err 51 | } 52 | 53 | func pathFlags(b *Backend) *framework.Path { 54 | var operations = map[logical.Operation]framework.OperationHandler{ 55 | logical.ReadOperation: &framework.PathOperation{ 56 | Callback: b.pathFlagsRead, 57 | DisplayAttrs: &framework.DisplayAttributes{ 58 | OperationVerb: "read", 59 | OperationSuffix: "flags", 60 | }, 61 | Summary: "Read the flags for the plugin.", 62 | Responses: map[int][]framework.Response{ 63 | http.StatusOK: {{ 64 | Description: http.StatusText(http.StatusOK), 65 | Fields: FieldSchemaFlags, 66 | }}, 67 | }, 68 | }, 69 | } 70 | 71 | if b.flags.AllowRuntimeFlagsChange { 72 | operations[logical.UpdateOperation] = &framework.PathOperation{ 73 | Callback: b.pathFlagsUpdate, 74 | DisplayAttrs: &framework.DisplayAttributes{ 75 | OperationVerb: "update", 76 | OperationSuffix: "flags", 77 | }, 78 | Summary: "Update the flags for the plugin.", 79 | Responses: map[int][]framework.Response{ 80 | http.StatusOK: {{ 81 | Description: http.StatusText(http.StatusOK), 82 | Fields: FieldSchemaFlags, 83 | }}, 84 | http.StatusBadRequest: {{ 85 | Description: http.StatusText(http.StatusBadRequest), 86 | Fields: FieldSchemaFlags, 87 | }}, 88 | }, 89 | } 90 | } 91 | 92 | return &framework.Path{ 93 | HelpSynopsis: strings.TrimSpace(pathFlagsHelpSynopsis), 94 | HelpDescription: strings.TrimSpace(pathFlagsHelpDescription), 95 | Pattern: PathConfigFlags, 96 | Fields: FieldSchemaFlags, 97 | DisplayAttrs: &framework.DisplayAttributes{ 98 | OperationPrefix: operationPrefixGitlabAccessTokens, 99 | }, 100 | Operations: operations, 101 | } 102 | } 103 | 104 | const pathFlagsHelpSynopsis = `Flags for the plugin.` 105 | 106 | const pathFlagsHelpDescription = `` 107 | -------------------------------------------------------------------------------- /path_flags_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/hashicorp/vault/sdk/logical" 9 | "github.com/stretchr/testify/require" 10 | 11 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 12 | ) 13 | 14 | func TestPathFlags(t *testing.T) { 15 | var ctx = t.Context() 16 | b, l, events, err := getBackendWithFlagsWithEvents(ctx, gitlab.Flags{AllowRuntimeFlagsChange: true}) 17 | require.NoError(t, err) 18 | 19 | resp, err := b.HandleRequest(ctx, &logical.Request{ 20 | Operation: logical.ReadOperation, 21 | Path: gitlab.PathConfigFlags, Storage: l, 22 | }) 23 | 24 | require.NoError(t, err) 25 | require.NotNil(t, resp) 26 | require.NoError(t, resp.Error()) 27 | require.False(t, resp.Data["show_config_token"].(bool)) 28 | 29 | resp, err = b.HandleRequest(ctx, &logical.Request{ 30 | Operation: logical.UpdateOperation, 31 | Path: gitlab.PathConfigFlags, Storage: l, 32 | Data: map[string]interface{}{ 33 | "show_config_token": "true", 34 | }, 35 | }) 36 | 37 | require.NoError(t, err) 38 | require.NotNil(t, resp) 39 | require.NoError(t, resp.Error()) 40 | require.True(t, resp.Data["show_config_token"].(bool)) 41 | 42 | events.expectEvents(t, []expectedEvent{ 43 | {eventType: "gitlab/flags-write"}, 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /path_role_deploy_tokens_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "cmp" 7 | "fmt" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/hashicorp/go-multierror" 13 | "github.com/hashicorp/vault/sdk/logical" 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | 17 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 18 | ) 19 | 20 | func TestPathRolesDeployTokens(t *testing.T) { 21 | var defaultConfig = map[string]any{ 22 | "token": getGitlabToken("admin_user_root").Token, 23 | "base_url": cmp.Or(os.Getenv("GITLAB_URL"), "http://localhost:8080/"), 24 | "type": gitlab.TypeSelfManaged.String(), 25 | } 26 | 27 | var tests = []struct { 28 | tokenType gitlab.TokenType 29 | accessLevel gitlab.AccessLevel 30 | scopes []string 31 | ttl string 32 | path string 33 | name string 34 | }{ 35 | { 36 | tokenType: gitlab.TokenTypeProjectDeploy, 37 | path: "example/example", 38 | scopes: []string{gitlab.TokenScopeReadRepository.String()}, 39 | }, 40 | { 41 | tokenType: gitlab.TokenTypeGroupDeploy, 42 | path: "test/test1", 43 | scopes: []string{gitlab.TokenScopeReadRepository.String()}, 44 | }, 45 | } 46 | 47 | for _, tt := range tests { 48 | t.Run(tt.tokenType.String(), func(t *testing.T) { 49 | t.Run("should create role successfully", func(t *testing.T) { 50 | ctx := getCtxGitlabClient(t, "unit") 51 | var b, l, err = getBackendWithConfig(ctx, defaultConfig) 52 | require.NoError(t, err) 53 | resp, err := b.HandleRequest(ctx, &logical.Request{ 54 | Operation: logical.CreateOperation, 55 | Path: fmt.Sprintf("%s/%d", gitlab.PathRoleStorage, time.Now().UnixNano()), Storage: l, 56 | Data: map[string]any{ 57 | "path": tt.path, 58 | "name": tt.name, 59 | "access_level": cmp.Or(tt.accessLevel, gitlab.AccessLevelUnknown).String(), 60 | "token_type": tt.tokenType.String(), 61 | "scopes": tt.scopes, 62 | "ttl": cmp.Or(tt.ttl, "1h"), 63 | }, 64 | }) 65 | require.NoError(t, err) 66 | require.NotNil(t, resp) 67 | }) 68 | 69 | t.Run("fail to create role due to missing scopes and wrong access level", func(t *testing.T) { 70 | ctx := getCtxGitlabClient(t, "unit") 71 | var b, l, err = getBackendWithConfig(ctx, defaultConfig) 72 | require.NoError(t, err) 73 | resp, err := b.HandleRequest(ctx, &logical.Request{ 74 | Operation: logical.CreateOperation, 75 | Path: fmt.Sprintf("%s/%d", gitlab.PathRoleStorage, time.Now().UnixNano()), Storage: l, 76 | Data: map[string]any{ 77 | "path": tt.path, 78 | "name": tt.name, 79 | "access_level": gitlab.AccessLevelNoPermissions.String(), 80 | "token_type": tt.tokenType.String(), 81 | "ttl": cmp.Or(tt.ttl, "1h"), 82 | "scopes": []string{}, 83 | }, 84 | }) 85 | require.Error(t, err) 86 | require.NotNil(t, resp) 87 | var errorMap = countErrByName(err.(*multierror.Error)) 88 | assert.EqualValues(t, 2, errorMap[gitlab.ErrFieldInvalidValue.Error()]) 89 | }) 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /path_role_pipeline_project_trigger_token_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "cmp" 7 | "fmt" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/hashicorp/go-multierror" 13 | "github.com/hashicorp/vault/sdk/logical" 14 | "github.com/stretchr/testify/assert" 15 | "github.com/stretchr/testify/require" 16 | 17 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 18 | ) 19 | 20 | func TestPathRolesPipelineProjectTrigger(t *testing.T) { 21 | var defaultConfig = map[string]any{ 22 | "token": getGitlabToken("admin_user_root").Token, 23 | "base_url": cmp.Or(os.Getenv("GITLAB_URL"), "http://localhost:8080/"), 24 | "type": gitlab.TypeSelfManaged.String(), 25 | } 26 | 27 | t.Run("should fail if have defined scopes or access level", func(t *testing.T) { 28 | ctx := getCtxGitlabClient(t, "unit") 29 | var b, l, err = getBackendWithConfig(ctx, defaultConfig) 30 | require.NoError(t, err) 31 | resp, err := b.HandleRequest(ctx, &logical.Request{ 32 | Operation: logical.CreateOperation, 33 | Path: fmt.Sprintf("%s/%d", gitlab.PathRoleStorage, time.Now().UnixNano()), Storage: l, 34 | Data: map[string]any{ 35 | "path": "user", 36 | "name": "Example user personal token", 37 | "access_level": gitlab.AccessLevelNoPermissions.String(), 38 | "token_type": gitlab.TokenTypePipelineProjectTrigger.String(), 39 | "scopes": []string{gitlab.TokenScopeApi.String()}, 40 | "ttl": "1h", 41 | }, 42 | }) 43 | require.Error(t, err) 44 | require.NotNil(t, resp) 45 | var errorMap = countErrByName(err.(*multierror.Error)) 46 | assert.EqualValues(t, 2, errorMap[gitlab.ErrFieldInvalidValue.Error()]) 47 | }) 48 | 49 | t.Run("ttl is set", func(t *testing.T) { 50 | ctx := getCtxGitlabClient(t, "unit") 51 | var b, l, err = getBackendWithConfig(ctx, defaultConfig) 52 | require.NoError(t, err) 53 | resp, err := b.HandleRequest(ctx, &logical.Request{ 54 | Operation: logical.CreateOperation, 55 | Path: fmt.Sprintf("%s/%d", gitlab.PathRoleStorage, time.Now().UnixNano()), Storage: l, 56 | Data: map[string]any{ 57 | "path": "user", 58 | "name": "Example user personal token", 59 | "access_level": gitlab.AccessLevelUnknown.String(), 60 | "token_type": gitlab.TokenTypePipelineProjectTrigger.String(), 61 | "scopes": []string{}, 62 | "ttl": "1h", 63 | }, 64 | }) 65 | require.NoError(t, err) 66 | require.NotNil(t, resp) 67 | require.EqualValues(t, 3600, resp.Data["ttl"]) 68 | }) 69 | 70 | t.Run("ttl is optional", func(t *testing.T) { 71 | ctx := getCtxGitlabClient(t, "unit") 72 | var b, l, err = getBackendWithConfig(ctx, defaultConfig) 73 | require.NoError(t, err) 74 | resp, err := b.HandleRequest(ctx, &logical.Request{ 75 | Operation: logical.CreateOperation, 76 | Path: fmt.Sprintf("%s/%d", gitlab.PathRoleStorage, time.Now().UnixNano()), Storage: l, 77 | Data: map[string]any{ 78 | "path": "user", 79 | "name": "Example user personal token", 80 | "access_level": gitlab.AccessLevelUnknown.String(), 81 | "token_type": gitlab.TokenTypePipelineProjectTrigger.String(), 82 | "scopes": []string{}, 83 | }, 84 | }) 85 | require.NoError(t, err) 86 | require.NotNil(t, resp) 87 | require.EqualValues(t, 0, resp.Data["ttl"]) 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /secret_access_tokens_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/hashicorp/vault/sdk/logical" 10 | "github.com/stretchr/testify/require" 11 | 12 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 13 | ) 14 | 15 | func TestSecretAccessTokenRevokeToken(t *testing.T) { 16 | httpClient, url := getClient(t, "unit") 17 | ctx := gitlab.HttpClientNewContext(t.Context(), httpClient) 18 | 19 | b, l, events, err := getBackendWithEvents(ctx) 20 | require.NoError(t, err) 21 | 22 | t.Run("nil storage", func(t *testing.T) { 23 | events.resetEvents(t) 24 | resp, err := b.Secret(gitlab.SecretAccessTokenType).HandleRevoke(ctx, &logical.Request{}) 25 | require.Error(t, err) 26 | require.Nil(t, resp) 27 | require.ErrorIs(t, err, gitlab.ErrNilValue) 28 | events.expectEvents(t, []expectedEvent{}) 29 | }) 30 | 31 | t.Run("nil secret", func(t *testing.T) { 32 | events.resetEvents(t) 33 | resp, err := b.HandleRequest(ctx, &logical.Request{ 34 | Operation: logical.UpdateOperation, 35 | Path: fmt.Sprintf("%s/%s", gitlab.PathConfigStorage, gitlab.DefaultConfigName), Storage: l, 36 | Data: map[string]any{ 37 | "token": getGitlabToken("admin_user_root").Token, 38 | "base_url": url, 39 | "auto_rotate_token": true, 40 | "auto_rotate_before": "24h", 41 | "type": gitlab.TypeSelfManaged.String(), 42 | }, 43 | }) 44 | 45 | require.NoError(t, err) 46 | require.NotNil(t, resp) 47 | require.NoError(t, resp.Error()) 48 | require.NotEmpty(t, events) 49 | 50 | resp, err = b.Secret(gitlab.SecretAccessTokenType).HandleRevoke(ctx, &logical.Request{Storage: l}) 51 | require.Error(t, err) 52 | require.Nil(t, resp) 53 | require.ErrorIs(t, err, gitlab.ErrNilValue) 54 | 55 | events.expectEvents(t, []expectedEvent{ 56 | {eventType: "gitlab/config-write"}, 57 | }) 58 | 59 | }) 60 | 61 | t.Run("token_id invalid value", func(t *testing.T) { 62 | events.resetEvents(t) 63 | resp, err := b.HandleRequest(ctx, &logical.Request{ 64 | Operation: logical.UpdateOperation, 65 | Path: fmt.Sprintf("%s/%s", gitlab.PathConfigStorage, gitlab.DefaultConfigName), Storage: l, 66 | Data: map[string]any{ 67 | "token": getGitlabToken("admin_user_root").Token, 68 | "base_url": url, 69 | "auto_rotate_token": true, 70 | "auto_rotate_before": "24h", 71 | "type": gitlab.TypeSelfManaged.String(), 72 | }, 73 | }) 74 | 75 | require.NoError(t, err) 76 | require.NotNil(t, resp) 77 | require.NoError(t, resp.Error()) 78 | require.NotEmpty(t, events) 79 | 80 | resp, err = b.Secret(gitlab.SecretAccessTokenType).HandleRevoke(ctx, &logical.Request{ 81 | Storage: l, 82 | Secret: &logical.Secret{ 83 | InternalData: map[string]interface{}{ 84 | "token_id": "asdf", 85 | }, 86 | }, 87 | }) 88 | require.Error(t, err) 89 | require.Nil(t, resp) 90 | require.ErrorIs(t, err, gitlab.ErrInvalidValue) 91 | 92 | events.expectEvents(t, []expectedEvent{ 93 | {eventType: "gitlab/config-write"}, 94 | }) 95 | }) 96 | 97 | } 98 | -------------------------------------------------------------------------------- /testdata/gitlab-com: -------------------------------------------------------------------------------- 1 | 2025-04-04T20:44:18.754Z -------------------------------------------------------------------------------- /testdata/gitlab-selfhosted: -------------------------------------------------------------------------------- 1 | 2025-04-04T20:55:28.379Z -------------------------------------------------------------------------------- /testdata/tokens.json: -------------------------------------------------------------------------------- 1 | { 2 | "admin_user_auto_rotate_token_1": { 3 | "created_at": "2025-04-04 18:35:29.407 +0000 UTC", 4 | "id": "3:4", 5 | "token": "glpat-QqqjWLooAa55hKFbWE8w" 6 | }, 7 | "admin_user_auto_rotate_token_main_token": { 8 | "created_at": "2025-04-04 18:35:29.489 +0000 UTC", 9 | "id": "3:6", 10 | "token": "glpat-qi3_APbMtb-SCbUFd-eM" 11 | }, 12 | "admin_user_initial_token": { 13 | "created_at": "2025-04-04 18:35:29.419 +0000 UTC", 14 | "id": "3:5", 15 | "token": "glpat-va5WDH4vz9bE1KeAR2C2" 16 | }, 17 | "admin_user_root": { 18 | "created_at": "2025-04-04 18:35:29.407 +0000 UTC", 19 | "id": "3:3", 20 | "token": "glpat-wU8yWBGat-nypZcyf1LL" 21 | }, 22 | "normal_user_initial_token": { 23 | "created_at": "2025-04-04 18:35:29.099 +0000 UTC", 24 | "id": "2:2", 25 | "token": "glpat-RKEynyYigffJp45zNFD-" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /testdata/unit/TestBackend.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestGitlabClient_CurrentTokenInfo.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 0 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: localhost:8080 13 | remote_addr: "" 14 | request_uri: "" 15 | body: "" 16 | form: {} 17 | headers: 18 | Accept: 19 | - application/json 20 | Private-Token: 21 | - REPLACED-TOKEN 22 | User-Agent: 23 | - go-gitlab 24 | url: http://localhost:8080/api/v4/personal_access_tokens/self 25 | method: GET 26 | response: 27 | proto: HTTP/1.1 28 | proto_major: 1 29 | proto_minor: 1 30 | transfer_encoding: [] 31 | trailer: {} 32 | content_length: 234 33 | uncompressed: false 34 | body: '{"id":3,"name":"Root token","revoked":false,"created_at":"2025-04-04T18:35:29.407Z","description":null,"scopes":["admin_mode","api","sudo"],"user_id":3,"last_used_at":"2025-04-04T19:59:18.239Z","active":true,"expires_at":"2026-04-03"}' 35 | headers: 36 | Cache-Control: 37 | - max-age=0, private, must-revalidate 38 | Connection: 39 | - keep-alive 40 | Content-Length: 41 | - "234" 42 | Content-Type: 43 | - application/json 44 | Date: 45 | - Fri, 04 Apr 2025 19:59:20 GMT 46 | Etag: 47 | - W/"dc59f4cab933045578c753eb12954522" 48 | Referrer-Policy: 49 | - strict-origin-when-cross-origin 50 | Server: 51 | - nginx 52 | Strict-Transport-Security: 53 | - max-age=63072000 54 | Vary: 55 | - Origin 56 | X-Content-Type-Options: 57 | - nosniff 58 | X-Frame-Options: 59 | - SAMEORIGIN 60 | X-Gitlab-Meta: 61 | - '{"correlation_id":"01JR165W85ZHHAAB9TACSXC7AA","version":"1"}' 62 | X-Request-Id: 63 | - 01JR165W85ZHHAAB9TACSXC7AA 64 | X-Runtime: 65 | - "0.017223" 66 | status: 200 OK 67 | code: 200 68 | duration: 19.967042ms 69 | -------------------------------------------------------------------------------- /testdata/unit/TestGitlabClient_GetUserIdByUsername.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 0 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: localhost:8080 13 | remote_addr: "" 14 | request_uri: "" 15 | body: "" 16 | form: {} 17 | headers: 18 | Accept: 19 | - application/json 20 | Private-Token: 21 | - REPLACED-TOKEN 22 | User-Agent: 23 | - go-gitlab 24 | url: http://localhost:8080/api/v4/users?username=root 25 | method: GET 26 | response: 27 | proto: HTTP/1.1 28 | proto_major: 1 29 | proto_minor: 1 30 | transfer_encoding: 31 | - chunked 32 | trailer: {} 33 | content_length: -1 34 | uncompressed: true 35 | body: '[{"id":1,"username":"root","name":"Administrator","state":"active","locked":false,"avatar_url":"https://www.gravatar.com/avatar/c0d13fdce10dba44c97ccfef9ce4578626652a9a45c4fbb8be86db84161ac4af?s=80\u0026d=identicon","web_url":"http://dce56ec495e2/root","created_at":"2025-04-04T18:34:18.395Z","bio":"","location":"","public_email":null,"skype":"","linkedin":"","twitter":"","discord":"","website_url":"","organization":"","job_title":"","pronouns":null,"bot":false,"work_information":null,"followers":0,"following":0,"is_followed":false,"local_time":null,"last_sign_in_at":null,"confirmed_at":"2025-04-04T18:34:18.287Z","last_activity_on":"2025-04-04","email":"gitlab_admin_747857@example.com","theme_id":3,"color_scheme_id":1,"projects_limit":100000,"current_sign_in_at":null,"identities":[],"can_create_group":true,"can_create_project":true,"two_factor_enabled":false,"external":false,"private_profile":false,"commit_email":"gitlab_admin_747857@example.com","is_admin":true,"note":null,"namespace_id":1,"created_by":null,"email_reset_offered_at":null}]' 36 | headers: 37 | Cache-Control: 38 | - max-age=0, private, must-revalidate 39 | Connection: 40 | - keep-alive 41 | Content-Type: 42 | - application/json 43 | Date: 44 | - Fri, 04 Apr 2025 19:59:19 GMT 45 | Etag: 46 | - W/"b9fb952ce8405b215e5ecf9c3220caf6" 47 | Link: 48 | - ; rel="first", ; rel="last" 49 | Referrer-Policy: 50 | - strict-origin-when-cross-origin 51 | Server: 52 | - nginx 53 | Strict-Transport-Security: 54 | - max-age=63072000 55 | Vary: 56 | - Accept-Encoding 57 | - Origin 58 | X-Content-Type-Options: 59 | - nosniff 60 | X-Frame-Options: 61 | - SAMEORIGIN 62 | X-Gitlab-Meta: 63 | - '{"correlation_id":"01JR165TZB2KH4ZF2B51DBE4A6","version":"1"}' 64 | X-Next-Page: 65 | - "" 66 | X-Page: 67 | - "1" 68 | X-Per-Page: 69 | - "20" 70 | X-Prev-Page: 71 | - "" 72 | X-Request-Id: 73 | - 01JR165TZB2KH4ZF2B51DBE4A6 74 | X-Runtime: 75 | - "0.271212" 76 | X-Total: 77 | - "1" 78 | X-Total-Pages: 79 | - "1" 80 | status: 200 OK 81 | code: 200 82 | duration: 274.43625ms 83 | -------------------------------------------------------------------------------- /testdata/unit/TestGitlabClient_Metadata.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 0 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: localhost:8080 13 | remote_addr: "" 14 | request_uri: "" 15 | body: "" 16 | form: {} 17 | headers: 18 | Accept: 19 | - application/json 20 | Private-Token: 21 | - REPLACED-TOKEN 22 | User-Agent: 23 | - go-gitlab 24 | url: http://localhost:8080/api/v4/metadata 25 | method: GET 26 | response: 27 | proto: HTTP/1.1 28 | proto_major: 1 29 | proto_minor: 1 30 | transfer_encoding: [] 31 | trailer: {} 32 | content_length: 236 33 | uncompressed: false 34 | body: '{"version":"17.10.3","revision":"22d4014a923","kas":{"enabled":true,"externalUrl":"ws://dce56ec495e2/-/kubernetes-agent/","externalK8sProxyUrl":"http://dce56ec495e2/-/kubernetes-agent/k8s-proxy/","version":"17.10.3"},"enterprise":false}' 35 | headers: 36 | Cache-Control: 37 | - max-age=0, private, must-revalidate 38 | Connection: 39 | - keep-alive 40 | Content-Length: 41 | - "236" 42 | Content-Type: 43 | - application/json 44 | Date: 45 | - Fri, 04 Apr 2025 19:59:20 GMT 46 | Etag: 47 | - W/"c7db13e569c99229215906145ea2e48a" 48 | Referrer-Policy: 49 | - strict-origin-when-cross-origin 50 | Server: 51 | - nginx 52 | Strict-Transport-Security: 53 | - max-age=63072000 54 | Vary: 55 | - Origin 56 | X-Content-Type-Options: 57 | - nosniff 58 | X-Frame-Options: 59 | - SAMEORIGIN 60 | X-Gitlab-Meta: 61 | - '{"correlation_id":"01JR165W9KH5PZ7E5FGNSFP122","version":"1"}' 62 | X-Request-Id: 63 | - 01JR165W9KH5PZ7E5FGNSFP122 64 | X-Runtime: 65 | - "0.111073" 66 | status: 200 OK 67 | code: 200 68 | duration: 114.099ms 69 | -------------------------------------------------------------------------------- /testdata/unit/TestPathConfigList_empty_list.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathConfigRotate_initial_config_should_be_empty_fail_with_backend_not_configured.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathConfig_AutoRotateToken_call_auto_rotate_the_main_token_and_rotate_the_token.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathConfig_AutoRotateToken_call_auto_rotate_the_main_token_but_the_token_is_still_valid.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathConfig_AutoRotateToken_no_error_when_auto_rotate_is_disabled_and_config_is_not_set.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathConfig_AutoRotateToken_no_error_when_auto_rotate_is_disabled_and_config_is_set.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathConfig_AutoRotate_auto_rotate_before_cannot_be_more_than_the_minimal_value.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathConfig_AutoRotate_auto_rotate_before_should_be_between_the_min_and_max_value.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathConfig_AutoRotate_auto_rotate_before_should_be_less_than_the_maximal_limit.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathConfig_AutoRotate_auto_rotate_before_should_be_more_than_the_minimal_limit.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathConfig_deleting_uninitialized_config_should_fail_with_backend_not_configured.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathConfig_initial_config_should_be_empty_fail_with_backend_not_configured.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathConfig_invalid_token.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 0 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: localhost:8080 13 | remote_addr: "" 14 | request_uri: "" 15 | body: "" 16 | form: {} 17 | headers: 18 | Accept: 19 | - application/json 20 | Private-Token: 21 | - REPLACED-TOKEN 22 | User-Agent: 23 | - go-gitlab 24 | url: http://localhost:8080/api/v4/personal_access_tokens/self 25 | method: GET 26 | response: 27 | proto: HTTP/1.1 28 | proto_major: 1 29 | proto_minor: 1 30 | transfer_encoding: [] 31 | trailer: {} 32 | content_length: 30 33 | uncompressed: false 34 | body: '{"message":"401 Unauthorized"}' 35 | headers: 36 | Cache-Control: 37 | - no-cache 38 | Connection: 39 | - keep-alive 40 | Content-Length: 41 | - "30" 42 | Content-Type: 43 | - application/json 44 | Date: 45 | - Fri, 04 Apr 2025 19:59:23 GMT 46 | Server: 47 | - nginx 48 | Strict-Transport-Security: 49 | - max-age=63072000 50 | Vary: 51 | - Origin 52 | X-Content-Type-Options: 53 | - nosniff 54 | X-Frame-Options: 55 | - SAMEORIGIN 56 | X-Gitlab-Meta: 57 | - '{"correlation_id":"01JR165Z2B40TPKJQS6CW3EZF0","version":"1"}' 58 | X-Request-Id: 59 | - 01JR165Z2B40TPKJQS6CW3EZF0 60 | X-Runtime: 61 | - "0.010139" 62 | status: 401 Unauthorized 63 | code: 401 64 | duration: 13.059584ms 65 | -------------------------------------------------------------------------------- /testdata/unit/TestPathConfig_missing_token_from_the_request.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathConfig_patch_a_config_no_backend.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathConfig_patch_a_config_with_no_storage.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathConfig_write_read_delete_and_read_config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 0 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: localhost:8080 13 | remote_addr: "" 14 | request_uri: "" 15 | body: "" 16 | form: {} 17 | headers: 18 | Accept: 19 | - application/json 20 | Private-Token: 21 | - REPLACED-TOKEN 22 | User-Agent: 23 | - go-gitlab 24 | url: http://localhost:8080/api/v4/personal_access_tokens/self 25 | method: GET 26 | response: 27 | proto: HTTP/1.1 28 | proto_major: 1 29 | proto_minor: 1 30 | transfer_encoding: [] 31 | trailer: {} 32 | content_length: 234 33 | uncompressed: false 34 | body: '{"id":3,"name":"Root token","revoked":false,"created_at":"2025-04-04T18:35:29.407Z","description":null,"scopes":["admin_mode","api","sudo"],"user_id":3,"last_used_at":"2025-04-04T19:59:18.239Z","active":true,"expires_at":"2026-04-03"}' 35 | headers: 36 | Cache-Control: 37 | - max-age=0, private, must-revalidate 38 | Connection: 39 | - keep-alive 40 | Content-Length: 41 | - "234" 42 | Content-Type: 43 | - application/json 44 | Date: 45 | - Fri, 04 Apr 2025 19:59:23 GMT 46 | Etag: 47 | - W/"dc59f4cab933045578c753eb12954522" 48 | Referrer-Policy: 49 | - strict-origin-when-cross-origin 50 | Server: 51 | - nginx 52 | Strict-Transport-Security: 53 | - max-age=63072000 54 | Vary: 55 | - Origin 56 | X-Content-Type-Options: 57 | - nosniff 58 | X-Frame-Options: 59 | - SAMEORIGIN 60 | X-Gitlab-Meta: 61 | - '{"correlation_id":"01JR165YR0K6YC868EHBGF17KV","version":"1"}' 62 | X-Request-Id: 63 | - 01JR165YR0K6YC868EHBGF17KV 64 | X-Runtime: 65 | - "0.012333" 66 | status: 200 OK 67 | code: 200 68 | duration: 14.759875ms 69 | - id: 1 70 | request: 71 | proto: HTTP/1.1 72 | proto_major: 1 73 | proto_minor: 1 74 | content_length: 0 75 | transfer_encoding: [] 76 | trailer: {} 77 | host: localhost:8080 78 | remote_addr: "" 79 | request_uri: "" 80 | body: "" 81 | form: {} 82 | headers: 83 | Accept: 84 | - application/json 85 | Private-Token: 86 | - REPLACED-TOKEN 87 | User-Agent: 88 | - go-gitlab 89 | url: http://localhost:8080/api/v4/metadata 90 | method: GET 91 | response: 92 | proto: HTTP/1.1 93 | proto_major: 1 94 | proto_minor: 1 95 | transfer_encoding: [] 96 | trailer: {} 97 | content_length: 236 98 | uncompressed: false 99 | body: '{"version":"17.10.3","revision":"22d4014a923","kas":{"enabled":true,"externalUrl":"ws://dce56ec495e2/-/kubernetes-agent/","externalK8sProxyUrl":"http://dce56ec495e2/-/kubernetes-agent/k8s-proxy/","version":"17.10.3"},"enterprise":false}' 100 | headers: 101 | Cache-Control: 102 | - max-age=0, private, must-revalidate 103 | Connection: 104 | - keep-alive 105 | Content-Length: 106 | - "236" 107 | Content-Type: 108 | - application/json 109 | Date: 110 | - Fri, 04 Apr 2025 19:59:23 GMT 111 | Etag: 112 | - W/"c7db13e569c99229215906145ea2e48a" 113 | Referrer-Policy: 114 | - strict-origin-when-cross-origin 115 | Server: 116 | - nginx 117 | Strict-Transport-Security: 118 | - max-age=63072000 119 | Vary: 120 | - Origin 121 | X-Content-Type-Options: 122 | - nosniff 123 | X-Frame-Options: 124 | - SAMEORIGIN 125 | X-Gitlab-Meta: 126 | - '{"correlation_id":"01JR165YRZSHNRN4CYJ36SF2WX","version":"1"}' 127 | X-Request-Id: 128 | - 01JR165YRZSHNRN4CYJ36SF2WX 129 | X-Runtime: 130 | - "0.016040" 131 | status: 200 OK 132 | code: 200 133 | duration: 18.17425ms 134 | -------------------------------------------------------------------------------- /testdata/unit/TestPathRolesList_empty_list.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathRolesPipelineProjectTrigger_ttl_is_set.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 0 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: localhost:8080 13 | remote_addr: "" 14 | request_uri: "" 15 | body: "" 16 | form: {} 17 | headers: 18 | Accept: 19 | - application/json 20 | Private-Token: 21 | - REPLACED-TOKEN 22 | User-Agent: 23 | - go-gitlab 24 | url: http://localhost:8080/api/v4/personal_access_tokens/self 25 | method: GET 26 | response: 27 | proto: HTTP/1.1 28 | proto_major: 1 29 | proto_minor: 1 30 | transfer_encoding: [] 31 | trailer: {} 32 | content_length: 234 33 | uncompressed: false 34 | body: '{"id":3,"name":"Root token","revoked":false,"created_at":"2025-04-04T18:35:29.407Z","description":null,"scopes":["admin_mode","api","sudo"],"user_id":3,"last_used_at":"2025-04-04T19:59:18.239Z","active":true,"expires_at":"2026-04-03"}' 35 | headers: 36 | Cache-Control: 37 | - max-age=0, private, must-revalidate 38 | Connection: 39 | - keep-alive 40 | Content-Length: 41 | - "234" 42 | Content-Type: 43 | - application/json 44 | Date: 45 | - Fri, 04 Apr 2025 19:59:25 GMT 46 | Etag: 47 | - W/"dc59f4cab933045578c753eb12954522" 48 | Referrer-Policy: 49 | - strict-origin-when-cross-origin 50 | Server: 51 | - nginx 52 | Strict-Transport-Security: 53 | - max-age=63072000 54 | Vary: 55 | - Origin 56 | X-Content-Type-Options: 57 | - nosniff 58 | X-Frame-Options: 59 | - SAMEORIGIN 60 | X-Gitlab-Meta: 61 | - '{"correlation_id":"01JR1660M4R1RSNMGM1WX9PBWB","version":"1"}' 62 | X-Request-Id: 63 | - 01JR1660M4R1RSNMGM1WX9PBWB 64 | X-Runtime: 65 | - "0.015135" 66 | status: 200 OK 67 | code: 200 68 | duration: 27.585375ms 69 | - id: 1 70 | request: 71 | proto: HTTP/1.1 72 | proto_major: 1 73 | proto_minor: 1 74 | content_length: 0 75 | transfer_encoding: [] 76 | trailer: {} 77 | host: localhost:8080 78 | remote_addr: "" 79 | request_uri: "" 80 | body: "" 81 | form: {} 82 | headers: 83 | Accept: 84 | - application/json 85 | Private-Token: 86 | - REPLACED-TOKEN 87 | User-Agent: 88 | - go-gitlab 89 | url: http://localhost:8080/api/v4/metadata 90 | method: GET 91 | response: 92 | proto: HTTP/1.1 93 | proto_major: 1 94 | proto_minor: 1 95 | transfer_encoding: [] 96 | trailer: {} 97 | content_length: 236 98 | uncompressed: false 99 | body: '{"version":"17.10.3","revision":"22d4014a923","kas":{"enabled":true,"externalUrl":"ws://dce56ec495e2/-/kubernetes-agent/","externalK8sProxyUrl":"http://dce56ec495e2/-/kubernetes-agent/k8s-proxy/","version":"17.10.3"},"enterprise":false}' 100 | headers: 101 | Cache-Control: 102 | - max-age=0, private, must-revalidate 103 | Connection: 104 | - keep-alive 105 | Content-Length: 106 | - "236" 107 | Content-Type: 108 | - application/json 109 | Date: 110 | - Fri, 04 Apr 2025 19:59:25 GMT 111 | Etag: 112 | - W/"c7db13e569c99229215906145ea2e48a" 113 | Referrer-Policy: 114 | - strict-origin-when-cross-origin 115 | Server: 116 | - nginx 117 | Strict-Transport-Security: 118 | - max-age=63072000 119 | Vary: 120 | - Origin 121 | X-Content-Type-Options: 122 | - nosniff 123 | X-Frame-Options: 124 | - SAMEORIGIN 125 | X-Gitlab-Meta: 126 | - '{"correlation_id":"01JR1660NZHJWP0Z09DZ15ZFW3","version":"1"}' 127 | X-Request-Id: 128 | - 01JR1660NZHJWP0Z09DZ15ZFW3 129 | X-Runtime: 130 | - "0.017556" 131 | status: 200 OK 132 | code: 200 133 | duration: 20.264542ms 134 | -------------------------------------------------------------------------------- /testdata/unit/TestPathRolesTTL_general_ttl_limits_ttl__maxTTL.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 0 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: localhost:8080 13 | remote_addr: "" 14 | request_uri: "" 15 | body: "" 16 | form: {} 17 | headers: 18 | Accept: 19 | - application/json 20 | Private-Token: 21 | - REPLACED-TOKEN 22 | User-Agent: 23 | - go-gitlab 24 | url: http://localhost:8080/api/v4/personal_access_tokens/self 25 | method: GET 26 | response: 27 | proto: HTTP/1.1 28 | proto_major: 1 29 | proto_minor: 1 30 | transfer_encoding: [] 31 | trailer: {} 32 | content_length: 234 33 | uncompressed: false 34 | body: '{"id":3,"name":"Root token","revoked":false,"created_at":"2025-04-04T18:35:29.407Z","description":null,"scopes":["admin_mode","api","sudo"],"user_id":3,"last_used_at":"2025-04-04T19:59:18.239Z","active":true,"expires_at":"2026-04-03"}' 35 | headers: 36 | Cache-Control: 37 | - max-age=0, private, must-revalidate 38 | Connection: 39 | - keep-alive 40 | Content-Length: 41 | - "234" 42 | Content-Type: 43 | - application/json 44 | Date: 45 | - Fri, 04 Apr 2025 19:59:27 GMT 46 | Etag: 47 | - W/"dc59f4cab933045578c753eb12954522" 48 | Referrer-Policy: 49 | - strict-origin-when-cross-origin 50 | Server: 51 | - nginx 52 | Strict-Transport-Security: 53 | - max-age=63072000 54 | Vary: 55 | - Origin 56 | X-Content-Type-Options: 57 | - nosniff 58 | X-Frame-Options: 59 | - SAMEORIGIN 60 | X-Gitlab-Meta: 61 | - '{"correlation_id":"01JR1662CCGDEBD8EQ6ZAM9QJ3","version":"1"}' 62 | X-Request-Id: 63 | - 01JR1662CCGDEBD8EQ6ZAM9QJ3 64 | X-Runtime: 65 | - "0.011477" 66 | status: 200 OK 67 | code: 200 68 | duration: 13.686083ms 69 | - id: 1 70 | request: 71 | proto: HTTP/1.1 72 | proto_major: 1 73 | proto_minor: 1 74 | content_length: 0 75 | transfer_encoding: [] 76 | trailer: {} 77 | host: localhost:8080 78 | remote_addr: "" 79 | request_uri: "" 80 | body: "" 81 | form: {} 82 | headers: 83 | Accept: 84 | - application/json 85 | Private-Token: 86 | - REPLACED-TOKEN 87 | User-Agent: 88 | - go-gitlab 89 | url: http://localhost:8080/api/v4/metadata 90 | method: GET 91 | response: 92 | proto: HTTP/1.1 93 | proto_major: 1 94 | proto_minor: 1 95 | transfer_encoding: [] 96 | trailer: {} 97 | content_length: 236 98 | uncompressed: false 99 | body: '{"version":"17.10.3","revision":"22d4014a923","kas":{"enabled":true,"externalUrl":"ws://dce56ec495e2/-/kubernetes-agent/","externalK8sProxyUrl":"http://dce56ec495e2/-/kubernetes-agent/k8s-proxy/","version":"17.10.3"},"enterprise":false}' 100 | headers: 101 | Cache-Control: 102 | - max-age=0, private, must-revalidate 103 | Connection: 104 | - keep-alive 105 | Content-Length: 106 | - "236" 107 | Content-Type: 108 | - application/json 109 | Date: 110 | - Fri, 04 Apr 2025 19:59:27 GMT 111 | Etag: 112 | - W/"c7db13e569c99229215906145ea2e48a" 113 | Referrer-Policy: 114 | - strict-origin-when-cross-origin 115 | Server: 116 | - nginx 117 | Strict-Transport-Security: 118 | - max-age=63072000 119 | Vary: 120 | - Origin 121 | X-Content-Type-Options: 122 | - nosniff 123 | X-Frame-Options: 124 | - SAMEORIGIN 125 | X-Gitlab-Meta: 126 | - '{"correlation_id":"01JR1662DADXSE4XAM33NZ62CS","version":"1"}' 127 | X-Request-Id: 128 | - 01JR1662DADXSE4XAM33NZ62CS 129 | X-Runtime: 130 | - "0.016284" 131 | status: 200 OK 132 | code: 200 133 | duration: 18.516459ms 134 | -------------------------------------------------------------------------------- /testdata/unit/TestPathRolesTTL_vault_revokes_the_token_ttl__1h.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 0 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: localhost:8080 13 | remote_addr: "" 14 | request_uri: "" 15 | body: "" 16 | form: {} 17 | headers: 18 | Accept: 19 | - application/json 20 | Private-Token: 21 | - REPLACED-TOKEN 22 | User-Agent: 23 | - go-gitlab 24 | url: http://localhost:8080/api/v4/personal_access_tokens/self 25 | method: GET 26 | response: 27 | proto: HTTP/1.1 28 | proto_major: 1 29 | proto_minor: 1 30 | transfer_encoding: [] 31 | trailer: {} 32 | content_length: 234 33 | uncompressed: false 34 | body: '{"id":3,"name":"Root token","revoked":false,"created_at":"2025-04-04T18:35:29.407Z","description":null,"scopes":["admin_mode","api","sudo"],"user_id":3,"last_used_at":"2025-04-04T19:59:18.239Z","active":true,"expires_at":"2026-04-03"}' 35 | headers: 36 | Cache-Control: 37 | - max-age=0, private, must-revalidate 38 | Connection: 39 | - keep-alive 40 | Content-Length: 41 | - "234" 42 | Content-Type: 43 | - application/json 44 | Date: 45 | - Fri, 04 Apr 2025 19:59:27 GMT 46 | Etag: 47 | - W/"dc59f4cab933045578c753eb12954522" 48 | Referrer-Policy: 49 | - strict-origin-when-cross-origin 50 | Server: 51 | - nginx 52 | Strict-Transport-Security: 53 | - max-age=63072000 54 | Vary: 55 | - Origin 56 | X-Content-Type-Options: 57 | - nosniff 58 | X-Frame-Options: 59 | - SAMEORIGIN 60 | X-Gitlab-Meta: 61 | - '{"correlation_id":"01JR1662H3CW0XEX8T2YQNQ726","version":"1"}' 62 | X-Request-Id: 63 | - 01JR1662H3CW0XEX8T2YQNQ726 64 | X-Runtime: 65 | - "0.012017" 66 | status: 200 OK 67 | code: 200 68 | duration: 14.10425ms 69 | - id: 1 70 | request: 71 | proto: HTTP/1.1 72 | proto_major: 1 73 | proto_minor: 1 74 | content_length: 0 75 | transfer_encoding: [] 76 | trailer: {} 77 | host: localhost:8080 78 | remote_addr: "" 79 | request_uri: "" 80 | body: "" 81 | form: {} 82 | headers: 83 | Accept: 84 | - application/json 85 | Private-Token: 86 | - REPLACED-TOKEN 87 | User-Agent: 88 | - go-gitlab 89 | url: http://localhost:8080/api/v4/metadata 90 | method: GET 91 | response: 92 | proto: HTTP/1.1 93 | proto_major: 1 94 | proto_minor: 1 95 | transfer_encoding: [] 96 | trailer: {} 97 | content_length: 236 98 | uncompressed: false 99 | body: '{"version":"17.10.3","revision":"22d4014a923","kas":{"enabled":true,"externalUrl":"ws://dce56ec495e2/-/kubernetes-agent/","externalK8sProxyUrl":"http://dce56ec495e2/-/kubernetes-agent/k8s-proxy/","version":"17.10.3"},"enterprise":false}' 100 | headers: 101 | Cache-Control: 102 | - max-age=0, private, must-revalidate 103 | Connection: 104 | - keep-alive 105 | Content-Length: 106 | - "236" 107 | Content-Type: 108 | - application/json 109 | Date: 110 | - Fri, 04 Apr 2025 19:59:27 GMT 111 | Etag: 112 | - W/"c7db13e569c99229215906145ea2e48a" 113 | Referrer-Policy: 114 | - strict-origin-when-cross-origin 115 | Server: 116 | - nginx 117 | Strict-Transport-Security: 118 | - max-age=63072000 119 | Vary: 120 | - Origin 121 | X-Content-Type-Options: 122 | - nosniff 123 | X-Frame-Options: 124 | - SAMEORIGIN 125 | X-Gitlab-Meta: 126 | - '{"correlation_id":"01JR1662J1GS5QCE861DB29BY9","version":"1"}' 127 | X-Request-Id: 128 | - 01JR1662J1GS5QCE861DB29BY9 129 | X-Runtime: 130 | - "0.015116" 131 | status: 200 OK 132 | code: 200 133 | duration: 17.549166ms 134 | -------------------------------------------------------------------------------- /testdata/unit/TestPathRoles_Group_token_scopes_invalid_scopes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 0 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: localhost:8080 13 | remote_addr: "" 14 | request_uri: "" 15 | body: "" 16 | form: {} 17 | headers: 18 | Accept: 19 | - application/json 20 | Private-Token: 21 | - REPLACED-TOKEN 22 | User-Agent: 23 | - go-gitlab 24 | url: http://localhost:8080/api/v4/personal_access_tokens/self 25 | method: GET 26 | response: 27 | proto: HTTP/1.1 28 | proto_major: 1 29 | proto_minor: 1 30 | transfer_encoding: [] 31 | trailer: {} 32 | content_length: 234 33 | uncompressed: false 34 | body: '{"id":3,"name":"Root token","revoked":false,"created_at":"2025-04-04T18:35:29.407Z","description":null,"scopes":["admin_mode","api","sudo"],"user_id":3,"last_used_at":"2025-04-04T19:59:18.239Z","active":true,"expires_at":"2026-04-03"}' 35 | headers: 36 | Cache-Control: 37 | - max-age=0, private, must-revalidate 38 | Connection: 39 | - keep-alive 40 | Content-Length: 41 | - "234" 42 | Content-Type: 43 | - application/json 44 | Date: 45 | - Fri, 04 Apr 2025 19:59:26 GMT 46 | Etag: 47 | - W/"dc59f4cab933045578c753eb12954522" 48 | Referrer-Policy: 49 | - strict-origin-when-cross-origin 50 | Server: 51 | - nginx 52 | Strict-Transport-Security: 53 | - max-age=63072000 54 | Vary: 55 | - Origin 56 | X-Content-Type-Options: 57 | - nosniff 58 | X-Frame-Options: 59 | - SAMEORIGIN 60 | X-Gitlab-Meta: 61 | - '{"correlation_id":"01JR166250J2E3VR2RED16PRRG","version":"1"}' 62 | X-Request-Id: 63 | - 01JR166250J2E3VR2RED16PRRG 64 | X-Runtime: 65 | - "0.012862" 66 | status: 200 OK 67 | code: 200 68 | duration: 15.351792ms 69 | - id: 1 70 | request: 71 | proto: HTTP/1.1 72 | proto_major: 1 73 | proto_minor: 1 74 | content_length: 0 75 | transfer_encoding: [] 76 | trailer: {} 77 | host: localhost:8080 78 | remote_addr: "" 79 | request_uri: "" 80 | body: "" 81 | form: {} 82 | headers: 83 | Accept: 84 | - application/json 85 | Private-Token: 86 | - REPLACED-TOKEN 87 | User-Agent: 88 | - go-gitlab 89 | url: http://localhost:8080/api/v4/metadata 90 | method: GET 91 | response: 92 | proto: HTTP/1.1 93 | proto_major: 1 94 | proto_minor: 1 95 | transfer_encoding: [] 96 | trailer: {} 97 | content_length: 236 98 | uncompressed: false 99 | body: '{"version":"17.10.3","revision":"22d4014a923","kas":{"enabled":true,"externalUrl":"ws://dce56ec495e2/-/kubernetes-agent/","externalK8sProxyUrl":"http://dce56ec495e2/-/kubernetes-agent/k8s-proxy/","version":"17.10.3"},"enterprise":false}' 100 | headers: 101 | Cache-Control: 102 | - max-age=0, private, must-revalidate 103 | Connection: 104 | - keep-alive 105 | Content-Length: 106 | - "236" 107 | Content-Type: 108 | - application/json 109 | Date: 110 | - Fri, 04 Apr 2025 19:59:26 GMT 111 | Etag: 112 | - W/"c7db13e569c99229215906145ea2e48a" 113 | Referrer-Policy: 114 | - strict-origin-when-cross-origin 115 | Server: 116 | - nginx 117 | Strict-Transport-Security: 118 | - max-age=63072000 119 | Vary: 120 | - Origin 121 | X-Content-Type-Options: 122 | - nosniff 123 | X-Frame-Options: 124 | - SAMEORIGIN 125 | X-Gitlab-Meta: 126 | - '{"correlation_id":"01JR166261WHPE1B1JCXWSA4WZ","version":"1"}' 127 | X-Request-Id: 128 | - 01JR166261WHPE1B1JCXWSA4WZ 129 | X-Runtime: 130 | - "0.016525" 131 | status: 200 OK 132 | code: 200 133 | duration: 19.018875ms 134 | -------------------------------------------------------------------------------- /testdata/unit/TestPathRoles_Group_token_scopes_valid_scopes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 0 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: localhost:8080 13 | remote_addr: "" 14 | request_uri: "" 15 | body: "" 16 | form: {} 17 | headers: 18 | Accept: 19 | - application/json 20 | Private-Token: 21 | - REPLACED-TOKEN 22 | User-Agent: 23 | - go-gitlab 24 | url: http://localhost:8080/api/v4/personal_access_tokens/self 25 | method: GET 26 | response: 27 | proto: HTTP/1.1 28 | proto_major: 1 29 | proto_minor: 1 30 | transfer_encoding: [] 31 | trailer: {} 32 | content_length: 234 33 | uncompressed: false 34 | body: '{"id":3,"name":"Root token","revoked":false,"created_at":"2025-04-04T18:35:29.407Z","description":null,"scopes":["admin_mode","api","sudo"],"user_id":3,"last_used_at":"2025-04-04T19:59:18.239Z","active":true,"expires_at":"2026-04-03"}' 35 | headers: 36 | Cache-Control: 37 | - max-age=0, private, must-revalidate 38 | Connection: 39 | - keep-alive 40 | Content-Length: 41 | - "234" 42 | Content-Type: 43 | - application/json 44 | Date: 45 | - Fri, 04 Apr 2025 19:59:26 GMT 46 | Etag: 47 | - W/"dc59f4cab933045578c753eb12954522" 48 | Referrer-Policy: 49 | - strict-origin-when-cross-origin 50 | Server: 51 | - nginx 52 | Strict-Transport-Security: 53 | - max-age=63072000 54 | Vary: 55 | - Origin 56 | X-Content-Type-Options: 57 | - nosniff 58 | X-Frame-Options: 59 | - SAMEORIGIN 60 | X-Gitlab-Meta: 61 | - '{"correlation_id":"01JR16622M8H5KY7XKK0KH7E16","version":"1"}' 62 | X-Request-Id: 63 | - 01JR16622M8H5KY7XKK0KH7E16 64 | X-Runtime: 65 | - "0.012713" 66 | status: 200 OK 67 | code: 200 68 | duration: 15.103709ms 69 | - id: 1 70 | request: 71 | proto: HTTP/1.1 72 | proto_major: 1 73 | proto_minor: 1 74 | content_length: 0 75 | transfer_encoding: [] 76 | trailer: {} 77 | host: localhost:8080 78 | remote_addr: "" 79 | request_uri: "" 80 | body: "" 81 | form: {} 82 | headers: 83 | Accept: 84 | - application/json 85 | Private-Token: 86 | - REPLACED-TOKEN 87 | User-Agent: 88 | - go-gitlab 89 | url: http://localhost:8080/api/v4/metadata 90 | method: GET 91 | response: 92 | proto: HTTP/1.1 93 | proto_major: 1 94 | proto_minor: 1 95 | transfer_encoding: [] 96 | trailer: {} 97 | content_length: 236 98 | uncompressed: false 99 | body: '{"version":"17.10.3","revision":"22d4014a923","kas":{"enabled":true,"externalUrl":"ws://dce56ec495e2/-/kubernetes-agent/","externalK8sProxyUrl":"http://dce56ec495e2/-/kubernetes-agent/k8s-proxy/","version":"17.10.3"},"enterprise":false}' 100 | headers: 101 | Cache-Control: 102 | - max-age=0, private, must-revalidate 103 | Connection: 104 | - keep-alive 105 | Content-Length: 106 | - "236" 107 | Content-Type: 108 | - application/json 109 | Date: 110 | - Fri, 04 Apr 2025 19:59:26 GMT 111 | Etag: 112 | - W/"c7db13e569c99229215906145ea2e48a" 113 | Referrer-Policy: 114 | - strict-origin-when-cross-origin 115 | Server: 116 | - nginx 117 | Strict-Transport-Security: 118 | - max-age=63072000 119 | Vary: 120 | - Origin 121 | X-Content-Type-Options: 122 | - nosniff 123 | X-Frame-Options: 124 | - SAMEORIGIN 125 | X-Gitlab-Meta: 126 | - '{"correlation_id":"01JR16623MV1JQWGVFNDWXAMR6","version":"1"}' 127 | X-Request-Id: 128 | - 01JR16623MV1JQWGVFNDWXAMR6 129 | X-Runtime: 130 | - "0.015080" 131 | status: 200 OK 132 | code: 200 133 | duration: 17.317208ms 134 | -------------------------------------------------------------------------------- /testdata/unit/TestPathRoles_Personal_token_scopes_valid_scopes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 0 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: localhost:8080 13 | remote_addr: "" 14 | request_uri: "" 15 | body: "" 16 | form: {} 17 | headers: 18 | Accept: 19 | - application/json 20 | Private-Token: 21 | - REPLACED-TOKEN 22 | User-Agent: 23 | - go-gitlab 24 | url: http://localhost:8080/api/v4/personal_access_tokens/self 25 | method: GET 26 | response: 27 | proto: HTTP/1.1 28 | proto_major: 1 29 | proto_minor: 1 30 | transfer_encoding: [] 31 | trailer: {} 32 | content_length: 234 33 | uncompressed: false 34 | body: '{"id":3,"name":"Root token","revoked":false,"created_at":"2025-04-04T18:35:29.407Z","description":null,"scopes":["admin_mode","api","sudo"],"user_id":3,"last_used_at":"2025-04-04T19:59:18.239Z","active":true,"expires_at":"2026-04-03"}' 35 | headers: 36 | Cache-Control: 37 | - max-age=0, private, must-revalidate 38 | Connection: 39 | - keep-alive 40 | Content-Length: 41 | - "234" 42 | Content-Type: 43 | - application/json 44 | Date: 45 | - Fri, 04 Apr 2025 19:59:26 GMT 46 | Etag: 47 | - W/"dc59f4cab933045578c753eb12954522" 48 | Referrer-Policy: 49 | - strict-origin-when-cross-origin 50 | Server: 51 | - nginx 52 | Strict-Transport-Security: 53 | - max-age=63072000 54 | Vary: 55 | - Origin 56 | X-Content-Type-Options: 57 | - nosniff 58 | X-Frame-Options: 59 | - SAMEORIGIN 60 | X-Gitlab-Meta: 61 | - '{"correlation_id":"01JR1661XQEVS25QYPRQ5Y5WC8","version":"1"}' 62 | X-Request-Id: 63 | - 01JR1661XQEVS25QYPRQ5Y5WC8 64 | X-Runtime: 65 | - "0.014616" 66 | status: 200 OK 67 | code: 200 68 | duration: 17.60175ms 69 | - id: 1 70 | request: 71 | proto: HTTP/1.1 72 | proto_major: 1 73 | proto_minor: 1 74 | content_length: 0 75 | transfer_encoding: [] 76 | trailer: {} 77 | host: localhost:8080 78 | remote_addr: "" 79 | request_uri: "" 80 | body: "" 81 | form: {} 82 | headers: 83 | Accept: 84 | - application/json 85 | Private-Token: 86 | - REPLACED-TOKEN 87 | User-Agent: 88 | - go-gitlab 89 | url: http://localhost:8080/api/v4/metadata 90 | method: GET 91 | response: 92 | proto: HTTP/1.1 93 | proto_major: 1 94 | proto_minor: 1 95 | transfer_encoding: [] 96 | trailer: {} 97 | content_length: 236 98 | uncompressed: false 99 | body: '{"version":"17.10.3","revision":"22d4014a923","kas":{"enabled":true,"externalUrl":"ws://dce56ec495e2/-/kubernetes-agent/","externalK8sProxyUrl":"http://dce56ec495e2/-/kubernetes-agent/k8s-proxy/","version":"17.10.3"},"enterprise":false}' 100 | headers: 101 | Cache-Control: 102 | - max-age=0, private, must-revalidate 103 | Connection: 104 | - keep-alive 105 | Content-Length: 106 | - "236" 107 | Content-Type: 108 | - application/json 109 | Date: 110 | - Fri, 04 Apr 2025 19:59:26 GMT 111 | Etag: 112 | - W/"c7db13e569c99229215906145ea2e48a" 113 | Referrer-Policy: 114 | - strict-origin-when-cross-origin 115 | Server: 116 | - nginx 117 | Strict-Transport-Security: 118 | - max-age=63072000 119 | Vary: 120 | - Origin 121 | X-Content-Type-Options: 122 | - nosniff 123 | X-Frame-Options: 124 | - SAMEORIGIN 125 | X-Gitlab-Meta: 126 | - '{"correlation_id":"01JR1661YVEFJ75F7A4A8ASSDB","version":"1"}' 127 | X-Request-Id: 128 | - 01JR1661YVEFJ75F7A4A8ASSDB 129 | X-Runtime: 130 | - "0.017611" 131 | status: 200 OK 132 | code: 200 133 | duration: 20.412625ms 134 | -------------------------------------------------------------------------------- /testdata/unit/TestPathRoles_Project_token_scopes_invalid_scopes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 0 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: localhost:8080 13 | remote_addr: "" 14 | request_uri: "" 15 | body: "" 16 | form: {} 17 | headers: 18 | Accept: 19 | - application/json 20 | Private-Token: 21 | - REPLACED-TOKEN 22 | User-Agent: 23 | - go-gitlab 24 | url: http://localhost:8080/api/v4/personal_access_tokens/self 25 | method: GET 26 | response: 27 | proto: HTTP/1.1 28 | proto_major: 1 29 | proto_minor: 1 30 | transfer_encoding: [] 31 | trailer: {} 32 | content_length: 234 33 | uncompressed: false 34 | body: '{"id":3,"name":"Root token","revoked":false,"created_at":"2025-04-04T18:35:29.407Z","description":null,"scopes":["admin_mode","api","sudo"],"user_id":3,"last_used_at":"2025-04-04T19:59:18.239Z","active":true,"expires_at":"2026-04-03"}' 35 | headers: 36 | Cache-Control: 37 | - max-age=0, private, must-revalidate 38 | Connection: 39 | - keep-alive 40 | Content-Length: 41 | - "234" 42 | Content-Type: 43 | - application/json 44 | Date: 45 | - Fri, 04 Apr 2025 19:59:26 GMT 46 | Etag: 47 | - W/"dc59f4cab933045578c753eb12954522" 48 | Referrer-Policy: 49 | - strict-origin-when-cross-origin 50 | Server: 51 | - nginx 52 | Strict-Transport-Security: 53 | - max-age=63072000 54 | Vary: 55 | - Origin 56 | X-Content-Type-Options: 57 | - nosniff 58 | X-Frame-Options: 59 | - SAMEORIGIN 60 | X-Gitlab-Meta: 61 | - '{"correlation_id":"01JR1661V57JDPEMWKBRG8F72G","version":"1"}' 62 | X-Request-Id: 63 | - 01JR1661V57JDPEMWKBRG8F72G 64 | X-Runtime: 65 | - "0.012868" 66 | status: 200 OK 67 | code: 200 68 | duration: 15.187083ms 69 | - id: 1 70 | request: 71 | proto: HTTP/1.1 72 | proto_major: 1 73 | proto_minor: 1 74 | content_length: 0 75 | transfer_encoding: [] 76 | trailer: {} 77 | host: localhost:8080 78 | remote_addr: "" 79 | request_uri: "" 80 | body: "" 81 | form: {} 82 | headers: 83 | Accept: 84 | - application/json 85 | Private-Token: 86 | - REPLACED-TOKEN 87 | User-Agent: 88 | - go-gitlab 89 | url: http://localhost:8080/api/v4/metadata 90 | method: GET 91 | response: 92 | proto: HTTP/1.1 93 | proto_major: 1 94 | proto_minor: 1 95 | transfer_encoding: [] 96 | trailer: {} 97 | content_length: 236 98 | uncompressed: false 99 | body: '{"version":"17.10.3","revision":"22d4014a923","kas":{"enabled":true,"externalUrl":"ws://dce56ec495e2/-/kubernetes-agent/","externalK8sProxyUrl":"http://dce56ec495e2/-/kubernetes-agent/k8s-proxy/","version":"17.10.3"},"enterprise":false}' 100 | headers: 101 | Cache-Control: 102 | - max-age=0, private, must-revalidate 103 | Connection: 104 | - keep-alive 105 | Content-Length: 106 | - "236" 107 | Content-Type: 108 | - application/json 109 | Date: 110 | - Fri, 04 Apr 2025 19:59:26 GMT 111 | Etag: 112 | - W/"c7db13e569c99229215906145ea2e48a" 113 | Referrer-Policy: 114 | - strict-origin-when-cross-origin 115 | Server: 116 | - nginx 117 | Strict-Transport-Security: 118 | - max-age=63072000 119 | Vary: 120 | - Origin 121 | X-Content-Type-Options: 122 | - nosniff 123 | X-Frame-Options: 124 | - SAMEORIGIN 125 | X-Gitlab-Meta: 126 | - '{"correlation_id":"01JR1661W52PKN9W63J1HKMC6R","version":"1"}' 127 | X-Request-Id: 128 | - 01JR1661W52PKN9W63J1HKMC6R 129 | X-Runtime: 130 | - "0.017342" 131 | status: 200 OK 132 | code: 200 133 | duration: 19.9575ms 134 | -------------------------------------------------------------------------------- /testdata/unit/TestPathRoles_Project_token_scopes_valid_scopes.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 0 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: localhost:8080 13 | remote_addr: "" 14 | request_uri: "" 15 | body: "" 16 | form: {} 17 | headers: 18 | Accept: 19 | - application/json 20 | Private-Token: 21 | - REPLACED-TOKEN 22 | User-Agent: 23 | - go-gitlab 24 | url: http://localhost:8080/api/v4/personal_access_tokens/self 25 | method: GET 26 | response: 27 | proto: HTTP/1.1 28 | proto_major: 1 29 | proto_minor: 1 30 | transfer_encoding: [] 31 | trailer: {} 32 | content_length: 234 33 | uncompressed: false 34 | body: '{"id":3,"name":"Root token","revoked":false,"created_at":"2025-04-04T18:35:29.407Z","description":null,"scopes":["admin_mode","api","sudo"],"user_id":3,"last_used_at":"2025-04-04T19:59:18.239Z","active":true,"expires_at":"2026-04-03"}' 35 | headers: 36 | Cache-Control: 37 | - max-age=0, private, must-revalidate 38 | Connection: 39 | - keep-alive 40 | Content-Length: 41 | - "234" 42 | Content-Type: 43 | - application/json 44 | Date: 45 | - Fri, 04 Apr 2025 19:59:26 GMT 46 | Etag: 47 | - W/"dc59f4cab933045578c753eb12954522" 48 | Referrer-Policy: 49 | - strict-origin-when-cross-origin 50 | Server: 51 | - nginx 52 | Strict-Transport-Security: 53 | - max-age=63072000 54 | Vary: 55 | - Origin 56 | X-Content-Type-Options: 57 | - nosniff 58 | X-Frame-Options: 59 | - SAMEORIGIN 60 | X-Gitlab-Meta: 61 | - '{"correlation_id":"01JR1661RF641ZRVPYR3ANDWP2","version":"1"}' 62 | X-Request-Id: 63 | - 01JR1661RF641ZRVPYR3ANDWP2 64 | X-Runtime: 65 | - "0.015731" 66 | status: 200 OK 67 | code: 200 68 | duration: 18.573167ms 69 | - id: 1 70 | request: 71 | proto: HTTP/1.1 72 | proto_major: 1 73 | proto_minor: 1 74 | content_length: 0 75 | transfer_encoding: [] 76 | trailer: {} 77 | host: localhost:8080 78 | remote_addr: "" 79 | request_uri: "" 80 | body: "" 81 | form: {} 82 | headers: 83 | Accept: 84 | - application/json 85 | Private-Token: 86 | - REPLACED-TOKEN 87 | User-Agent: 88 | - go-gitlab 89 | url: http://localhost:8080/api/v4/metadata 90 | method: GET 91 | response: 92 | proto: HTTP/1.1 93 | proto_major: 1 94 | proto_minor: 1 95 | transfer_encoding: [] 96 | trailer: {} 97 | content_length: 236 98 | uncompressed: false 99 | body: '{"version":"17.10.3","revision":"22d4014a923","kas":{"enabled":true,"externalUrl":"ws://dce56ec495e2/-/kubernetes-agent/","externalK8sProxyUrl":"http://dce56ec495e2/-/kubernetes-agent/k8s-proxy/","version":"17.10.3"},"enterprise":false}' 100 | headers: 101 | Cache-Control: 102 | - max-age=0, private, must-revalidate 103 | Connection: 104 | - keep-alive 105 | Content-Length: 106 | - "236" 107 | Content-Type: 108 | - application/json 109 | Date: 110 | - Fri, 04 Apr 2025 19:59:26 GMT 111 | Etag: 112 | - W/"c7db13e569c99229215906145ea2e48a" 113 | Referrer-Policy: 114 | - strict-origin-when-cross-origin 115 | Server: 116 | - nginx 117 | Strict-Transport-Security: 118 | - max-age=63072000 119 | Vary: 120 | - Origin 121 | X-Content-Type-Options: 122 | - nosniff 123 | X-Frame-Options: 124 | - SAMEORIGIN 125 | X-Gitlab-Meta: 126 | - '{"correlation_id":"01JR1661SPFTD17AJ3KH8ZCEZF","version":"1"}' 127 | X-Request-Id: 128 | - 01JR1661SPFTD17AJ3KH8ZCEZF 129 | X-Runtime: 130 | - "0.016561" 131 | status: 200 OK 132 | code: 200 133 | duration: 18.930583ms 134 | -------------------------------------------------------------------------------- /testdata/unit/TestPathRoles_delete_non_existing_role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathRoles_full_flow_check_roles.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 0 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: localhost:8080 13 | remote_addr: "" 14 | request_uri: "" 15 | body: "" 16 | form: {} 17 | headers: 18 | Accept: 19 | - application/json 20 | Private-Token: 21 | - REPLACED-TOKEN 22 | User-Agent: 23 | - go-gitlab 24 | url: http://localhost:8080/api/v4/personal_access_tokens/self 25 | method: GET 26 | response: 27 | proto: HTTP/1.1 28 | proto_major: 1 29 | proto_minor: 1 30 | transfer_encoding: [] 31 | trailer: {} 32 | content_length: 234 33 | uncompressed: false 34 | body: '{"id":3,"name":"Root token","revoked":false,"created_at":"2025-04-04T18:35:29.407Z","description":null,"scopes":["admin_mode","api","sudo"],"user_id":3,"last_used_at":"2025-04-04T19:59:18.239Z","active":true,"expires_at":"2026-04-03"}' 35 | headers: 36 | Cache-Control: 37 | - max-age=0, private, must-revalidate 38 | Connection: 39 | - keep-alive 40 | Content-Length: 41 | - "234" 42 | Content-Type: 43 | - application/json 44 | Date: 45 | - Fri, 04 Apr 2025 19:59:26 GMT 46 | Etag: 47 | - W/"dc59f4cab933045578c753eb12954522" 48 | Referrer-Policy: 49 | - strict-origin-when-cross-origin 50 | Server: 51 | - nginx 52 | Strict-Transport-Security: 53 | - max-age=63072000 54 | Vary: 55 | - Origin 56 | X-Content-Type-Options: 57 | - nosniff 58 | X-Frame-Options: 59 | - SAMEORIGIN 60 | X-Gitlab-Meta: 61 | - '{"correlation_id":"01JR16627H6CYPBKSEJG7F2846","version":"1"}' 62 | X-Request-Id: 63 | - 01JR16627H6CYPBKSEJG7F2846 64 | X-Runtime: 65 | - "0.013886" 66 | status: 200 OK 67 | code: 200 68 | duration: 16.259916ms 69 | - id: 1 70 | request: 71 | proto: HTTP/1.1 72 | proto_major: 1 73 | proto_minor: 1 74 | content_length: 0 75 | transfer_encoding: [] 76 | trailer: {} 77 | host: localhost:8080 78 | remote_addr: "" 79 | request_uri: "" 80 | body: "" 81 | form: {} 82 | headers: 83 | Accept: 84 | - application/json 85 | Private-Token: 86 | - REPLACED-TOKEN 87 | User-Agent: 88 | - go-gitlab 89 | url: http://localhost:8080/api/v4/metadata 90 | method: GET 91 | response: 92 | proto: HTTP/1.1 93 | proto_major: 1 94 | proto_minor: 1 95 | transfer_encoding: [] 96 | trailer: {} 97 | content_length: 236 98 | uncompressed: false 99 | body: '{"version":"17.10.3","revision":"22d4014a923","kas":{"enabled":true,"externalUrl":"ws://dce56ec495e2/-/kubernetes-agent/","externalK8sProxyUrl":"http://dce56ec495e2/-/kubernetes-agent/k8s-proxy/","version":"17.10.3"},"enterprise":false}' 100 | headers: 101 | Cache-Control: 102 | - max-age=0, private, must-revalidate 103 | Connection: 104 | - keep-alive 105 | Content-Length: 106 | - "236" 107 | Content-Type: 108 | - application/json 109 | Date: 110 | - Fri, 04 Apr 2025 19:59:27 GMT 111 | Etag: 112 | - W/"c7db13e569c99229215906145ea2e48a" 113 | Referrer-Policy: 114 | - strict-origin-when-cross-origin 115 | Server: 116 | - nginx 117 | Strict-Transport-Security: 118 | - max-age=63072000 119 | Vary: 120 | - Origin 121 | X-Content-Type-Options: 122 | - nosniff 123 | X-Frame-Options: 124 | - SAMEORIGIN 125 | X-Gitlab-Meta: 126 | - '{"correlation_id":"01JR16628MPFYCNVJTB3QADXAV","version":"1"}' 127 | X-Request-Id: 128 | - 01JR16628MPFYCNVJTB3QADXAV 129 | X-Runtime: 130 | - "0.016579" 131 | status: 200 OK 132 | code: 200 133 | duration: 19.004875ms 134 | -------------------------------------------------------------------------------- /testdata/unit/TestPathRoles_invalid_name_template.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: 4 | - id: 0 5 | request: 6 | proto: HTTP/1.1 7 | proto_major: 1 8 | proto_minor: 1 9 | content_length: 0 10 | transfer_encoding: [] 11 | trailer: {} 12 | host: localhost:8080 13 | remote_addr: "" 14 | request_uri: "" 15 | body: "" 16 | form: {} 17 | headers: 18 | Accept: 19 | - application/json 20 | Private-Token: 21 | - REPLACED-TOKEN 22 | User-Agent: 23 | - go-gitlab 24 | url: http://localhost:8080/api/v4/personal_access_tokens/self 25 | method: GET 26 | response: 27 | proto: HTTP/1.1 28 | proto_major: 1 29 | proto_minor: 1 30 | transfer_encoding: [] 31 | trailer: {} 32 | content_length: 234 33 | uncompressed: false 34 | body: '{"id":3,"name":"Root token","revoked":false,"created_at":"2025-04-04T18:35:29.407Z","description":null,"scopes":["admin_mode","api","sudo"],"user_id":3,"last_used_at":"2025-04-04T19:59:18.239Z","active":true,"expires_at":"2026-04-03"}' 35 | headers: 36 | Cache-Control: 37 | - max-age=0, private, must-revalidate 38 | Connection: 39 | - keep-alive 40 | Content-Length: 41 | - "234" 42 | Content-Type: 43 | - application/json 44 | Date: 45 | - Fri, 04 Apr 2025 19:59:26 GMT 46 | Etag: 47 | - W/"dc59f4cab933045578c753eb12954522" 48 | Referrer-Policy: 49 | - strict-origin-when-cross-origin 50 | Server: 51 | - nginx 52 | Strict-Transport-Security: 53 | - max-age=63072000 54 | Vary: 55 | - Origin 56 | X-Content-Type-Options: 57 | - nosniff 58 | X-Frame-Options: 59 | - SAMEORIGIN 60 | X-Gitlab-Meta: 61 | - '{"correlation_id":"01JR1661G80H7YS4NNZRJ5FD3Z","version":"1"}' 62 | X-Request-Id: 63 | - 01JR1661G80H7YS4NNZRJ5FD3Z 64 | X-Runtime: 65 | - "0.013950" 66 | status: 200 OK 67 | code: 200 68 | duration: 16.070666ms 69 | - id: 1 70 | request: 71 | proto: HTTP/1.1 72 | proto_major: 1 73 | proto_minor: 1 74 | content_length: 0 75 | transfer_encoding: [] 76 | trailer: {} 77 | host: localhost:8080 78 | remote_addr: "" 79 | request_uri: "" 80 | body: "" 81 | form: {} 82 | headers: 83 | Accept: 84 | - application/json 85 | Private-Token: 86 | - REPLACED-TOKEN 87 | User-Agent: 88 | - go-gitlab 89 | url: http://localhost:8080/api/v4/metadata 90 | method: GET 91 | response: 92 | proto: HTTP/1.1 93 | proto_major: 1 94 | proto_minor: 1 95 | transfer_encoding: [] 96 | trailer: {} 97 | content_length: 236 98 | uncompressed: false 99 | body: '{"version":"17.10.3","revision":"22d4014a923","kas":{"enabled":true,"externalUrl":"ws://dce56ec495e2/-/kubernetes-agent/","externalK8sProxyUrl":"http://dce56ec495e2/-/kubernetes-agent/k8s-proxy/","version":"17.10.3"},"enterprise":false}' 100 | headers: 101 | Cache-Control: 102 | - max-age=0, private, must-revalidate 103 | Connection: 104 | - keep-alive 105 | Content-Length: 106 | - "236" 107 | Content-Type: 108 | - application/json 109 | Date: 110 | - Fri, 04 Apr 2025 19:59:26 GMT 111 | Etag: 112 | - W/"c7db13e569c99229215906145ea2e48a" 113 | Referrer-Policy: 114 | - strict-origin-when-cross-origin 115 | Server: 116 | - nginx 117 | Strict-Transport-Security: 118 | - max-age=63072000 119 | Vary: 120 | - Origin 121 | X-Content-Type-Options: 122 | - nosniff 123 | X-Frame-Options: 124 | - SAMEORIGIN 125 | X-Gitlab-Meta: 126 | - '{"correlation_id":"01JR1661HBGA3XGD3MZ6DBDN6Z","version":"1"}' 127 | X-Request-Id: 128 | - 01JR1661HBGA3XGD3MZ6DBDN6Z 129 | X-Runtime: 130 | - "0.106788" 131 | status: 200 OK 132 | code: 200 133 | duration: 109.642292ms 134 | -------------------------------------------------------------------------------- /testdata/unit/TestPathRoles_update_handler_existence_check.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathRoles_we_get_error_if_backend_is_not_set_up_during_role_write.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathTokenRoles_group_access_token.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathTokenRoles_personal_access_token.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathTokenRoles_project_access_token.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /testdata/unit/TestPathTokenRoles_role_not_found.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | interactions: [] 4 | -------------------------------------------------------------------------------- /token.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | "maps" 7 | "strconv" 8 | "time" 9 | ) 10 | 11 | type IToken interface { 12 | Internal() map[string]any 13 | Data() map[string]any 14 | Event(map[string]string) map[string]string 15 | Type() TokenType 16 | SetConfigName(string) 17 | SetRoleName(string) 18 | SetGitlabRevokesToken(bool) 19 | SetExpiresAt(*time.Time) 20 | GetExpiresAt() time.Time 21 | GetCreatedAt() time.Time 22 | TTL() time.Duration 23 | } 24 | 25 | type Token struct { 26 | RoleName string `json:"role_name"` 27 | ConfigName string `json:"config_name"` 28 | GitlabRevokesToken bool `json:"gitlab_revokes_token"` 29 | CreatedAt *time.Time `json:"created_at"` 30 | ExpiresAt *time.Time `json:"expires_at"` 31 | TokenType TokenType `json:"type"` 32 | Token string `json:"token"` 33 | TokenID int `json:"token_id"` 34 | ParentID string `json:"parent_id"` 35 | Name string `json:"name"` 36 | Path string `json:"path"` 37 | } 38 | 39 | func (t *Token) TTL() time.Duration { 40 | return t.GetExpiresAt().Sub(t.GetCreatedAt()) 41 | } 42 | 43 | func (t *Token) GetExpiresAt() (tm time.Time) { 44 | if t.ExpiresAt != nil { 45 | tm = *t.ExpiresAt 46 | } 47 | return tm 48 | } 49 | func (t *Token) GetCreatedAt() (tm time.Time) { 50 | if t.CreatedAt != nil { 51 | tm = *t.CreatedAt 52 | } 53 | return tm 54 | } 55 | func (t *Token) SetExpiresAt(expiresAt *time.Time) { t.ExpiresAt = expiresAt } 56 | func (t *Token) SetConfigName(name string) { t.ConfigName = name } 57 | func (t *Token) SetRoleName(name string) { t.RoleName = name } 58 | func (t *Token) SetGitlabRevokesToken(b bool) { t.GitlabRevokesToken = b } 59 | func (t *Token) Type() TokenType { return t.TokenType } 60 | 61 | func (t *Token) Internal() map[string]any { 62 | return map[string]any{ 63 | "name": t.Name, 64 | "path": t.Path, 65 | "token": t.Token, 66 | "token_id": t.TokenID, 67 | "parent_id": t.ParentID, 68 | "role_name": t.RoleName, 69 | "config_name": t.ConfigName, 70 | "gitlab_revokes_token": t.GitlabRevokesToken, 71 | "created_at": t.CreatedAt, 72 | "expires_at": t.ExpiresAt, 73 | "token_type": t.Type().String(), 74 | } 75 | } 76 | 77 | func (t *Token) Data() map[string]any { 78 | return map[string]any{ 79 | "path": t.Path, 80 | "name": t.Name, 81 | "token": t.Token, 82 | "token_sha1_hash": fmt.Sprintf("%x", sha1.Sum([]byte(t.Token))), 83 | "token_id": t.TokenID, 84 | "token_type": t.Type().String(), 85 | "parent_id": t.ParentID, 86 | "role_name": t.RoleName, 87 | "config_name": t.ConfigName, 88 | "created_at": t.CreatedAt, 89 | "expires_at": t.ExpiresAt, 90 | } 91 | } 92 | 93 | func (t *Token) Event(m map[string]string) (d map[string]string) { 94 | d = map[string]string{ 95 | "config_name": t.ConfigName, 96 | "role_name": t.RoleName, 97 | "token_id": strconv.Itoa(t.TokenID), 98 | "parent_id": t.ParentID, 99 | "token_type": t.Type().String(), 100 | "ttl": t.TTL().String(), 101 | "name": t.Name, 102 | } 103 | maps.Copy(d, m) 104 | return d 105 | } 106 | 107 | var _ IToken = (*Token)(nil) 108 | -------------------------------------------------------------------------------- /token_config.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | type TokenConfig struct { 4 | TokenWithScopes `json:",inline"` 5 | 6 | UserID int `json:"user_id"` 7 | } 8 | -------------------------------------------------------------------------------- /token_group.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | type TokenGroup struct { 4 | TokenWithScopesAndAccessLevel `json:",inline"` 5 | } 6 | -------------------------------------------------------------------------------- /token_group_deploy.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "maps" 5 | ) 6 | 7 | type TokenGroupDeploy struct { 8 | TokenWithScopes `json:",inline"` 9 | 10 | Username string `json:"username"` 11 | } 12 | 13 | func (t *TokenGroupDeploy) Internal() (d map[string]any) { 14 | d = map[string]any{"username": t.Username} 15 | maps.Copy(d, t.TokenWithScopes.Internal()) 16 | return d 17 | } 18 | 19 | func (t *TokenGroupDeploy) Data() (d map[string]any) { 20 | d = map[string]any{"username": t.Username} 21 | maps.Copy(d, t.TokenWithScopes.Data()) 22 | return d 23 | } 24 | 25 | func (t *TokenGroupDeploy) Event(m map[string]string) (d map[string]string) { 26 | d = map[string]string{"username": t.Username} 27 | maps.Copy(d, t.Token.Event(m)) 28 | return d 29 | } 30 | -------------------------------------------------------------------------------- /token_group_service_account.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "maps" 5 | "strconv" 6 | ) 7 | 8 | type TokenGroupServiceAccount struct { 9 | TokenWithScopes `json:",inline"` 10 | 11 | UserID int `json:"user_id"` 12 | } 13 | 14 | func (t *TokenGroupServiceAccount) Internal() (d map[string]any) { 15 | d = map[string]any{"user_id": t.UserID} 16 | maps.Copy(d, t.TokenWithScopes.Internal()) 17 | return d 18 | } 19 | 20 | func (t *TokenGroupServiceAccount) Data() (d map[string]any) { 21 | d = map[string]any{"user_id": t.UserID} 22 | maps.Copy(d, t.TokenWithScopes.Data()) 23 | return d 24 | } 25 | 26 | func (t *TokenGroupServiceAccount) Event(m map[string]string) (d map[string]string) { 27 | d = map[string]string{"user_id": strconv.Itoa(t.UserID)} 28 | maps.Copy(d, t.Token.Event(m)) 29 | return d 30 | } 31 | -------------------------------------------------------------------------------- /token_personal.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "maps" 5 | "strconv" 6 | ) 7 | 8 | type TokenPersonal struct { 9 | TokenWithScopes `json:",inline"` 10 | 11 | UserID int `json:"user_id"` 12 | } 13 | 14 | func (t *TokenPersonal) Internal() (d map[string]any) { 15 | d = map[string]any{"user_id": t.UserID} 16 | maps.Copy(d, t.TokenWithScopes.Internal()) 17 | return d 18 | } 19 | 20 | func (t *TokenPersonal) Data() (d map[string]any) { 21 | d = map[string]any{"user_id": t.UserID} 22 | maps.Copy(d, t.TokenWithScopes.Data()) 23 | return d 24 | } 25 | 26 | func (t *TokenPersonal) Event(m map[string]string) (d map[string]string) { 27 | d = map[string]string{"user_id": strconv.Itoa(t.UserID)} 28 | maps.Copy(d, t.Token.Event(m)) 29 | return d 30 | } 31 | -------------------------------------------------------------------------------- /token_pipeline_project_trigger.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | type TokenPipelineProjectTrigger struct { 4 | Token `json:",inline"` 5 | } 6 | -------------------------------------------------------------------------------- /token_project.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | type TokenProject struct { 4 | TokenWithScopesAndAccessLevel `json:",inline"` 5 | } 6 | -------------------------------------------------------------------------------- /token_project_deploy.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import "maps" 4 | 5 | type TokenProjectDeploy struct { 6 | TokenWithScopes `json:",inline"` 7 | 8 | Username string `json:"username"` 9 | } 10 | 11 | func (t *TokenProjectDeploy) Internal() (d map[string]any) { 12 | d = map[string]any{"username": t.Username} 13 | maps.Copy(d, t.TokenWithScopes.Internal()) 14 | return d 15 | } 16 | 17 | func (t *TokenProjectDeploy) Data() (d map[string]any) { 18 | d = map[string]any{"username": t.Username} 19 | maps.Copy(d, t.TokenWithScopes.Data()) 20 | return d 21 | } 22 | 23 | func (t *TokenProjectDeploy) Event(m map[string]string) (d map[string]string) { 24 | d = map[string]string{"username": t.Username} 25 | maps.Copy(d, t.Token.Event(m)) 26 | return d 27 | } 28 | -------------------------------------------------------------------------------- /token_user_service_account.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | type TokenUserServiceAccount struct { 4 | TokenWithScopes `json:",inline"` 5 | } 6 | -------------------------------------------------------------------------------- /token_with_scopes.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "maps" 5 | "strings" 6 | ) 7 | 8 | type TokenWithScopes struct { 9 | Token `json:",inline"` 10 | 11 | Scopes []string `json:"scopes"` 12 | } 13 | 14 | func (t *TokenWithScopes) Internal() (d map[string]any) { 15 | d = map[string]any{"scopes": t.Scopes} 16 | maps.Copy(d, t.Token.Internal()) 17 | return d 18 | } 19 | 20 | func (t *TokenWithScopes) Data() (d map[string]any) { 21 | d = map[string]any{"scopes": t.Scopes} 22 | maps.Copy(d, t.Token.Data()) 23 | return d 24 | } 25 | 26 | func (t *TokenWithScopes) Event(m map[string]string) (d map[string]string) { 27 | d = map[string]string{"scopes": strings.Join(t.Scopes, ",")} 28 | maps.Copy(d, t.Token.Event(m)) 29 | return d 30 | } 31 | 32 | var _ IToken = (*TokenWithScopes)(nil) 33 | -------------------------------------------------------------------------------- /token_with_scopes_and_access_level.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "maps" 5 | "strings" 6 | ) 7 | 8 | type TokenWithScopesAndAccessLevel struct { 9 | Token `json:",inline"` 10 | 11 | Scopes []string `json:"scopes"` 12 | AccessLevel AccessLevel `json:"access_level"` 13 | } 14 | 15 | func (t *TokenWithScopesAndAccessLevel) Internal() (d map[string]any) { 16 | d = map[string]any{ 17 | "scopes": t.Scopes, 18 | "access_level": t.AccessLevel.String(), 19 | } 20 | maps.Copy(d, t.Token.Internal()) 21 | return d 22 | } 23 | 24 | func (t *TokenWithScopesAndAccessLevel) Data() (d map[string]any) { 25 | d = map[string]any{ 26 | "scopes": t.Scopes, 27 | "access_level": t.AccessLevel.String(), 28 | } 29 | maps.Copy(d, t.Token.Data()) 30 | return d 31 | } 32 | 33 | func (t *TokenWithScopesAndAccessLevel) Event(m map[string]string) (d map[string]string) { 34 | d = map[string]string{ 35 | "scopes": strings.Join(t.Scopes, ","), 36 | "access_level": t.AccessLevel.String(), 37 | } 38 | maps.Copy(d, t.Token.Event(m)) 39 | return d 40 | } 41 | 42 | var _ IToken = (*TokenWithScopesAndAccessLevel)(nil) 43 | -------------------------------------------------------------------------------- /type_access_level.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "slices" 7 | 8 | g "gitlab.com/gitlab-org/api/client-go" 9 | ) 10 | 11 | type AccessLevel string 12 | 13 | const ( 14 | AccessLevelNoPermissions = AccessLevel("no_permissions") 15 | AccessLevelMinimalAccessPermissions = AccessLevel("minimal_access") 16 | AccessLevelGuestPermissions = AccessLevel("guest") 17 | AccessLevelReporterPermissions = AccessLevel("reporter") 18 | AccessLevelDeveloperPermissions = AccessLevel("developer") 19 | AccessLevelMaintainerPermissions = AccessLevel("maintainer") 20 | AccessLevelOwnerPermissions = AccessLevel("owner") 21 | 22 | AccessLevelUnknown = AccessLevel("") 23 | ) 24 | 25 | var ( 26 | ErrUnknownAccessLevel = errors.New("unknown access level") 27 | 28 | ValidAccessLevels = []string{ 29 | AccessLevelNoPermissions.String(), 30 | AccessLevelMinimalAccessPermissions.String(), 31 | AccessLevelGuestPermissions.String(), 32 | AccessLevelReporterPermissions.String(), 33 | AccessLevelDeveloperPermissions.String(), 34 | AccessLevelMaintainerPermissions.String(), 35 | AccessLevelOwnerPermissions.String(), 36 | } 37 | ValidPersonalAccessLevels = []string{ 38 | AccessLevelUnknown.String(), 39 | } 40 | ValidUserServiceAccountAccessLevels = []string{ 41 | AccessLevelUnknown.String(), 42 | } 43 | ValidGroupServiceAccountAccessLevels = []string{ 44 | AccessLevelUnknown.String(), 45 | } 46 | ValidProjectAccessLevels = []string{ 47 | AccessLevelGuestPermissions.String(), 48 | AccessLevelReporterPermissions.String(), 49 | AccessLevelDeveloperPermissions.String(), 50 | AccessLevelMaintainerPermissions.String(), 51 | AccessLevelOwnerPermissions.String(), 52 | } 53 | ValidGroupAccessLevels = []string{ 54 | AccessLevelGuestPermissions.String(), 55 | AccessLevelReporterPermissions.String(), 56 | AccessLevelDeveloperPermissions.String(), 57 | AccessLevelMaintainerPermissions.String(), 58 | AccessLevelOwnerPermissions.String(), 59 | } 60 | 61 | ValidPipelineProjectTriggerAccessLevels = []string{AccessLevelUnknown.String()} 62 | ValidProjectDeployAccessLevels = []string{AccessLevelUnknown.String()} 63 | ValidGroupDeployAccessLevels = []string{AccessLevelUnknown.String()} 64 | ) 65 | 66 | func (i AccessLevel) String() string { 67 | return string(i) 68 | } 69 | 70 | func (i AccessLevel) Value() int { 71 | switch i { 72 | case AccessLevelNoPermissions: 73 | return int(g.NoPermissions) 74 | case AccessLevelMinimalAccessPermissions: 75 | return int(g.MinimalAccessPermissions) 76 | case AccessLevelGuestPermissions: 77 | return int(g.GuestPermissions) 78 | case AccessLevelReporterPermissions: 79 | return int(g.ReporterPermissions) 80 | case AccessLevelDeveloperPermissions: 81 | return int(g.DeveloperPermissions) 82 | case AccessLevelMaintainerPermissions: 83 | return int(g.MaintainerPermissions) 84 | case AccessLevelOwnerPermissions: 85 | return int(g.OwnerPermissions) 86 | } 87 | 88 | return -1 89 | } 90 | 91 | func AccessLevelParse(value string) (AccessLevel, error) { 92 | if slices.Contains(ValidAccessLevels, value) { 93 | return AccessLevel(value), nil 94 | } 95 | return AccessLevelUnknown, fmt.Errorf("failed to parse '%s': %w", value, ErrUnknownAccessLevel) 96 | } 97 | -------------------------------------------------------------------------------- /type_access_level_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 11 | ) 12 | 13 | func TestAccessLevel(t *testing.T) { 14 | var tests = []struct { 15 | expected gitlab.AccessLevel 16 | input string 17 | err bool 18 | }{ 19 | { 20 | expected: gitlab.AccessLevelOwnerPermissions, 21 | input: gitlab.AccessLevelOwnerPermissions.String(), 22 | }, 23 | { 24 | expected: gitlab.AccessLevelReporterPermissions, 25 | input: gitlab.AccessLevelReporterPermissions.String(), 26 | }, 27 | { 28 | expected: gitlab.AccessLevelMaintainerPermissions, 29 | input: gitlab.AccessLevelMaintainerPermissions.String(), 30 | }, 31 | { 32 | expected: gitlab.AccessLevelDeveloperPermissions, 33 | input: gitlab.AccessLevelDeveloperPermissions.String(), 34 | }, 35 | { 36 | expected: gitlab.AccessLevelGuestPermissions, 37 | input: gitlab.AccessLevelGuestPermissions.String(), 38 | }, 39 | { 40 | expected: gitlab.AccessLevelNoPermissions, 41 | input: gitlab.AccessLevelNoPermissions.String(), 42 | }, 43 | { 44 | expected: gitlab.AccessLevelMinimalAccessPermissions, 45 | input: gitlab.AccessLevelMinimalAccessPermissions.String(), 46 | }, 47 | { 48 | expected: gitlab.AccessLevelUnknown, 49 | input: "unknown", 50 | err: true, 51 | }, 52 | } 53 | 54 | for _, test := range tests { 55 | t.Logf("assert parse(%s) = %s (err: %v)", test.input, test.expected, test.err) 56 | val, err := gitlab.AccessLevelParse(test.input) 57 | assert.EqualValues(t, test.expected, val) 58 | if test.err { 59 | assert.ErrorIs(t, err, gitlab.ErrUnknownAccessLevel) 60 | assert.Less(t, val.Value(), 0) 61 | } else { 62 | assert.NoError(t, err) 63 | assert.GreaterOrEqual(t, val.Value(), 0) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /type_token_scope_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 11 | ) 12 | 13 | func TestTokenScope(t *testing.T) { 14 | var tests = []struct { 15 | expected gitlab.TokenScope 16 | input string 17 | err bool 18 | }{ 19 | { 20 | expected: gitlab.TokenScopeApi, 21 | input: gitlab.TokenScopeApi.String(), 22 | }, 23 | { 24 | expected: gitlab.TokenScopeReadApi, 25 | input: gitlab.TokenScopeReadApi.String(), 26 | }, 27 | { 28 | expected: gitlab.TokenScopeReadRegistry, 29 | input: gitlab.TokenScopeReadRegistry.String(), 30 | }, 31 | { 32 | expected: gitlab.TokenScopeWriteRegistry, 33 | input: gitlab.TokenScopeWriteRegistry.String(), 34 | }, 35 | { 36 | expected: gitlab.TokenScopeReadRepository, 37 | input: gitlab.TokenScopeReadRepository.String(), 38 | }, 39 | { 40 | expected: gitlab.TokenScopeWriteRepository, 41 | input: gitlab.TokenScopeWriteRepository.String(), 42 | }, 43 | { 44 | expected: gitlab.TokenScopeCreateRunner, 45 | input: gitlab.TokenScopeCreateRunner.String(), 46 | }, 47 | { 48 | expected: gitlab.TokenScopeReadUser, 49 | input: gitlab.TokenScopeReadUser.String(), 50 | }, 51 | { 52 | expected: gitlab.TokenScopeSudo, 53 | input: gitlab.TokenScopeSudo.String(), 54 | }, 55 | { 56 | expected: gitlab.TokenScopeAdminMode, 57 | input: gitlab.TokenScopeAdminMode.String(), 58 | }, 59 | { 60 | expected: gitlab.TokenScopeReadPackageRegistry, 61 | input: gitlab.TokenScopeReadPackageRegistry.String(), 62 | }, 63 | { 64 | expected: gitlab.TokenScopeWritePackageRegistry, 65 | input: gitlab.TokenScopeWritePackageRegistry.String(), 66 | }, 67 | { 68 | expected: gitlab.TokenScopeUnknown, 69 | input: "what", 70 | err: true, 71 | }, 72 | { 73 | expected: gitlab.TokenScopeUnknown, 74 | input: "unknown", 75 | err: true, 76 | }, 77 | } 78 | 79 | for _, test := range tests { 80 | t.Logf("assert parse(%s) = %s (err: %v)", test.input, test.expected, test.err) 81 | val, err := gitlab.TokenScopeParse(test.input) 82 | assert.EqualValues(t, test.expected, val) 83 | assert.EqualValues(t, test.expected.Value(), test.expected.String()) 84 | if test.err { 85 | assert.ErrorIs(t, err, gitlab.ErrUnknownTokenScope) 86 | } else { 87 | assert.NoError(t, err) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /type_token_type.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "slices" 7 | ) 8 | 9 | type TokenType string 10 | 11 | const ( 12 | TokenTypePersonal = TokenType("personal") 13 | TokenTypeProject = TokenType("project") 14 | TokenTypeGroup = TokenType("group") 15 | TokenTypeUserServiceAccount = TokenType("user-service-account") 16 | TokenTypeGroupServiceAccount = TokenType("group-service-account") 17 | TokenTypePipelineProjectTrigger = TokenType("pipeline-project-trigger") 18 | TokenTypeProjectDeploy = TokenType("project-deploy") 19 | TokenTypeGroupDeploy = TokenType("group-deploy") 20 | 21 | TokenTypeUnknown = TokenType("") 22 | ) 23 | 24 | var ( 25 | ErrUnknownTokenType = errors.New("unknown token type") 26 | 27 | validTokenTypes = []string{ 28 | TokenTypePersonal.String(), 29 | TokenTypeProject.String(), 30 | TokenTypeGroup.String(), 31 | TokenTypeUserServiceAccount.String(), 32 | TokenTypeGroupServiceAccount.String(), 33 | TokenTypePipelineProjectTrigger.String(), 34 | TokenTypeProjectDeploy.String(), 35 | TokenTypeGroupDeploy.String(), 36 | } 37 | ) 38 | 39 | func (i TokenType) String() string { 40 | return string(i) 41 | } 42 | 43 | func (i TokenType) Value() string { 44 | return i.String() 45 | } 46 | 47 | func TokenTypeParse(value string) (TokenType, error) { 48 | if slices.Contains(validTokenTypes, value) { 49 | return TokenType(value), nil 50 | } 51 | return TokenTypeUnknown, fmt.Errorf("failed to parse '%s': %w", value, ErrUnknownTokenType) 52 | } 53 | -------------------------------------------------------------------------------- /type_token_type_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 11 | ) 12 | 13 | func TestTokenType(t *testing.T) { 14 | var tests = []struct { 15 | expected gitlab.TokenType 16 | input string 17 | err bool 18 | }{ 19 | { 20 | expected: gitlab.TokenTypePersonal, 21 | input: gitlab.TokenTypePersonal.String(), 22 | }, 23 | { 24 | expected: gitlab.TokenTypeGroup, 25 | input: gitlab.TokenTypeGroup.String(), 26 | }, 27 | { 28 | expected: gitlab.TokenTypeProject, 29 | input: gitlab.TokenTypeProject.String(), 30 | }, 31 | { 32 | expected: gitlab.TokenTypeUserServiceAccount, 33 | input: gitlab.TokenTypeUserServiceAccount.String(), 34 | }, 35 | { 36 | expected: gitlab.TokenTypeGroupServiceAccount, 37 | input: gitlab.TokenTypeGroupServiceAccount.String(), 38 | }, 39 | { 40 | expected: gitlab.TokenTypePipelineProjectTrigger, 41 | input: gitlab.TokenTypePipelineProjectTrigger.String(), 42 | }, 43 | { 44 | expected: gitlab.TokenTypeProjectDeploy, 45 | input: gitlab.TokenTypeProjectDeploy.String(), 46 | }, 47 | { 48 | expected: gitlab.TokenTypeGroupDeploy, 49 | input: gitlab.TokenTypeGroupDeploy.String(), 50 | }, 51 | { 52 | expected: gitlab.TokenTypeUnknown, 53 | input: "unknown", 54 | err: true, 55 | }, 56 | { 57 | expected: gitlab.TokenTypeUnknown, 58 | input: "unknown", 59 | err: true, 60 | }, 61 | } 62 | 63 | for _, test := range tests { 64 | t.Logf("assert parse(%s) = %s (err: %v)", test.input, test.expected, test.err) 65 | val, err := gitlab.TokenTypeParse(test.input) 66 | assert.EqualValues(t, test.expected, val) 67 | assert.EqualValues(t, test.expected.Value(), test.expected.String()) 68 | if test.err { 69 | assert.ErrorIs(t, err, gitlab.ErrUnknownTokenType) 70 | } else { 71 | assert.NoError(t, err) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func allowedValues(values ...string) (ret []any) { 9 | for _, value := range values { 10 | ret = append(ret, value) 11 | } 12 | return ret 13 | } 14 | 15 | func convertToInt(num any) (int, error) { 16 | switch val := num.(type) { 17 | case int: 18 | return val, nil 19 | case int8: 20 | return int(val), nil 21 | case int16: 22 | return int(val), nil 23 | case int32: 24 | return int(val), nil 25 | case int64: 26 | return int(val), nil 27 | case float32: 28 | return int(val), nil 29 | case float64: 30 | return int(val), nil 31 | } 32 | return 0, fmt.Errorf("%v: %w", num, ErrInvalidValue) 33 | } 34 | 35 | func calculateGitlabTTL(duration time.Duration, start time.Time) (ttl time.Duration, exp time.Time, err error) { 36 | start = start.UTC() 37 | const D = 24 * time.Hour 38 | const maxDuration = 365 * 24 * time.Hour 39 | if duration > maxDuration { 40 | duration = maxDuration 41 | } 42 | var val = start.Add(duration).Round(0) 43 | exp = val.AddDate(0, 0, 1).Truncate(D) 44 | ttl = exp.Sub(start.Round(0)) 45 | if ttl > maxDuration { 46 | m := start.Add(maxDuration) 47 | exp = time.Date(m.Year(), m.Month(), m.Day(), 0, 0, 0, 0, m.Location()) 48 | ttl = exp.Sub(start.Round(0)) 49 | } 50 | return ttl, exp, nil 51 | } 52 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | //go:build unit 2 | 3 | package gitlab 4 | 5 | import ( 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestConvertToInt(t *testing.T) { 14 | var tests = []struct { 15 | in any 16 | outVal int 17 | outErr error 18 | }{ 19 | {int(52), int(52), nil}, 20 | {int8(13), int(13), nil}, 21 | {int16(612), int(612), nil}, 22 | {int32(56236), int(56236), nil}, 23 | {int64(23462346), int(23462346), nil}, 24 | {float32(62346.62), int(62346), nil}, 25 | {float64(263467.26), int(263467), nil}, 26 | {"1", int(0), ErrInvalidValue}, 27 | } 28 | 29 | for _, tst := range tests { 30 | t.Logf("convertToInt(%T(%v))", tst.in, tst.in) 31 | val, err := convertToInt(tst.in) 32 | assert.Equal(t, tst.outVal, val) 33 | if tst.outErr != nil { 34 | assert.ErrorIs(t, err, tst.outErr) 35 | } 36 | } 37 | } 38 | 39 | func TestCalculateGitlabTTL(t *testing.T) { 40 | locMST, err := time.LoadLocation("MST") 41 | require.NoError(t, err) 42 | var tests = []struct { 43 | inDuration time.Duration 44 | inTime time.Time 45 | outDuration time.Duration 46 | outExpiry time.Time 47 | outErr error 48 | }{ 49 | // 1h on 2024-02-22T13:06:10.575Z, should expire 2024-02-23 50 | { 51 | inDuration: time.Hour, 52 | inTime: time.Date(2024, 2, 22, 13, 6, 10, 0, time.UTC), 53 | outDuration: (time.Hour * 10) + (53 * time.Minute) + (50 * time.Second), 54 | outExpiry: time.Date(2024, 2, 23, 0, 0, 0, 0, time.UTC), 55 | outErr: nil, 56 | }, 57 | // 3h 58 | { 59 | inDuration: 3 * time.Hour, 60 | inTime: time.Date(2024, 2, 22, 13, 0, 0, 0, time.UTC), 61 | outDuration: time.Hour * 11, 62 | outExpiry: time.Date(2024, 2, 23, 0, 0, 0, 0, time.UTC), 63 | outErr: nil, 64 | }, 65 | 66 | // 1h1s 67 | { 68 | inDuration: time.Hour + time.Second, 69 | inTime: time.Date(2024, 2, 22, 23, 0, 0, 0, time.UTC), 70 | outDuration: time.Hour * 25, 71 | outExpiry: time.Date(2024, 2, 24, 0, 0, 0, 0, time.UTC), 72 | outErr: nil, 73 | }, 74 | 75 | // 23h on 2024-02-22T20:00:00.000Z, should expire 2024-02-24 76 | { 77 | inDuration: time.Hour * 23, 78 | inTime: time.Date(2024, 2, 22, 20, 0, 0, 0, time.UTC), 79 | outDuration: 28 * time.Hour, 80 | outExpiry: time.Date(2024, 2, 24, 0, 0, 0, 0, time.UTC), 81 | outErr: nil, 82 | }, 83 | 84 | // 5h on 2024-02-22T20:00:00.000Z, should expire 2024-02-24 85 | { 86 | inDuration: time.Hour * 5, 87 | inTime: time.Date(2024, 2, 22, 20, 0, 0, 0, time.UTC), 88 | outDuration: time.Hour * 28, 89 | outExpiry: time.Date(2024, 2, 24, 0, 0, 0, 0, time.UTC), 90 | outErr: nil, 91 | }, 92 | 93 | // 45h on 2024-02-22T20:00:00.000Z, should expire 2024-02-25 94 | { 95 | inDuration: time.Hour * 45, 96 | inTime: time.Date(2024, 2, 22, 20, 0, 0, 0, time.UTC), 97 | outDuration: time.Hour * 52, 98 | outExpiry: time.Date(2024, 2, 25, 0, 0, 0, 0, time.UTC), 99 | outErr: nil, 100 | }, 101 | 102 | { 103 | inDuration: time.Hour * 2, 104 | // 2024-05-30 13:01:43 -0600 MDT 105 | inTime: time.Date(2024, 5, 30, 13, 01, 43, 0, locMST), 106 | outDuration: time.Hour*3 + time.Minute*58 + time.Second*17, 107 | outExpiry: time.Date(2024, 5, 31, 0, 0, 0, 0, time.UTC), 108 | outErr: nil, 109 | }, 110 | 111 | { 112 | inDuration: 390 * 25 * time.Hour, 113 | inTime: time.Date(2024, 7, 11, 15, 41, 0, 0, time.UTC), 114 | outDuration: 8744*time.Hour + 19*time.Minute, 115 | outExpiry: time.Date(2025, 7, 11, 0, 0, 0, 0, time.UTC), 116 | outErr: nil, 117 | }, 118 | } 119 | 120 | for _, tst := range tests { 121 | t.Logf("calculateGitlabTTL(%s, %s) = duration %s, expiry %s, error %v", tst.inDuration, tst.inTime.Format(time.RFC3339), tst.outDuration, tst.outExpiry.Format(time.RFC3339), tst.outErr) 122 | dur, exp, err := calculateGitlabTTL(tst.inDuration, tst.inTime) 123 | if err != nil { 124 | assert.ErrorIs(t, err, tst.outErr) 125 | } 126 | assert.EqualValues(t, tst.outExpiry, exp) 127 | assert.WithinDuration(t, tst.outExpiry, exp, time.Minute) 128 | assert.EqualValues(t, tst.outDuration, dur) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package gitlab 2 | 3 | var Version string = "v0.0.0-dev" 4 | var FullCommit string 5 | var BuildDate string 6 | -------------------------------------------------------------------------------- /with_admin_user_pat_gitlab_revokes_token_test.go: -------------------------------------------------------------------------------- 1 | //go:build local 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/hashicorp/vault/sdk/logical" 14 | "github.com/stretchr/testify/require" 15 | g "gitlab.com/gitlab-org/api/client-go" 16 | 17 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 18 | ) 19 | 20 | func TestWithAdminUser_PAT_AdminUser_GitlabRevokesToken(t *testing.T) { 21 | httpClient, url := getClient(t, "local") 22 | ctx := gitlab.HttpClientNewContext(t.Context(), httpClient) 23 | 24 | b, l, events, err := getBackendWithEvents(ctx) 25 | require.NoError(t, err) 26 | var tokenName = "admin_user_initial_token" 27 | 28 | resp, err := b.HandleRequest(ctx, &logical.Request{ 29 | Operation: logical.UpdateOperation, 30 | Path: fmt.Sprintf("%s/%s", gitlab.PathConfigStorage, gitlab.DefaultConfigName), Storage: l, 31 | Data: map[string]any{ 32 | "token": getGitlabToken(tokenName).Token, 33 | "base_url": url, 34 | "auto_rotate_token": true, 35 | "auto_rotate_before": "24h", 36 | "type": gitlab.TypeSelfManaged.String(), 37 | }, 38 | }) 39 | 40 | require.NoError(t, err) 41 | require.NotNil(t, resp) 42 | require.NoError(t, resp.Error()) 43 | require.NotEmpty(t, events) 44 | 45 | var c *g.Client 46 | var token string 47 | var secret *logical.Secret 48 | 49 | { 50 | resp, err := b.HandleRequest(ctx, &logical.Request{ 51 | Operation: logical.CreateOperation, 52 | Path: fmt.Sprintf("%s/normal-user", gitlab.PathRoleStorage), Storage: l, 53 | Data: map[string]any{ 54 | "path": "normal-user", 55 | "name": gitlab.TokenTypePersonal.String(), 56 | "token_type": gitlab.TokenTypePersonal.String(), 57 | "ttl": time.Hour * 120, 58 | "gitlab_revokes_token": strconv.FormatBool(true), 59 | "scopes": strings.Join( 60 | []string{ 61 | gitlab.TokenScopeReadApi.String(), 62 | }, 63 | ","), 64 | }, 65 | }) 66 | require.NoError(t, err) 67 | require.NotNil(t, resp) 68 | require.NoError(t, resp.Error()) 69 | } 70 | 71 | // issue a personal access token 72 | { 73 | ctxIssueToken, _ := ctxTestTime(ctx, t.Name(), tokenName) 74 | resp, err := b.HandleRequest(ctxIssueToken, &logical.Request{ 75 | Operation: logical.ReadOperation, Storage: l, 76 | Path: fmt.Sprintf("%s/normal-user", gitlab.PathTokenRoleStorage), 77 | }) 78 | 79 | require.NoError(t, err) 80 | require.NotNil(t, resp) 81 | require.NoError(t, resp.Error()) 82 | token = resp.Data["token"].(string) 83 | require.NotEmpty(t, token) 84 | secret = resp.Secret 85 | require.NotNil(t, secret) 86 | } 87 | 88 | c, err = g.NewClient(token, g.WithHTTPClient(httpClient), g.WithBaseURL(url)) 89 | require.NoError(t, err) 90 | require.NotNil(t, c) 91 | 92 | // should have access with token to Gitlab 93 | { 94 | var pat *g.PersonalAccessToken 95 | var r *g.Response 96 | pat, r, err = c.PersonalAccessTokens.GetSinglePersonalAccessToken() 97 | require.NoError(t, err) 98 | require.NotNil(t, pat) 99 | require.NotNil(t, r) 100 | require.EqualValues(t, r.StatusCode, http.StatusOK) 101 | } 102 | 103 | // revoke the token 104 | { 105 | resp, err := b.HandleRequest(ctx, &logical.Request{ 106 | Operation: logical.RevokeOperation, 107 | Path: "/", 108 | Storage: l, 109 | Secret: secret, 110 | }) 111 | require.NoError(t, err) 112 | require.Nil(t, resp) 113 | } 114 | 115 | // should still have access with token to Gitlab as Gitlab is managing the revocation 116 | { 117 | var pat *g.PersonalAccessToken 118 | var r *g.Response 119 | pat, r, err = c.PersonalAccessTokens.GetSinglePersonalAccessToken() 120 | require.NoError(t, err) 121 | require.NotNil(t, pat) 122 | require.NotNil(t, r) 123 | require.EqualValues(t, r.StatusCode, http.StatusOK) 124 | } 125 | 126 | events.expectEvents(t, []expectedEvent{ 127 | {eventType: "gitlab/config-write"}, 128 | {eventType: "gitlab/role-write"}, 129 | {eventType: "gitlab/token-write"}, 130 | {eventType: "gitlab/token-revoke"}, 131 | }) 132 | 133 | } 134 | -------------------------------------------------------------------------------- /with_admin_user_pat_vault_revokes_token_test.go: -------------------------------------------------------------------------------- 1 | //go:build local 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/hashicorp/vault/sdk/logical" 14 | "github.com/stretchr/testify/require" 15 | g "gitlab.com/gitlab-org/api/client-go" 16 | 17 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 18 | ) 19 | 20 | func TestWithAdminUser_PAT_AdminUser_VaultRevokesToken(t *testing.T) { 21 | httpClient, url := getClient(t, "local") 22 | ctx := gitlab.HttpClientNewContext(t.Context(), httpClient) 23 | var tokenName = "admin_user_initial_token" 24 | 25 | b, l, events, err := getBackendWithEvents(ctx) 26 | require.NoError(t, err) 27 | 28 | resp, err := b.HandleRequest(ctx, &logical.Request{ 29 | Operation: logical.UpdateOperation, 30 | Path: fmt.Sprintf("%s/%s", gitlab.PathConfigStorage, gitlab.DefaultConfigName), Storage: l, 31 | Data: map[string]any{ 32 | "token": getGitlabToken(tokenName).Token, 33 | "base_url": url, 34 | "auto_rotate_token": true, 35 | "auto_rotate_before": "24h", 36 | "type": gitlab.TypeSelfManaged.String(), 37 | }, 38 | }) 39 | 40 | require.NoError(t, err) 41 | require.NotNil(t, resp) 42 | require.NoError(t, resp.Error()) 43 | require.NotEmpty(t, events) 44 | 45 | var c *g.Client 46 | var token string 47 | var secret *logical.Secret 48 | 49 | // create the role 50 | { 51 | resp, err := b.HandleRequest(ctx, &logical.Request{ 52 | Operation: logical.CreateOperation, Storage: l, 53 | Path: fmt.Sprintf("%s/admin-user", gitlab.PathRoleStorage), 54 | Data: map[string]any{ 55 | "path": "admin-user", 56 | "name": gitlab.TokenTypePersonal.String(), 57 | "token_type": gitlab.TokenTypePersonal.String(), 58 | "scopes": strings.Join( 59 | []string{ 60 | gitlab.TokenScopeReadApi.String(), 61 | }, 62 | ","), 63 | "ttl": time.Hour, 64 | "gitlab_revokes_token": strconv.FormatBool(false), 65 | }, 66 | }) 67 | require.NoError(t, err) 68 | require.NotNil(t, resp) 69 | require.NoError(t, resp.Error()) 70 | } 71 | 72 | // issue a personal access token 73 | { 74 | ctxIssueToken, _ := ctxTestTime(ctx, t.Name(), tokenName) 75 | resp, err := b.HandleRequest(ctxIssueToken, &logical.Request{ 76 | Operation: logical.ReadOperation, Storage: l, 77 | Path: fmt.Sprintf("%s/admin-user", gitlab.PathTokenRoleStorage), 78 | }) 79 | 80 | require.NoError(t, err) 81 | require.NotNil(t, resp) 82 | require.NoError(t, resp.Error()) 83 | token = resp.Data["token"].(string) 84 | require.NotEmpty(t, token) 85 | secret = resp.Secret 86 | require.NotNil(t, secret) 87 | } 88 | 89 | c, err = g.NewClient(token, g.WithHTTPClient(httpClient), g.WithBaseURL(url)) 90 | require.NoError(t, err) 91 | require.NotNil(t, c) 92 | 93 | // should have access with token to Gitlab 94 | { 95 | var pat *g.PersonalAccessToken 96 | var r *g.Response 97 | pat, r, err = c.PersonalAccessTokens.GetSinglePersonalAccessToken() 98 | require.NoError(t, err) 99 | require.NotNil(t, pat) 100 | require.NotNil(t, r) 101 | require.EqualValues(t, r.StatusCode, http.StatusOK) 102 | } 103 | 104 | // revoke the token 105 | { 106 | resp, err := b.HandleRequest(ctx, &logical.Request{ 107 | Operation: logical.RevokeOperation, 108 | Path: "/", 109 | Storage: l, 110 | Secret: secret, 111 | }) 112 | require.NoError(t, err) 113 | require.Nil(t, resp) 114 | } 115 | 116 | // no longer has access with token to Gitlab 117 | { 118 | var pat *g.PersonalAccessToken 119 | var r *g.Response 120 | pat, r, err = c.PersonalAccessTokens.GetSinglePersonalAccessToken() 121 | require.Error(t, err) 122 | require.Nil(t, pat) 123 | require.NotNil(t, r) 124 | require.EqualValues(t, r.StatusCode, http.StatusUnauthorized) 125 | } 126 | 127 | events.expectEvents(t, []expectedEvent{ 128 | {eventType: "gitlab/config-write"}, 129 | {eventType: "gitlab/role-write"}, 130 | {eventType: "gitlab/token-write"}, 131 | {eventType: "gitlab/token-revoke"}, 132 | }) 133 | } 134 | -------------------------------------------------------------------------------- /with_gitlab_com_user_rotate_token_test.go: -------------------------------------------------------------------------------- 1 | //go:build saas 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/hashicorp/vault/sdk/logical" 11 | "github.com/stretchr/testify/require" 12 | g "gitlab.com/gitlab-org/api/client-go" 13 | 14 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 15 | ) 16 | 17 | func TestWithGitlabUser_RotateToken(t *testing.T) { 18 | httpClient, _ := getClient(t, "saas") 19 | ctx := gitlab.HttpClientNewContext(t.Context(), httpClient) 20 | var tokenName = "" 21 | 22 | b, l, events, err := getBackendWithEvents(ctx) 23 | require.NoError(t, err) 24 | 25 | resp, err := b.HandleRequest(ctx, &logical.Request{ 26 | Operation: logical.UpdateOperation, 27 | Path: fmt.Sprintf("%s/%s", gitlab.PathConfigStorage, gitlab.DefaultConfigName), 28 | Storage: l, 29 | Data: map[string]any{ 30 | "token": gitlabComPersonalAccessToken, 31 | "base_url": gitlabComUrl, 32 | "auto_rotate_token": true, 33 | "auto_rotate_before": "24h", 34 | "type": gitlab.TypeSaaS.String(), 35 | }, 36 | }) 37 | 38 | require.NoError(t, err) 39 | require.NotNil(t, resp) 40 | require.NoError(t, resp.Error()) 41 | require.NotEmpty(t, events) 42 | 43 | var oldToken, newToken string 44 | 45 | // Rotate the main token 46 | { 47 | ctxRotate, _ := ctxTestTime(ctx, t.Name(), tokenName) 48 | resp, err := b.HandleRequest(ctxRotate, &logical.Request{ 49 | Operation: logical.UpdateOperation, 50 | Path: fmt.Sprintf("%s/%s/rotate", gitlab.PathConfigStorage, gitlab.DefaultConfigName), Storage: l, 51 | Data: map[string]any{}, 52 | }) 53 | require.NoError(t, err) 54 | require.NotNil(t, resp) 55 | require.NotEqualValues(t, resp.Data["token"], gitlabComPersonalAccessToken) 56 | oldToken = gitlabComPersonalAccessToken 57 | newToken = resp.Data["token"].(string) 58 | require.Nil(t, resp.Secret) // This must not be a secret 59 | } 60 | 61 | // Old token should not have access anymore 62 | { 63 | c, err := g.NewClient(oldToken, g.WithHTTPClient(httpClient), g.WithBaseURL(gitlabComUrl)) 64 | require.NoError(t, err) 65 | require.NotNil(t, c) 66 | pat, r, err := c.PersonalAccessTokens.GetSinglePersonalAccessToken() 67 | require.Error(t, err) 68 | require.Nil(t, pat) 69 | require.NotNil(t, r) 70 | require.EqualValues(t, r.StatusCode, http.StatusUnauthorized) 71 | } 72 | 73 | // New token should have access 74 | { 75 | c, err := g.NewClient(newToken, g.WithHTTPClient(httpClient), g.WithBaseURL(gitlabComUrl)) 76 | require.NoError(t, err) 77 | require.NotNil(t, c) 78 | pat, r, err := c.PersonalAccessTokens.GetSinglePersonalAccessToken() 79 | require.NoError(t, err) 80 | require.NotNil(t, pat) 81 | require.NotNil(t, r) 82 | require.EqualValues(t, r.StatusCode, http.StatusOK) 83 | } 84 | 85 | events.expectEvents(t, []expectedEvent{ 86 | {eventType: "gitlab/config-write"}, 87 | {eventType: "gitlab/config-token-rotate"}, 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /with_group_deploy_token_test.go: -------------------------------------------------------------------------------- 1 | //go:build local 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "fmt" 7 | "strconv" 8 | "testing" 9 | "time" 10 | 11 | "github.com/hashicorp/vault/sdk/logical" 12 | "github.com/stretchr/testify/require" 13 | g "gitlab.com/gitlab-org/api/client-go" 14 | 15 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 16 | ) 17 | 18 | func TestWithGroupDeployToken(t *testing.T) { 19 | httpClient, url := getClient(t, "local") 20 | ctx := gitlab.HttpClientNewContext(t.Context(), httpClient) 21 | var tokenName = "normal_user_initial_token" 22 | 23 | b, l, events, err := getBackendWithEvents(ctx) 24 | require.NoError(t, err) 25 | 26 | resp, err := b.HandleRequest(ctx, &logical.Request{ 27 | Operation: logical.UpdateOperation, 28 | Path: fmt.Sprintf("%s/%s", gitlab.PathConfigStorage, gitlab.DefaultConfigName), Storage: l, 29 | Data: map[string]any{ 30 | "token": getGitlabToken(tokenName).Token, 31 | "base_url": url, 32 | "auto_rotate_token": true, 33 | "auto_rotate_before": "24h", 34 | "type": gitlab.TypeSelfManaged.String(), 35 | }, 36 | }) 37 | 38 | require.NoError(t, err) 39 | require.NotNil(t, resp) 40 | require.NoError(t, resp.Error()) 41 | require.NotEmpty(t, events) 42 | 43 | var c *g.Client 44 | var token string 45 | var secret *logical.Secret 46 | 47 | { 48 | resp, err := b.HandleRequest(ctx, &logical.Request{ 49 | Operation: logical.CreateOperation, 50 | Path: fmt.Sprintf("%s/role", gitlab.PathRoleStorage), Storage: l, 51 | Data: map[string]any{ 52 | "path": "example", 53 | "name": gitlab.TokenTypeGroupDeploy.String(), 54 | "token_type": gitlab.TokenTypeGroupDeploy.String(), 55 | "gitlab_revokes_token": strconv.FormatBool(false), 56 | "ttl": 120 * time.Hour, 57 | "scopes": []string{gitlab.TokenScopeReadRepository.String()}, 58 | }, 59 | }) 60 | require.NoError(t, err) 61 | require.NotNil(t, resp) 62 | require.NoError(t, resp.Error()) 63 | } 64 | 65 | { 66 | ctxIssueToken, _ := ctxTestTime(ctx, t.Name(), tokenName) 67 | resp, err := b.HandleRequest(ctxIssueToken, &logical.Request{ 68 | Operation: logical.ReadOperation, Storage: l, 69 | Path: fmt.Sprintf("%s/role", gitlab.PathTokenRoleStorage), 70 | }) 71 | 72 | require.NoError(t, err) 73 | require.NotNil(t, resp) 74 | require.NoError(t, resp.Error()) 75 | token = resp.Data["token"].(string) 76 | require.NotEmpty(t, token) 77 | secret = resp.Secret 78 | require.NotNil(t, secret) 79 | } 80 | 81 | c = b.GetClient(gitlab.DefaultConfigName).GitlabClient(ctx) 82 | require.NotNil(t, c) 83 | 84 | { 85 | tt, _, err := c.DeployTokens.ListGroupDeployTokens("example", &g.ListGroupDeployTokensOptions{}) 86 | require.NoError(t, err) 87 | out := filterSlice(tt, func(item *g.DeployToken, index int) bool { return !item.Expired && !item.Revoked }) 88 | require.Len(t, out, 1) 89 | } 90 | 91 | { 92 | resp, err := b.HandleRequest(ctx, &logical.Request{ 93 | Operation: logical.RevokeOperation, 94 | Path: "/", 95 | Storage: l, 96 | Secret: secret, 97 | }) 98 | require.NoError(t, err) 99 | require.Nil(t, resp) 100 | } 101 | 102 | { 103 | tt, _, err := c.DeployTokens.ListGroupDeployTokens("example", &g.ListGroupDeployTokensOptions{}) 104 | require.NoError(t, err) 105 | out := filterSlice(tt, func(item *g.DeployToken, index int) bool { return !item.Expired && !item.Revoked }) 106 | require.Len(t, out, 0) 107 | } 108 | 109 | events.expectEvents(t, []expectedEvent{ 110 | {eventType: "gitlab/config-write"}, 111 | {eventType: "gitlab/role-write"}, 112 | {eventType: "gitlab/token-write"}, 113 | {eventType: "gitlab/token-revoke"}, 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /with_normal_user_gat_test.go: -------------------------------------------------------------------------------- 1 | //go:build local 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/hashicorp/vault/sdk/logical" 14 | "github.com/stretchr/testify/require" 15 | g "gitlab.com/gitlab-org/api/client-go" 16 | 17 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 18 | ) 19 | 20 | func TestWithNormalUser_GAT(t *testing.T) { 21 | httpClient, url := getClient(t, "local") 22 | ctx := gitlab.HttpClientNewContext(t.Context(), httpClient) 23 | var tokenName = "normal_user_initial_token" 24 | 25 | b, l, events, err := getBackendWithEvents(ctx) 26 | require.NoError(t, err) 27 | 28 | resp, err := b.HandleRequest(ctx, &logical.Request{ 29 | Operation: logical.UpdateOperation, 30 | Path: fmt.Sprintf("%s/%s", gitlab.PathConfigStorage, gitlab.DefaultConfigName), Storage: l, 31 | Data: map[string]any{ 32 | "token": getGitlabToken(tokenName).Token, 33 | "base_url": url, 34 | "auto_rotate_token": true, 35 | "auto_rotate_before": "24h", 36 | "type": gitlab.TypeSelfManaged.String(), 37 | }, 38 | }) 39 | 40 | require.NoError(t, err) 41 | require.NotNil(t, resp) 42 | require.NoError(t, resp.Error()) 43 | require.NotEmpty(t, events) 44 | 45 | var token string 46 | var secret *logical.Secret 47 | 48 | { 49 | resp, err := b.HandleRequest(ctx, &logical.Request{ 50 | Operation: logical.CreateOperation, 51 | Path: fmt.Sprintf("%s/gat", gitlab.PathRoleStorage), Storage: l, 52 | Data: map[string]any{ 53 | "path": "example", 54 | "name": `gat-token`, 55 | "token_type": gitlab.TokenTypeGroup.String(), 56 | "ttl": time.Hour * 120, 57 | "gitlab_revokes_token": strconv.FormatBool(false), 58 | "access_level": gitlab.AccessLevelMaintainerPermissions.String(), 59 | "scopes": strings.Join([]string{gitlab.TokenScopeReadApi.String()}, ","), 60 | }, 61 | }) 62 | require.NoError(t, err) 63 | require.NotNil(t, resp) 64 | require.NoError(t, resp.Error()) 65 | } 66 | 67 | // issue a group access token 68 | { 69 | ctxIssueToken, _ := ctxTestTime(ctx, t.Name(), tokenName) 70 | resp, err := b.HandleRequest(ctxIssueToken, &logical.Request{ 71 | Operation: logical.ReadOperation, Storage: l, 72 | Path: fmt.Sprintf("%s/gat", gitlab.PathTokenRoleStorage), 73 | }) 74 | 75 | require.NoError(t, err) 76 | require.NotNil(t, resp) 77 | require.NoError(t, resp.Error()) 78 | token = resp.Data["token"].(string) 79 | require.NotEmpty(t, token) 80 | secret = resp.Secret 81 | require.NotNil(t, secret) 82 | } 83 | 84 | var c *g.Client 85 | c, err = g.NewClient(token, g.WithHTTPClient(httpClient), g.WithBaseURL(url)) 86 | require.NoError(t, err) 87 | require.NotNil(t, c) 88 | 89 | // should have access with token to Gitlab 90 | { 91 | var pat *g.PersonalAccessToken 92 | var r *g.Response 93 | pat, r, err = c.PersonalAccessTokens.GetSinglePersonalAccessToken() 94 | require.NoError(t, err) 95 | require.NotNil(t, pat) 96 | require.NotNil(t, r) 97 | require.EqualValues(t, r.StatusCode, http.StatusOK) 98 | } 99 | 100 | // revoke the token 101 | { 102 | resp, err := b.HandleRequest(ctx, &logical.Request{ 103 | Operation: logical.RevokeOperation, 104 | Path: "/", 105 | Storage: l, 106 | Secret: secret, 107 | }) 108 | require.NoError(t, err) 109 | require.Nil(t, resp) 110 | } 111 | 112 | // no longer has access with token to Gitlab 113 | { 114 | var pat *g.PersonalAccessToken 115 | var r *g.Response 116 | pat, r, err = c.PersonalAccessTokens.GetSinglePersonalAccessToken() 117 | require.Error(t, err) 118 | require.Nil(t, pat) 119 | require.NotNil(t, r) 120 | require.EqualValues(t, r.StatusCode, http.StatusUnauthorized) 121 | } 122 | 123 | events.expectEvents(t, []expectedEvent{ 124 | {eventType: "gitlab/config-write"}, 125 | {eventType: "gitlab/role-write"}, 126 | {eventType: "gitlab/token-write"}, 127 | {eventType: "gitlab/token-revoke"}, 128 | }) 129 | } 130 | -------------------------------------------------------------------------------- /with_normal_user_personal_at_fails_test.go: -------------------------------------------------------------------------------- 1 | //go:build local 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/hashicorp/vault/sdk/logical" 13 | "github.com/stretchr/testify/require" 14 | 15 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 16 | ) 17 | 18 | func TestWithNormalUser_PersonalAT_Fails(t *testing.T) { 19 | httpClient, url := getClient(t, "local") 20 | ctx := gitlab.HttpClientNewContext(t.Context(), httpClient) 21 | var tokenName = "normal_user_initial_token" 22 | 23 | b, l, events, err := getBackendWithEvents(ctx) 24 | require.NoError(t, err) 25 | 26 | resp, err := b.HandleRequest(ctx, &logical.Request{ 27 | Operation: logical.UpdateOperation, 28 | Path: fmt.Sprintf("%s/%s", gitlab.PathConfigStorage, gitlab.DefaultConfigName), Storage: l, 29 | Data: map[string]any{ 30 | "token": getGitlabToken(tokenName).Token, 31 | "base_url": url, 32 | "auto_rotate_token": true, 33 | "auto_rotate_before": "24h", 34 | "type": gitlab.TypeSelfManaged.String(), 35 | }, 36 | }) 37 | 38 | require.NoError(t, err) 39 | require.NotNil(t, resp) 40 | require.NoError(t, resp.Error()) 41 | require.NotEmpty(t, events) 42 | 43 | { 44 | resp, err := b.HandleRequest(ctx, &logical.Request{ 45 | Operation: logical.CreateOperation, 46 | Path: fmt.Sprintf("%s/normal-user", gitlab.PathRoleStorage), Storage: l, 47 | Data: map[string]any{ 48 | "path": "normal-user", 49 | "name": gitlab.TokenTypePersonal.String(), 50 | "token_type": gitlab.TokenTypePersonal.String(), 51 | "ttl": time.Hour * 120, 52 | "gitlab_revokes_token": strconv.FormatBool(true), 53 | "scopes": strings.Join( 54 | []string{ 55 | gitlab.TokenScopeReadApi.String(), 56 | }, 57 | ","), 58 | }, 59 | }) 60 | require.NoError(t, err) 61 | require.NotNil(t, resp) 62 | require.NoError(t, resp.Error()) 63 | } 64 | 65 | // issue a personal access token 66 | { 67 | ctxIssueToken, _ := ctxTestTime(ctx, t.Name(), tokenName) 68 | resp, err := b.HandleRequest(ctxIssueToken, &logical.Request{ 69 | Operation: logical.ReadOperation, Storage: l, 70 | Path: fmt.Sprintf("%s/normal-user", gitlab.PathTokenRoleStorage), 71 | }) 72 | 73 | require.Nil(t, resp) 74 | require.Error(t, err) 75 | require.ErrorContains(t, err, "403 Forbidden") 76 | } 77 | 78 | events.expectEvents(t, []expectedEvent{ 79 | {eventType: "gitlab/config-write"}, 80 | {eventType: "gitlab/role-write"}, 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /with_normal_user_project_at_test.go: -------------------------------------------------------------------------------- 1 | //go:build local 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | "strings" 10 | "testing" 11 | "time" 12 | 13 | "github.com/hashicorp/vault/sdk/logical" 14 | "github.com/stretchr/testify/require" 15 | g "gitlab.com/gitlab-org/api/client-go" 16 | 17 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 18 | ) 19 | 20 | func TestWithNormalUser_ProjectAT(t *testing.T) { 21 | httpClient, url := getClient(t, "local") 22 | ctx := gitlab.HttpClientNewContext(t.Context(), httpClient) 23 | var tokenName = "normal_user_initial_token" 24 | 25 | b, l, events, err := getBackendWithEvents(ctx) 26 | require.NoError(t, err) 27 | 28 | resp, err := b.HandleRequest(ctx, &logical.Request{ 29 | Operation: logical.UpdateOperation, 30 | Path: fmt.Sprintf("%s/%s", gitlab.PathConfigStorage, gitlab.DefaultConfigName), Storage: l, 31 | Data: map[string]any{ 32 | "token": getGitlabToken(tokenName).Token, 33 | "base_url": url, 34 | "auto_rotate_token": true, 35 | "auto_rotate_before": "24h", 36 | "type": gitlab.TypeSelfManaged.String(), 37 | }, 38 | }) 39 | 40 | require.NoError(t, err) 41 | require.NotNil(t, resp) 42 | require.NoError(t, resp.Error()) 43 | require.NotEmpty(t, events) 44 | 45 | var c *g.Client 46 | var token string 47 | var secret *logical.Secret 48 | 49 | { 50 | resp, err := b.HandleRequest(ctx, &logical.Request{ 51 | Operation: logical.CreateOperation, 52 | Path: fmt.Sprintf("%s/pat", gitlab.PathRoleStorage), Storage: l, 53 | Data: map[string]any{ 54 | "path": "example/example", 55 | "name": gitlab.TokenTypeProject.String(), 56 | "token_type": gitlab.TokenTypeProject.String(), 57 | "ttl": time.Hour * 120, 58 | "gitlab_revokes_token": strconv.FormatBool(false), 59 | "access_level": gitlab.AccessLevelMaintainerPermissions.String(), 60 | "scopes": strings.Join( 61 | []string{ 62 | gitlab.TokenScopeReadApi.String(), 63 | }, 64 | ","), 65 | }, 66 | }) 67 | require.NoError(t, err) 68 | require.NotNil(t, resp) 69 | require.NoError(t, resp.Error()) 70 | } 71 | 72 | // issue a group access token 73 | { 74 | ctxIssueToken, _ := ctxTestTime(ctx, t.Name(), tokenName) 75 | resp, err := b.HandleRequest(ctxIssueToken, &logical.Request{ 76 | Operation: logical.ReadOperation, Storage: l, 77 | Path: fmt.Sprintf("%s/pat", gitlab.PathTokenRoleStorage), 78 | }) 79 | 80 | require.NoError(t, err) 81 | require.NotNil(t, resp) 82 | require.NoError(t, resp.Error()) 83 | token = resp.Data["token"].(string) 84 | require.NotEmpty(t, token) 85 | secret = resp.Secret 86 | require.NotNil(t, secret) 87 | } 88 | 89 | c, err = g.NewClient(token, g.WithHTTPClient(httpClient), g.WithBaseURL(url)) 90 | require.NoError(t, err) 91 | require.NotNil(t, c) 92 | 93 | // should have access with token to Gitlab 94 | { 95 | var pat *g.PersonalAccessToken 96 | var r *g.Response 97 | pat, r, err = c.PersonalAccessTokens.GetSinglePersonalAccessToken() 98 | require.NoError(t, err) 99 | require.NotNil(t, pat) 100 | require.NotNil(t, r) 101 | require.EqualValues(t, r.StatusCode, http.StatusOK) 102 | } 103 | 104 | // revoke the token 105 | { 106 | resp, err := b.HandleRequest(ctx, &logical.Request{ 107 | Operation: logical.RevokeOperation, 108 | Path: "/", 109 | Storage: l, 110 | Secret: secret, 111 | }) 112 | require.NoError(t, err) 113 | require.Nil(t, resp) 114 | } 115 | 116 | // no longer has access with token to Gitlab 117 | { 118 | var pat *g.PersonalAccessToken 119 | var r *g.Response 120 | pat, r, err = c.PersonalAccessTokens.GetSinglePersonalAccessToken() 121 | require.Error(t, err) 122 | require.Nil(t, pat) 123 | require.NotNil(t, r) 124 | require.EqualValues(t, r.StatusCode, http.StatusUnauthorized) 125 | } 126 | 127 | events.expectEvents(t, []expectedEvent{ 128 | {eventType: "gitlab/config-write"}, 129 | {eventType: "gitlab/role-write"}, 130 | {eventType: "gitlab/token-write"}, 131 | {eventType: "gitlab/token-revoke"}, 132 | }) 133 | } 134 | -------------------------------------------------------------------------------- /with_pipeline_project_trigger_token_test.go: -------------------------------------------------------------------------------- 1 | //go:build local 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "fmt" 7 | "strconv" 8 | "testing" 9 | 10 | "github.com/hashicorp/vault/sdk/logical" 11 | "github.com/stretchr/testify/require" 12 | g "gitlab.com/gitlab-org/api/client-go" 13 | 14 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 15 | ) 16 | 17 | func TestWithPipelineProjectTriggerAccessToken(t *testing.T) { 18 | httpClient, url := getClient(t, "local") 19 | ctx := gitlab.HttpClientNewContext(t.Context(), httpClient) 20 | var tokenName = "normal_user_initial_token" 21 | 22 | b, l, events, err := getBackendWithEvents(ctx) 23 | require.NoError(t, err) 24 | 25 | resp, err := b.HandleRequest(ctx, &logical.Request{ 26 | Operation: logical.UpdateOperation, 27 | Path: fmt.Sprintf("%s/%s", gitlab.PathConfigStorage, gitlab.DefaultConfigName), Storage: l, 28 | Data: map[string]any{ 29 | "token": getGitlabToken(tokenName).Token, 30 | "base_url": url, 31 | "auto_rotate_token": true, 32 | "auto_rotate_before": "24h", 33 | "type": gitlab.TypeSelfManaged.String(), 34 | }, 35 | }) 36 | 37 | require.NoError(t, err) 38 | require.NotNil(t, resp) 39 | require.NoError(t, resp.Error()) 40 | require.NotEmpty(t, events) 41 | 42 | var c *g.Client 43 | var token string 44 | var secret *logical.Secret 45 | 46 | { 47 | resp, err := b.HandleRequest(ctx, &logical.Request{ 48 | Operation: logical.CreateOperation, 49 | Path: fmt.Sprintf("%s/pptat", gitlab.PathRoleStorage), Storage: l, 50 | Data: map[string]any{ 51 | "path": "example/example", 52 | "name": gitlab.TokenTypePipelineProjectTrigger.String(), 53 | "token_type": gitlab.TokenTypePipelineProjectTrigger.String(), 54 | "gitlab_revokes_token": strconv.FormatBool(false), 55 | }, 56 | }) 57 | require.NoError(t, err) 58 | require.NotNil(t, resp) 59 | require.NoError(t, resp.Error()) 60 | } 61 | 62 | { 63 | ctxIssueToken, _ := ctxTestTime(ctx, t.Name(), tokenName) 64 | resp, err := b.HandleRequest(ctxIssueToken, &logical.Request{ 65 | Operation: logical.ReadOperation, Storage: l, 66 | Path: fmt.Sprintf("%s/pptat", gitlab.PathTokenRoleStorage), 67 | }) 68 | 69 | require.NoError(t, err) 70 | require.NotNil(t, resp) 71 | require.NoError(t, resp.Error()) 72 | token = resp.Data["token"].(string) 73 | require.NotEmpty(t, token) 74 | secret = resp.Secret 75 | require.NotNil(t, secret) 76 | } 77 | 78 | c = b.GetClient(gitlab.DefaultConfigName).GitlabClient(ctx) 79 | require.NotNil(t, c) 80 | 81 | { 82 | tt, _, err := c.PipelineTriggers.ListPipelineTriggers("example/example", &g.ListPipelineTriggersOptions{}) 83 | require.NoError(t, err) 84 | require.Len(t, tt, 1) 85 | } 86 | 87 | { 88 | resp, err := b.HandleRequest(ctx, &logical.Request{ 89 | Operation: logical.RevokeOperation, 90 | Path: "/", 91 | Storage: l, 92 | Secret: secret, 93 | }) 94 | require.NoError(t, err) 95 | require.Nil(t, resp) 96 | } 97 | 98 | { 99 | tt, _, err := c.PipelineTriggers.ListPipelineTriggers("example/example", &g.ListPipelineTriggersOptions{}) 100 | require.NoError(t, err) 101 | require.Len(t, tt, 0) 102 | } 103 | 104 | events.expectEvents(t, []expectedEvent{ 105 | {eventType: "gitlab/config-write"}, 106 | {eventType: "gitlab/role-write"}, 107 | {eventType: "gitlab/token-write"}, 108 | {eventType: "gitlab/token-revoke"}, 109 | }) 110 | } 111 | -------------------------------------------------------------------------------- /with_project_deploy_token_test.go: -------------------------------------------------------------------------------- 1 | //go:build local 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "fmt" 7 | "strconv" 8 | "testing" 9 | "time" 10 | 11 | "github.com/hashicorp/vault/sdk/logical" 12 | "github.com/stretchr/testify/require" 13 | g "gitlab.com/gitlab-org/api/client-go" 14 | 15 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 16 | ) 17 | 18 | func TestWithProjectDeployToken(t *testing.T) { 19 | httpClient, url := getClient(t, "local") 20 | ctx := gitlab.HttpClientNewContext(t.Context(), httpClient) 21 | var tokenName = "normal_user_initial_token" 22 | 23 | b, l, events, err := getBackendWithEvents(ctx) 24 | require.NoError(t, err) 25 | 26 | resp, err := b.HandleRequest(ctx, &logical.Request{ 27 | Operation: logical.UpdateOperation, 28 | Path: fmt.Sprintf("%s/%s", gitlab.PathConfigStorage, gitlab.DefaultConfigName), Storage: l, 29 | Data: map[string]any{ 30 | "token": getGitlabToken(tokenName).Token, 31 | "base_url": url, 32 | "auto_rotate_token": true, 33 | "auto_rotate_before": "24h", 34 | "type": gitlab.TypeSelfManaged.String(), 35 | }, 36 | }) 37 | 38 | require.NoError(t, err) 39 | require.NotNil(t, resp) 40 | require.NoError(t, resp.Error()) 41 | require.NotEmpty(t, events) 42 | 43 | var c *g.Client 44 | var token string 45 | var secret *logical.Secret 46 | 47 | { 48 | resp, err := b.HandleRequest(ctx, &logical.Request{ 49 | Operation: logical.CreateOperation, 50 | Path: fmt.Sprintf("%s/role", gitlab.PathRoleStorage), Storage: l, 51 | Data: map[string]any{ 52 | "path": "example/example", 53 | "name": gitlab.TokenTypeProjectDeploy.String(), 54 | "token_type": gitlab.TokenTypeProjectDeploy.String(), 55 | "gitlab_revokes_token": strconv.FormatBool(false), 56 | "ttl": 120 * time.Hour, 57 | "scopes": []string{gitlab.TokenScopeReadRepository.String()}, 58 | }, 59 | }) 60 | require.NoError(t, err) 61 | require.NotNil(t, resp) 62 | require.NoError(t, resp.Error()) 63 | } 64 | 65 | { 66 | ctxIssueToken, _ := ctxTestTime(ctx, t.Name(), tokenName) 67 | resp, err := b.HandleRequest(ctxIssueToken, &logical.Request{ 68 | Operation: logical.ReadOperation, Storage: l, 69 | Path: fmt.Sprintf("%s/role", gitlab.PathTokenRoleStorage), 70 | }) 71 | 72 | require.NoError(t, err) 73 | require.NotNil(t, resp) 74 | require.NoError(t, resp.Error()) 75 | token = resp.Data["token"].(string) 76 | require.NotEmpty(t, token) 77 | secret = resp.Secret 78 | require.NotNil(t, secret) 79 | } 80 | 81 | c = b.GetClient(gitlab.DefaultConfigName).GitlabClient(ctx) 82 | require.NotNil(t, c) 83 | 84 | { 85 | tt, _, err := c.DeployTokens.ListProjectDeployTokens("example/example", &g.ListProjectDeployTokensOptions{}) 86 | require.NoError(t, err) 87 | out := filterSlice(tt, func(item *g.DeployToken, index int) bool { return !item.Expired && !item.Revoked }) 88 | require.Len(t, out, 1) 89 | } 90 | 91 | { 92 | resp, err := b.HandleRequest(ctx, &logical.Request{ 93 | Operation: logical.RevokeOperation, 94 | Path: "/", 95 | Storage: l, 96 | Secret: secret, 97 | }) 98 | require.NoError(t, err) 99 | require.Nil(t, resp) 100 | } 101 | 102 | { 103 | tt, _, err := c.DeployTokens.ListProjectDeployTokens("example/example", &g.ListProjectDeployTokensOptions{}) 104 | require.NoError(t, err) 105 | out := filterSlice(tt, func(item *g.DeployToken, index int) bool { return !item.Expired && !item.Revoked }) 106 | require.Len(t, out, 0) 107 | } 108 | 109 | events.expectEvents(t, []expectedEvent{ 110 | {eventType: "gitlab/config-write"}, 111 | {eventType: "gitlab/role-write"}, 112 | {eventType: "gitlab/token-write"}, 113 | {eventType: "gitlab/token-revoke"}, 114 | }) 115 | } 116 | -------------------------------------------------------------------------------- /with_service_account_fail_test.go: -------------------------------------------------------------------------------- 1 | //go:build selfhosted 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "fmt" 7 | "testing" 8 | 9 | "github.com/hashicorp/vault/sdk/logical" 10 | "github.com/stretchr/testify/require" 11 | g "gitlab.com/gitlab-org/api/client-go" 12 | 13 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 14 | ) 15 | 16 | func TestWithServiceAccountUserFail(t *testing.T) { 17 | for _, typ := range []gitlab.Type{ 18 | gitlab.TypeSaaS, 19 | gitlab.TypeDedicated, 20 | } { 21 | t.Run(typ.String(), func(t *testing.T) { 22 | httpClient, _ := getClient(t, "selfhosted") 23 | ctx := gitlab.HttpClientNewContext(t.Context(), httpClient) 24 | 25 | b, l, events, err := getBackendWithEvents(ctx) 26 | require.NoError(t, err) 27 | 28 | resp, err := b.HandleRequest(ctx, &logical.Request{ 29 | Operation: logical.UpdateOperation, 30 | Path: fmt.Sprintf("%s/%s", gitlab.PathConfigStorage, gitlab.DefaultConfigName), Storage: l, 31 | Data: map[string]any{ 32 | "token": gitlabServiceAccountToken, 33 | "base_url": gitlabServiceAccountUrl, 34 | "auto_rotate_token": true, 35 | "auto_rotate_before": "24h", 36 | "type": typ.String(), 37 | }, 38 | }) 39 | 40 | require.NoError(t, err) 41 | require.NotNil(t, resp) 42 | require.NoError(t, resp.Error()) 43 | require.NotEmpty(t, events) 44 | 45 | require.NotNil(t, b.GetClient(gitlab.DefaultConfigName)) 46 | var gClient = b.GetClient(gitlab.DefaultConfigName).GitlabClient(ctx) 47 | require.NotNil(t, gClient) 48 | 49 | usr, _, err := gClient.Users.CreateServiceAccountUser(&g.CreateServiceAccountUserOptions{}) 50 | require.NoError(t, err) 51 | require.NotNil(t, usr) 52 | 53 | resp, err = b.HandleRequest(ctx, &logical.Request{ 54 | Operation: logical.CreateOperation, 55 | Path: fmt.Sprintf("%s/user-service-account", gitlab.PathRoleStorage), Storage: l, 56 | Data: map[string]any{ 57 | "path": usr.Username, 58 | "name": fmt.Sprintf(`user-service-account-%s`, usr.Username), 59 | "token_type": gitlab.TokenTypeUserServiceAccount.String(), 60 | "ttl": gitlab.DefaultAccessTokenMinTTL, 61 | "scopes": gitlab.ValidUserServiceAccountTokenScopes, 62 | "gitlab_revokes_token": false, 63 | }, 64 | }) 65 | require.Error(t, err) 66 | require.NotNil(t, resp) 67 | require.Error(t, resp.Error()) 68 | require.Empty(t, resp.Warnings) 69 | 70 | events.expectEvents(t, []expectedEvent{ 71 | {eventType: "gitlab/config-write"}, 72 | }) 73 | }) 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /with_service_account_group_test.go: -------------------------------------------------------------------------------- 1 | //go:build selfhosted 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "strconv" 9 | "testing" 10 | 11 | "github.com/hashicorp/vault/sdk/logical" 12 | "github.com/stretchr/testify/require" 13 | g "gitlab.com/gitlab-org/api/client-go" 14 | 15 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 16 | ) 17 | 18 | func TestWithServiceAccountGroup(t *testing.T) { 19 | httpClient, _ := getClient(t, "selfhosted") 20 | ctx := gitlab.HttpClientNewContext(t.Context(), httpClient) 21 | var tokenName = "" 22 | 23 | b, l, events, err := getBackendWithEvents(ctx) 24 | require.NoError(t, err) 25 | 26 | resp, err := b.HandleRequest(ctx, &logical.Request{ 27 | Operation: logical.UpdateOperation, 28 | Path: fmt.Sprintf("%s/%s", gitlab.PathConfigStorage, gitlab.DefaultConfigName), Storage: l, 29 | Data: map[string]any{ 30 | "token": gitlabServiceAccountToken, 31 | "base_url": gitlabServiceAccountUrl, 32 | "auto_rotate_token": true, 33 | "auto_rotate_before": "24h", 34 | "type": gitlab.TypeSelfManaged.String(), 35 | }, 36 | }) 37 | 38 | require.NoError(t, err) 39 | require.NotNil(t, resp) 40 | require.NoError(t, resp.Error()) 41 | require.NotEmpty(t, events) 42 | 43 | require.NotNil(t, b.GetClient(gitlab.DefaultConfigName)) 44 | var gClient = b.GetClient(gitlab.DefaultConfigName).GitlabClient(ctx) 45 | require.NotNil(t, gClient) 46 | 47 | // Create a group service account 48 | var gid = strconv.Itoa(265) 49 | sa, _, err := gClient.Groups.CreateServiceAccount(gid, &g.CreateServiceAccountOptions{}) 50 | require.NoError(t, err) 51 | require.NotNil(t, sa) 52 | 53 | t.Cleanup(func() { 54 | _, _ = gClient.Users.DeleteUser(sa.ID) 55 | }) 56 | 57 | // Create a group service account role 58 | resp, err = b.HandleRequest(ctx, &logical.Request{ 59 | Operation: logical.CreateOperation, 60 | Path: fmt.Sprintf("%s/group-service-account", gitlab.PathRoleStorage), Storage: l, 61 | Data: map[string]any{ 62 | "path": fmt.Sprintf("%s/%s", gid, sa.UserName), 63 | "name": `vault-generated-{{ .token_type }}-token`, 64 | "token_type": gitlab.TokenTypeGroupServiceAccount.String(), 65 | "ttl": gitlab.DefaultAccessTokenMinTTL, 66 | "scopes": gitlab.ValidGroupServiceAccountTokenScopes, 67 | "gitlab_revokes_token": false, 68 | }, 69 | }) 70 | require.NoError(t, err) 71 | require.NotNil(t, resp) 72 | require.NoError(t, resp.Error()) 73 | require.Empty(t, resp.Warnings) 74 | require.EqualValues(t, resp.Data["config_name"], gitlab.TypeConfigDefault) 75 | 76 | // Get a new token for the service account 77 | ctxIssueToken, _ := ctxTestTime(ctx, t.Name(), tokenName) 78 | resp, err = b.HandleRequest(ctxIssueToken, &logical.Request{ 79 | Operation: logical.ReadOperation, Storage: l, 80 | Path: fmt.Sprintf("%s/group-service-account", gitlab.PathTokenRoleStorage), 81 | }) 82 | 83 | require.NoError(t, err) 84 | require.NotNil(t, resp) 85 | require.NoError(t, resp.Error()) 86 | var token = resp.Data["token"].(string) 87 | require.NotEmpty(t, token) 88 | secret := resp.Secret 89 | require.NotNil(t, secret) 90 | 91 | { 92 | // Validate that the new token works 93 | c, err := g.NewClient(token, g.WithHTTPClient(httpClient), g.WithBaseURL(gitlabServiceAccountUrl)) 94 | require.NoError(t, err) 95 | require.NotNil(t, c) 96 | pat, resp, err := c.PersonalAccessTokens.GetSinglePersonalAccessToken() 97 | require.NoError(t, err) 98 | require.NotNil(t, resp) 99 | require.Equal(t, http.StatusOK, resp.StatusCode) 100 | require.NotNil(t, pat) 101 | } 102 | 103 | { 104 | resp, err := b.HandleRequest(ctx, &logical.Request{ 105 | Operation: logical.RevokeOperation, 106 | Path: "/", 107 | Storage: l, 108 | Secret: secret, 109 | }) 110 | require.NoError(t, err) 111 | require.Nil(t, resp) 112 | } 113 | 114 | events.expectEvents(t, []expectedEvent{ 115 | {eventType: "gitlab/config-write"}, 116 | {eventType: "gitlab/role-write"}, 117 | {eventType: "gitlab/token-write"}, 118 | {eventType: "gitlab/token-revoke"}, 119 | }) 120 | 121 | } 122 | -------------------------------------------------------------------------------- /with_service_account_user_test.go: -------------------------------------------------------------------------------- 1 | //go:build selfhosted 2 | 3 | package gitlab_test 4 | 5 | import ( 6 | "fmt" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/hashicorp/vault/sdk/logical" 11 | "github.com/stretchr/testify/require" 12 | g "gitlab.com/gitlab-org/api/client-go" 13 | 14 | gitlab "github.com/ilijamt/vault-plugin-secrets-gitlab" 15 | ) 16 | 17 | func TestWithServiceAccountUser(t *testing.T) { 18 | httpClient, _ := getClient(t, "selfhosted") 19 | ctx := gitlab.HttpClientNewContext(t.Context(), httpClient) 20 | var tokenName = "" 21 | 22 | b, l, events, err := getBackendWithEvents(ctx) 23 | require.NoError(t, err) 24 | 25 | resp, err := b.HandleRequest(ctx, &logical.Request{ 26 | Operation: logical.UpdateOperation, 27 | Path: fmt.Sprintf("%s/%s", gitlab.PathConfigStorage, gitlab.DefaultConfigName), Storage: l, 28 | Data: map[string]any{ 29 | "token": gitlabServiceAccountToken, 30 | "base_url": gitlabServiceAccountUrl, 31 | "auto_rotate_token": true, 32 | "auto_rotate_before": "24h", 33 | "type": gitlab.TypeSelfManaged.String(), 34 | }, 35 | }) 36 | 37 | require.NoError(t, err) 38 | require.NotNil(t, resp) 39 | require.NoError(t, resp.Error()) 40 | require.NotEmpty(t, events) 41 | 42 | require.NotNil(t, b.GetClient(gitlab.DefaultConfigName)) 43 | var gClient = b.GetClient(gitlab.DefaultConfigName).GitlabClient(ctx) 44 | require.NotNil(t, gClient) 45 | 46 | // Create a service account user 47 | usr, _, err := gClient.Users.CreateServiceAccountUser(&g.CreateServiceAccountUserOptions{}) 48 | require.NoError(t, err) 49 | require.NotNil(t, usr) 50 | 51 | t.Cleanup(func() { 52 | _, _ = gClient.Users.DeleteUser(usr.ID) 53 | }) 54 | 55 | // Create a user service account role 56 | resp, err = b.HandleRequest(ctx, &logical.Request{ 57 | Operation: logical.CreateOperation, 58 | Path: fmt.Sprintf("%s/user-service-account", gitlab.PathRoleStorage), Storage: l, 59 | Data: map[string]any{ 60 | "path": usr.Username, 61 | "name": `vault-generated-{{ .token_type }}-token`, 62 | "token_type": gitlab.TokenTypeUserServiceAccount.String(), 63 | "ttl": gitlab.DefaultAccessTokenMinTTL, 64 | "scopes": gitlab.ValidUserServiceAccountTokenScopes, 65 | "gitlab_revokes_token": false, 66 | }, 67 | }) 68 | require.NoError(t, err) 69 | require.NotNil(t, resp) 70 | require.NoError(t, resp.Error()) 71 | require.Empty(t, resp.Warnings) 72 | require.EqualValues(t, resp.Data["config_name"], gitlab.TypeConfigDefault) 73 | 74 | // Get a new token for the service account 75 | ctxIssueToken, _ := ctxTestTime(ctx, t.Name(), tokenName) 76 | resp, err = b.HandleRequest(ctxIssueToken, &logical.Request{ 77 | Operation: logical.ReadOperation, Storage: l, 78 | Path: fmt.Sprintf("%s/user-service-account", gitlab.PathTokenRoleStorage), 79 | }) 80 | 81 | require.NoError(t, err) 82 | require.NotNil(t, resp) 83 | require.NoError(t, resp.Error()) 84 | var token = resp.Data["token"].(string) 85 | require.NotEmpty(t, token) 86 | secret := resp.Secret 87 | require.NotNil(t, secret) 88 | 89 | { 90 | // Validate that the new token works 91 | c, err := g.NewClient(token, g.WithHTTPClient(httpClient), g.WithBaseURL(gitlabServiceAccountUrl)) 92 | require.NoError(t, err) 93 | require.NotNil(t, c) 94 | pat, resp, err := c.PersonalAccessTokens.GetSinglePersonalAccessToken() 95 | require.NoError(t, err) 96 | require.NotNil(t, resp) 97 | require.Equal(t, http.StatusOK, resp.StatusCode) 98 | require.NotNil(t, pat) 99 | } 100 | 101 | { 102 | resp, err := b.HandleRequest(ctx, &logical.Request{ 103 | Operation: logical.RevokeOperation, 104 | Path: "/", 105 | Storage: l, 106 | Secret: secret, 107 | }) 108 | require.NoError(t, err) 109 | require.Nil(t, resp) 110 | } 111 | 112 | events.expectEvents(t, []expectedEvent{ 113 | {eventType: "gitlab/config-write"}, 114 | {eventType: "gitlab/role-write"}, 115 | {eventType: "gitlab/token-write"}, 116 | {eventType: "gitlab/token-revoke"}, 117 | }) 118 | } 119 | --------------------------------------------------------------------------------