├── .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 |
--------------------------------------------------------------------------------