From ffa03ce93bdfb03a2ceb8b86d87cc596bbc6583c Mon Sep 17 00:00:00 2001 From: Vasilii Iakliushin Date: Thu, 19 Sep 2024 11:50:50 +0200 Subject: [PATCH] Protected branches API: support `deploy_key_id` Contributes to https://gitlab.com/gitlab-org/gitlab/-/issues/354657 **Problem** It's possible to modify `deploy_key_id` via UI, but this functionality is missing for REST API. **Solution** Allow to accept `deploy_key_id` for `Allowed to push` settings. Changelog: added EE: true --- doc/api/protected_branches.md | 66 +++++++++++++++- .../api/helpers/protected_branches_helpers.rb | 1 + .../protected_branches/push_access_levels.rb | 1 + .../requests/api/protected_branches_spec.rb | 78 +++++++++++++++++++ 4 files changed, 144 insertions(+), 2 deletions(-) diff --git a/doc/api/protected_branches.md b/doc/api/protected_branches.md index e12550d638838e..3c40ea54cbbe84 100644 --- a/doc/api/protected_branches.md +++ b/doc/api/protected_branches.md @@ -217,6 +217,8 @@ Example response: ## Protect repository branches +> - `deploy_key_id` configuration [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/166598) in GitLab 17.5. + Protects a single repository branch or several project repository branches using a wildcard protected branch. @@ -234,7 +236,7 @@ curl --request POST --header "PRIVATE-TOKEN: " "https://gitla | `name` | string | yes | The name of the branch or wildcard. | | `allow_force_push` | boolean | no | When enabled, members who can push to this branch can also force push. (default: `false`) | | `allowed_to_merge` | array | no | Array of merge access levels, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}`. Premium and Ultimate only. | -| `allowed_to_push` | array | no | Array of push access levels, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}`. Premium and Ultimate only. | +| `allowed_to_push` | array | no | Array of push access levels, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, `{deploy_key_id: integer}`, or `{access_level: integer}`. Premium and Ultimate only. | | `allowed_to_unprotect` | array | no | Array of unprotect access levels, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}`. The access level `No access` is not available for this field. Premium and Ultimate only. | | `code_owner_approval_required` | boolean | no | Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/codeowners/index.md). (defaults: false) Premium and Ultimate only. | | `merge_access_level` | integer | no | Access levels allowed to merge. (defaults: `40`, Maintainer role). | @@ -365,6 +367,62 @@ Example response: } ``` +### Example with deploy key access + +DETAILS: +**Tier:** Premium, Ultimate +**Offering:** GitLab.com, Self-managed, GitLab Dedicated + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/166598) in GitLab 17.5. + +Elements in the `allowed_to_push` array should take the form `{user_id: integer}`, `{group_id: integer}`, +`{deploy_key_id: integer}`, or `{access_level: integer}`. +The deploy key must be enabled for your project and it must have write access to your project repository. +For other requirements, see [Allow deploy keys to push to a protected branch](../user/project/repository/branches/protected.md#allow-deploy-keys-to-push-to-a-protected-branch). + +```shell +curl --request POST --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Bdeploy_key_id%5D=1" +``` + +Example response: + +```json +{ + "id": 1, + "name": "*-stable", + "push_access_levels": [ + { + "id": 1, + "access_level": null, + "user_id": null, + "group_id": null, + "deploy_key_id: 1, + "access_level_description": "Deploy" + } + ], + "merge_access_levels": [ + { + "id": 1, + "access_level": 40, + "user_id": null, + "group_id": null, + "access_level_description": "Maintainers" + } + ], + "unprotect_access_levels": [ + { + "id": 1, + "access_level": 40, + "user_id": null, + "group_id": null, + "access_level_description": "Maintainers" + } + ], + "allow_force_push":false, + "code_owner_approval_required": false +} +``` + ### Example with allow to push and allow to merge access DETAILS: @@ -456,6 +514,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: " "https://git ## Update a protected branch > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101903) in GitLab 15.6. +> - `deploy_key_id` configuration [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/166598) in GitLab 17.5. Updates a protected branch. @@ -473,7 +532,7 @@ curl --request PATCH --header "PRIVATE-TOKEN: " "https://gitl | `name` | string | yes | The name of the branch or wildcard. | | `allow_force_push` | boolean | no | When enabled, members who can push to this branch can also force push. | | `allowed_to_merge` | array | no | Array of merge access levels, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}`. Premium and Ultimate only. | -| `allowed_to_push` | array | no | Array of push access levels, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, or `{access_level: integer}`. Premium and Ultimate only. | +| `allowed_to_push` | array | no | Array of push access levels, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, `{deploy_key_id: integer}`, or `{access_level: integer}`. Premium and Ultimate only. | | `allowed_to_unprotect` | array | no | Array of unprotect access levels, with each described by a hash of the form `{user_id: integer}`, `{group_id: integer}`, `{access_level: integer}`, or `{id: integer, _destroy: true}` to destroy an existing access level. The access level `No access` is not available for this field. Premium and Ultimate only. | | `code_owner_approval_required` | boolean | no | Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/codeowners/index.md). Premium and Ultimate only. | @@ -481,12 +540,15 @@ Elements in the `allowed_to_push`, `allowed_to_merge` and `allowed_to_unprotect` `access_level`, and take the form `{user_id: integer}`, `{group_id: integer}` or `{access_level: integer}`. +`allowed_to_push` includes an extra element, `deploy_key_id`, that takes the form `{deploy_key_id: integer}`. + To update: - `user_id`: Ensure the updated user has access to the project. You must also pass the `id` of the `access_level` in the respective hash. - `group_id`: Ensure the updated group [has this project shared](../user/project/members/sharing_projects_groups.md). You must also pass the `id` of the `access_level` in the respective hash. +- `deploy_key_id`: Ensure the deploy key is enabled for your project and it must have write access to your project repository. To delete: diff --git a/ee/lib/ee/api/helpers/protected_branches_helpers.rb b/ee/lib/ee/api/helpers/protected_branches_helpers.rb index 0cb01cb5d4c442..ad0cde8463bb3e 100644 --- a/ee/lib/ee/api/helpers/protected_branches_helpers.rb +++ b/ee/lib/ee/api/helpers/protected_branches_helpers.rb @@ -23,6 +23,7 @@ module ProtectedBranchesHelpers optional :allowed_to_push, type: Array[JSON], desc: 'An array of users/groups allowed to push' do optional :access_level, type: Integer, values: ::ProtectedBranch::PushAccessLevel.allowed_access_levels + optional :deploy_key_id, type: Integer, documentation: { example: 1 } use :shared_params end diff --git a/ee/spec/factories/protected_branches/push_access_levels.rb b/ee/spec/factories/protected_branches/push_access_levels.rb index b960d37d115507..d42a2eeb18aa0c 100644 --- a/ee/spec/factories/protected_branches/push_access_levels.rb +++ b/ee/spec/factories/protected_branches/push_access_levels.rb @@ -4,5 +4,6 @@ factory :protected_branch_push_access_level, class: 'ProtectedBranch::PushAccessLevel' do user { nil } group { nil } + deploy_key { nil } end end diff --git a/ee/spec/requests/api/protected_branches_spec.rb b/ee/spec/requests/api/protected_branches_spec.rb index b593c73dbe90c8..4bf4fd9343e66e 100644 --- a/ee/spec/requests/api/protected_branches_spec.rb +++ b/ee/spec/requests/api/protected_branches_spec.rb @@ -189,6 +189,60 @@ end end + context 'with deploy key' do + let(:deploy_key) do + create(:deploy_key, deploy_keys_projects: [create(:deploy_keys_project, :write_access, project: project)]) + end + + it 'adds a deploy key for allowed to push option' do + patch api(route, user), params: { allowed_to_push: [{ deploy_key_id: deploy_key.id }] } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['push_access_levels']).to match_array( + [ + a_hash_including('id' => push_access_level.id, 'access_level' => push_access_level.access_level), + a_hash_including('access_level_description' => deploy_key.title, 'deploy_key_id' => deploy_key.id) + ] + ) + end + + it 'does not support a deploy key for other options' do + patch api(route, user), params: { allowed_to_merge: [{ deploy_key_id: deploy_key.id }] } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + + it 'updates an existing access level' do + patch api(route, user), params: { + allowed_to_push: [{ id: push_access_level.id, deploy_key_id: deploy_key.id }] + } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['push_access_levels']).to match_array( + a_hash_including( + 'id' => push_access_level.id, + 'access_level_description' => deploy_key.title, + 'deploy_key_id' => deploy_key.id + ) + ) + end + + context 'when deploy key is already set' do + let!(:deploy_key_access_level) do + create(:protected_branch_push_access_level, protected_branch: protected_branch, deploy_key: deploy_key) + end + + it 'deletes a deploy key' do + patch api(route, user), params: { allowed_to_push: [{ id: deploy_key_access_level.id, _destroy: true }] } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['push_access_levels']).to match_array( + a_hash_including('id' => push_access_level.id, 'access_level' => push_access_level.access_level) + ) + end + end + end + context "when the feature is enabled" do before do stub_licensed_features(code_owner_approval_required: true) @@ -341,6 +395,30 @@ def expect_protection_to_be_successful expect(response.body).to include('unprotect_access_level does not have a valid value') end + context 'with deploy key' do + let(:deploy_key) do + create(:deploy_key, deploy_keys_projects: [create(:deploy_keys_project, :write_access, project: project)]) + end + + it 'adds a deploy key for allowed to push option' do + post post_endpoint, params: { name: branch_name, allowed_to_push: [{ deploy_key_id: deploy_key.id }] } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['push_access_levels']).to match_array( + a_hash_including('access_level_description' => deploy_key.title, 'deploy_key_id' => deploy_key.id) + ) + end + + it 'ignores a deploy key for other options' do + post post_endpoint, params: { name: branch_name, allowed_to_merge: [{ deploy_key_id: deploy_key.id }] } + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['merge_access_levels']).to match_array( + a_hash_including('access_level' => Gitlab::Access::MAINTAINER) + ) + end + end + context "code_owner_approval_required" do context "when feature is enabled" do before do -- GitLab