diff --git a/app/validators/json_schemas/member_role_permissions.json b/app/validators/json_schemas/member_role_permissions.json index 2257ec489342e4b4f6bb5a1d82cc08f20cd7acc5..8b75568ce8132ebe419d3674fe26f3cbf7bac648 100644 --- a/app/validators/json_schemas/member_role_permissions.json +++ b/app/validators/json_schemas/member_role_permissions.json @@ -7,6 +7,9 @@ "admin_cicd_variables": { "type": "boolean" }, + "admin_compliance_framework": { + "type": "boolean" + }, "admin_group_member": { "type": "boolean" }, @@ -16,16 +19,13 @@ "admin_push_rules": { "type": "boolean" }, - "manage_security_policy_link": { - "type": "boolean" - }, "admin_terraform_state": { "type": "boolean" }, - "admin_compliance_framework": { + "admin_vulnerability": { "type": "boolean" }, - "admin_vulnerability": { + "admin_web_hook": { "type": "boolean" }, "archive_project": { @@ -37,6 +37,9 @@ "manage_project_access_tokens": { "type": "boolean" }, + "manage_security_policy_link": { + "type": "boolean" + }, "read_code": { "type": "boolean" }, diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 5b1742ba8a389bcbb1cb6c291225988ffa5be5df..192b72a8c1d3731e94414e83d6c24207de6a06a4 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -33577,6 +33577,7 @@ Member role permission. | `ADMIN_PUSH_RULES` | Configure push rules for repositories at the group or project level. | | `ADMIN_TERRAFORM_STATE` | Execute terraform commands, lock/unlock terraform state files, and remove file versions. | | `ADMIN_VULNERABILITY` | Edit the vulnerability object, including the status and linking an issue. Includes the `read_vulnerability` permission actions. | +| `ADMIN_WEB_HOOK` | Manage webhooks. | | `ARCHIVE_PROJECT` | Allows archiving of projects. | | `MANAGE_GROUP_ACCESS_TOKENS` | Create, read, update, and delete group access tokens. When creating a token, users with this custom permission must select a role for that token that has the same or fewer permissions as the default role used as the base for the custom role. | | `MANAGE_PROJECT_ACCESS_TOKENS` | Create, read, update, and delete project access tokens. When creating a token, users with this custom permission must select a role for that token that has the same or fewer permissions as the default role used as the base for the custom role. | diff --git a/doc/user/custom_roles/abilities.md b/doc/user/custom_roles/abilities.md index 7a7379f03f6204037f3b1320e9bc4ec130db1653..c19192be819d785a9110af91642ddb6b676337f3 100644 --- a/doc/user/custom_roles/abilities.md +++ b/doc/user/custom_roles/abilities.md @@ -78,3 +78,9 @@ These requirements are documented in the `Required permission` column in the fol | [`admin_vulnerability`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/121534) | | Edit the vulnerability object, including the status and linking an issue. Includes the `read_vulnerability` permission actions. | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/412536) | | | | [`read_dependency`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126247) | | Allows read-only access to the dependencies and licenses. | GitLab [16.3](https://gitlab.com/gitlab-org/gitlab/-/issues/415255) | | | | [`read_vulnerability`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120704) | | Read vulnerability reports and security dashboards. | GitLab [16.1](https://gitlab.com/gitlab-org/gitlab/-/issues/399119) | | | + +## Webhooks + +| Name | Required permission | Description | Introduced in | Feature flag | Enabled in | +|:-----|:------------|:------------------|:---------|:--------------|:---------| +| [`admin_web_hook`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151551) | | Manage webhooks | GitLab [17.0](https://gitlab.com/gitlab-org/quality/triage-ops/-/issues/1373) | | | diff --git a/ee/app/policies/ee/group_policy.rb b/ee/app/policies/ee/group_policy.rb index 8372deeb5c1fcacad749c2bb72509295e1362e81..c7a855b4dbd086a6a1dc7bab5af5aa60c16a566e 100644 --- a/ee/app/policies/ee/group_policy.rb +++ b/ee/app/policies/ee/group_policy.rb @@ -811,6 +811,19 @@ module GroupPolicy enable :read_web_hook enable :admin_web_hook end + + condition(:role_enables_admin_web_hook) do + ::Auth::MemberRoleAbilityLoader.new( + user: @user, + resource: @subject, + ability: :admin_web_hook + ).has_ability? + end + + rule { custom_roles_allowed & role_enables_admin_web_hook }.policy do + enable :read_web_hook + enable :admin_web_hook + end end override :lookup_access_level! diff --git a/ee/app/policies/ee/project_policy.rb b/ee/app/policies/ee/project_policy.rb index 27b650c733429f63f22830279befaebeb5db5383..1e310a432a57940a6ca4163214f4476d7711f34d 100644 --- a/ee/app/policies/ee/project_policy.rb +++ b/ee/app/policies/ee/project_policy.rb @@ -1001,6 +1001,19 @@ module ProjectPolicy rule { container_scanning_for_registry_available & can?(:maintainer_access) }.policy do enable :enable_container_scanning_for_registry end + + condition(:role_enables_admin_web_hook) do + ::Auth::MemberRoleAbilityLoader.new( + user: @user, + resource: @subject, + ability: :admin_web_hook + ).has_ability? + end + + rule { custom_roles_allowed & role_enables_admin_web_hook }.policy do + enable :read_web_hook + enable :admin_web_hook + end end override :lookup_access_level! diff --git a/ee/config/custom_abilities/admin_web_hook.yml b/ee/config/custom_abilities/admin_web_hook.yml new file mode 100644 index 0000000000000000000000000000000000000000..7833651642bda282b74b8804fa6d8802efc96b9e --- /dev/null +++ b/ee/config/custom_abilities/admin_web_hook.yml @@ -0,0 +1,11 @@ +--- +name: admin_web_hook +description: Manage webhooks +introduced_by_issue: https://gitlab.com/gitlab-org/quality/triage-ops/-/issues/1373 +introduced_by_mr: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151551 +feature_category: webhooks +milestone: '17.0' +group_ability: true +project_ability: true +requirements: [] +available_from_access_level: diff --git a/ee/spec/factories/member_roles.rb b/ee/spec/factories/member_roles.rb index 6ffca3184d9176b3a5f73d0c69da1950db4debb0..8ddcaa8a788e27a7f3ce93003a6e867687a8ebc9 100644 --- a/ee/spec/factories/member_roles.rb +++ b/ee/spec/factories/member_roles.rb @@ -34,6 +34,10 @@ read_dependency { true } end + trait :admin_web_hook do + admin_web_hook { true } + end + # this trait can be used only for self-managed trait(:instance) { namespace { nil } } end diff --git a/ee/spec/policies/group_policy_spec.rb b/ee/spec/policies/group_policy_spec.rb index 9a07eee76a959be2af246516e419c66386f7f289..c2169ad5fee8928d77c887835fc2f8c3ad8c5885 100644 --- a/ee/spec/policies/group_policy_spec.rb +++ b/ee/spec/policies/group_policy_spec.rb @@ -3556,6 +3556,13 @@ def create_member_role(member, abilities = member_role_abilities) it_behaves_like 'custom roles abilities' end + + context 'for a member role with admin_web_hook true' do + let(:member_role_abilities) { { admin_web_hook: true } } + let(:allowed_abilities) { [:admin_web_hook, :read_web_hook] } + + it_behaves_like 'custom roles abilities' + end end context 'for :read_limit_alert' do diff --git a/ee/spec/policies/project_policy_spec.rb b/ee/spec/policies/project_policy_spec.rb index 87ed600d37bf6e267de2abe3ff692403d54a1978..aacdfd93626300be9c10e8d114f8f5566ccfbb11 100644 --- a/ee/spec/policies/project_policy_spec.rb +++ b/ee/spec/policies/project_policy_spec.rb @@ -2847,6 +2847,13 @@ def create_member_role(member, abilities = member_role_abilities) it_behaves_like 'custom roles abilities' end + + context 'for a member role with `admin_web_hook` true' do + let(:member_role_abilities) { { admin_web_hook: true } } + let(:allowed_abilities) { [:admin_web_hook, :read_web_hook] } + + it_behaves_like 'custom roles abilities' + end end describe 'permissions for suggested reviewers bot', :saas do diff --git a/ee/spec/requests/api/group_hooks_spec.rb b/ee/spec/requests/api/group_hooks_spec.rb index dd05a866f9a8e900fc4b996e0ace3c2bde215c90..501c56eccbc88e0a89e261ac8b87c3812490eda0 100644 --- a/ee/spec/requests/api/group_hooks_spec.rb +++ b/ee/spec/requests/api/group_hooks_spec.rb @@ -65,4 +65,26 @@ def event_names it_behaves_like 'web-hook API endpoints with branch-filter', '/projects/:id' end + + describe 'with admin_web_hook custom role' do + before do + stub_licensed_features(custom_roles: true) + sign_in(user) + end + + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:role) { create(:member_role, :guest, :admin_web_hook, namespace: group) } + let_it_be(:membership) { create(:group_member, :guest, member_role: role, user: user, group: group) } + let_it_be(:group_hook) { create(:group_hook, group: group, url: 'http://example.test/') } + + let(:hook) { create(:group_hook, group: group) } + let(:list_url) { "/groups/#{group.id}/hooks" } + let(:get_url) { "/groups/#{group.id}/hooks/#{group_hook.id}" } + let(:add_url) { "/groups/#{group.id}/hooks" } + let(:edit_url) { "/groups/#{group.id}/hooks/#{group_hook.id}" } + let(:delete_url) { "/groups/#{group.id}/hooks/#{hook.id}" } + + it_behaves_like 'web-hook API endpoints with admin_web_hook custom role' + end end diff --git a/ee/spec/requests/api/project_hooks_spec.rb b/ee/spec/requests/api/project_hooks_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..affe03525a4ac64f24ed4abeab1de1c5d6706366 --- /dev/null +++ b/ee/spec/requests/api/project_hooks_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe API::ProjectHooks, :aggregate_failures, feature_category: :webhooks do + describe 'with admin_web_hook custom role' do + before do + stub_licensed_features(custom_roles: true) + sign_in(user) + end + + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:role) { create(:member_role, :guest, :admin_web_hook, namespace: group) } + let_it_be(:membership) { create(:project_member, :guest, member_role: role, user: user, project: project) } + let_it_be(:project_hook) { create(:project_hook, project: project, url: 'http://example.test/') } + + let(:hook) { create(:project_hook, project: project) } + + let(:list_url) { "/projects/#{project.id}/hooks" } + let(:get_url) { "/projects/#{project.id}/hooks/#{project_hook.id}" } + let(:add_url) { "/projects/#{project.id}/hooks" } + let(:edit_url) { "/projects/#{project.id}/hooks/#{project_hook.id}" } + let(:delete_url) { "/projects/#{project.id}/hooks/#{hook.id}" } + + it_behaves_like 'web-hook API endpoints with admin_web_hook custom role' + end +end diff --git a/ee/spec/requests/custom_roles/admin_web_hook/request_spec.rb b/ee/spec/requests/custom_roles/admin_web_hook/request_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..f976eb438e8f53c6f093968d4edb059715fda0b4 --- /dev/null +++ b/ee/spec/requests/custom_roles/admin_web_hook/request_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'User with admin_web_hook custom role', feature_category: :webhooks do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:role) { create(:member_role, :guest, :admin_web_hook, namespace: group) } + + before do + stub_licensed_features(custom_roles: true) + sign_in(user) + end + + shared_examples 'HooksController' do + describe '#index' do + it 'allows access' do + get index_path + + expect(response).to have_gitlab_http_status(:ok) + end + end + + describe '#edit' do + it 'allows access' do + get edit_path + + expect(response).to have_gitlab_http_status(:ok) + end + end + + describe '#create' do + it 'allows access' do + post create_path, params: { hook: { url: 'http://example.test/' } } + + expect(response).to have_gitlab_http_status(:redirect) + end + end + + describe '#update' do + it 'allows access' do + patch update_path, params: { hook: { name: 'Test' } } + + expect(response).to have_gitlab_http_status(:redirect) + end + end + + describe '#destroy' do + it 'allows access' do + delete destroy_path + + expect(response).to have_gitlab_http_status(:redirect) + end + end + + describe '#test' do + it 'allows access' do + stub_request(:post, 'http://example.test/') + + post test_path + + expect(response).to have_gitlab_http_status(:redirect) + end + end + end + + describe Projects::HooksController do + let_it_be(:membership) { create(:project_member, :guest, member_role: role, user: user, project: project) } + let_it_be(:project_hook) { create(:project_hook, project: project, url: 'http://example.test/') } + + let(:hook) { create(:project_hook, project: project) } + + let(:index_path) { project_hooks_path(project) } + let(:edit_path) { edit_project_hook_path(project, project_hook) } + let(:create_path) { project_hooks_path(project) } + let(:update_path) { project_hook_path(project, project_hook) } + let(:destroy_path) { project_hook_path(project, hook) } + let(:test_path) { test_project_hook_path(project, project_hook) } + + it_behaves_like 'HooksController' + end + + describe Groups::HooksController do + let_it_be(:membership) { create(:group_member, :guest, member_role: role, user: user, group: group) } + let_it_be(:group_hook) { create(:group_hook, group: group, url: 'http://example.test/') } + + let(:hook) { create(:group_hook, group: group) } + + let(:index_path) { group_hooks_path(group) } + let(:edit_path) { edit_group_hook_path(group, group_hook) } + let(:create_path) { group_hooks_path(group) } + let(:update_path) { group_hook_path(group, group_hook) } + let(:destroy_path) { group_hook_path(group, hook) } + let(:test_path) { test_group_hook_path(group, group_hook) } + + it_behaves_like 'HooksController' + end +end diff --git a/ee/spec/support/shared_examples/lib/api/hooks_shared_examples.rb b/ee/spec/support/shared_examples/lib/api/hooks_shared_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..55a1a4cd35871fa54ac7cbc758356a509e50ca58 --- /dev/null +++ b/ee/spec/support/shared_examples/lib/api/hooks_shared_examples.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'web-hook API endpoints with admin_web_hook custom role' do + describe 'List hooks' do + it 'allows access' do + get api(list_url, user) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + describe 'Get hook' do + it 'allows access' do + get api(get_url, user) + + expect(response).to have_gitlab_http_status(:ok) + end + end + + describe 'Add hook' do + it 'allows access' do + post api(add_url, user), params: { url: 'http://example.test/' } + + expect(response).to have_gitlab_http_status(:created) + end + end + + describe 'Edit hook' do + it 'allows access' do + put api(edit_url, user), params: { url: 'http://example1.test' } + + expect(response).to have_gitlab_http_status(:ok) + end + end + + describe 'Delete hook' do + it 'allows access' do + delete api(delete_url, user) + + expect(response).to have_gitlab_http_status(:no_content) + end + end +end