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