Commit 5e1fdec0 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch 'group-webhooks-api' into 'master'

Group WebHooks API

Closes #2792

See merge request gitlab-org/gitlab!22994
parents 7a3b67c6 8d4ad3fa
...@@ -660,6 +660,118 @@ GET /groups?search=foobar ...@@ -660,6 +660,118 @@ GET /groups?search=foobar
] ]
``` ```
## Hooks
Also called Group Hooks and Webhooks.
These are different from [System Hooks](system_hooks.md) that are system wide and [Project Hooks](projects.md#hooks) that are limited to one project.
### List group hooks
Get a list of group hooks
```
GET /groups/:id/hooks
```
| Attribute | Type | Required | Description |
| --------- | --------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
### Get group hook
Get a specific hook for a group.
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `hook_id` | integer | yes | The ID of a group hook |
```
GET /groups/:id/hooks/:hook_id
```
```json
{
"id": 1,
"url": "http://example.com/hook",
"group_id": 3,
"push_events": true,
"issues_events": true,
"confidential_issues_events": true,
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
"job_events": true,
"pipeline_events": true,
"wiki_page_events": true,
"enable_ssl_verification": true,
"created_at": "2012-10-12T17:04:47Z"
}
```
### Add group hook
Adds a hook to a specified group.
```
POST /groups/:id/hooks
```
| Attribute | Type | Required | Description |
| -----------------------------| -------------- | ---------| ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `url` | string | yes | The hook URL |
| `push_events` | boolean | no | Trigger hook on push events |
| `issues_events` | boolean | no | Trigger hook on issues events |
| `confidential_issues_events` | boolean | no | Trigger hook on confidential issues events |
| `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
| `tag_push_events` | boolean | no | Trigger hook on tag push events |
| `note_events` | boolean | no | Trigger hook on note events |
| `job_events` | boolean | no | Trigger hook on job events |
| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
| `wiki_page_events` | boolean | no | Trigger hook on wiki events |
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response |
### Edit group hook
Edits a hook for a specified group.
```
PUT /groups/:id/hooks/:hook_id
```
| Attribute | Type | Required | Description |
| ---------------------------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `hook_id` | integer | yes | The ID of the group hook |
| `url` | string | yes | The hook URL |
| `push_events` | boolean | no | Trigger hook on push events |
| `issues_events` | boolean | no | Trigger hook on issues events |
| `confidential_issues_events` | boolean | no | Trigger hook on confidential issues events |
| `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
| `tag_push_events` | boolean | no | Trigger hook on tag push events |
| `note_events` | boolean | no | Trigger hook on note events |
| `job_events` | boolean | no | Trigger hook on job events |
| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
| `wiki_events` | boolean | no | Trigger hook on wiki events |
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response |
### Delete group hook
Removes a hook from a group. This is an idempotent method and can be called multiple times.
Either the hook is available or not.
```
DELETE /groups/:id/hooks/:hook_id
```
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `hook_id` | integer | yes | The ID of the group hook. |
## Group Audit Events **(STARTER)** ## Group Audit Events **(STARTER)**
Group audit events can be accessed via the [Group Audit Events API](audit_events.md#group-audit-events-starter) Group audit events can be accessed via the [Group Audit Events API](audit_events.md#group-audit-events-starter)
......
---
title: Add Group WebHooks API
merge_request: 22994
author: Rajendra Kadam
type: added
# frozen_string_literal: true
module API
class GroupHooks < Grape::API
include ::API::PaginationParams
before { authenticate! }
before { authorize! :admin_group, user_group }
helpers do
params :group_hook_properties do
requires :url, type: String, desc: "The URL to send the request to"
optional :push_events, type: Boolean, desc: "Trigger hook on push events"
optional :issues_events, type: Boolean, desc: "Trigger hook on issues events"
optional :confidential_issues_events, type: Boolean, desc: "Trigger hook on confidential issues events"
optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events"
optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
optional :confidential_note_events, type: Boolean, desc: "Trigger hook on confidential note(comment) events"
optional :job_events, type: Boolean, desc: "Trigger hook on job events"
optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events"
optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events"
optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response"
end
end
params do
requires :id, type: String, desc: 'The ID of a group'
end
resource :groups, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get group hooks' do
success EE::API::Entities::GroupHook
end
params do
use :pagination
end
get ":id/hooks" do
present paginate(user_group.hooks), with: EE::API::Entities::GroupHook
end
desc 'Get a group hook' do
success EE::API::Entities::GroupHook
end
params do
requires :hook_id, type: Integer, desc: 'The ID of a group hook'
end
get ":id/hooks/:hook_id" do
hook = user_group.hooks.find(params[:hook_id])
present hook, with: EE::API::Entities::GroupHook
end
desc 'Add hook to group' do
success EE::API::Entities::GroupHook
end
params do
use :group_hook_properties
end
post ":id/hooks" do
hook_params = declared_params(include_missing: false)
hook = user_group.hooks.new(hook_params)
if hook.save
present hook, with: EE::API::Entities::GroupHook
else
error!("Invalid url given", 422) if hook.errors[:url].present?
render_api_error!("Group hook #{hook.errors.messages}", 422)
end
end
desc 'Update an existing group hook' do
success EE::API::Entities::GroupHook
end
params do
requires :hook_id, type: Integer, desc: "The ID of the hook to update"
use :group_hook_properties
end
put ":id/hooks/:hook_id" do
hook = user_group.hooks.find(params.delete(:hook_id))
update_params = declared_params(include_missing: false)
if hook.update(update_params)
present hook, with: EE::API::Entities::GroupHook
else
error!("Invalid url given", 422) if hook.errors[:url].present?
render_api_error!("Group hook #{hook.errors.messages}", 422)
end
end
desc 'Deletes group hook' do
success EE::API::Entities::GroupHook
end
params do
requires :hook_id, type: Integer, desc: 'The ID of the hook to delete'
end
delete ":id/hooks/:hook_id" do
hook = user_group.hooks.find(params.delete(:hook_id))
destroy_conditionally!(hook)
end
end
end
end
...@@ -37,6 +37,7 @@ module EE ...@@ -37,6 +37,7 @@ module EE
mount ::API::NpmPackages mount ::API::NpmPackages
mount ::API::ProjectPackages mount ::API::ProjectPackages
mount ::API::GroupPackages mount ::API::GroupPackages
mount ::API::GroupHooks
mount ::API::PackageFiles mount ::API::PackageFiles
mount ::API::Scim mount ::API::Scim
mount ::API::ManagedLicenses mount ::API::ManagedLicenses
......
# frozen_string_literal: true
module EE
module API
module Entities
class GroupHook < ::API::Entities::Hook
expose :group_id, :issues_events, :confidential_issues_events,
:note_events, :confidential_note_events, :pipeline_events, :wiki_page_events,
:job_events
end
end
end
end
...@@ -3,5 +3,19 @@ ...@@ -3,5 +3,19 @@
FactoryBot.define do FactoryBot.define do
factory :group_hook do factory :group_hook do
url { generate(:url) } url { generate(:url) }
trait :all_events_enabled do
push_events { true }
merge_requests_events { true }
tag_push_events { true }
repository_update_events { true }
issues_events { true }
confidential_issues_events { true }
note_events { true }
confidential_note_events { true }
job_events { true }
pipeline_events { true }
wiki_page_events { true }
end
end end
end end
{
"type": "object",
"required": [
"id",
"url",
"created_at",
"push_events",
"tag_push_events",
"merge_requests_events",
"repository_update_events",
"enable_ssl_verification",
"group_id",
"issues_events",
"confidential_issues_events",
"note_events",
"confidential_note_events",
"pipeline_events",
"wiki_page_events",
"job_events"
],
"properties": {
"id": { "type": "integer" },
"group_id": { "type": "integer" },
"url": { "type": "string" },
"created_at": { "type": "date" },
"push_events": { "type": "boolean" },
"tag_push_events": { "type": "boolean" },
"merge_requests_events": { "type": "boolean" },
"repository_update_events": { "type": "boolean" },
"enable_ssl_verification": { "type": "boolean" },
"issues_events": { "type": "boolean" },
"confidential_issues_events": { "type": ["boolean", "null"] },
"note_events": { "type": "boolean" },
"confidential_note_events": { "type": ["boolean", "null"] },
"pipeline_events": { "type": "boolean" },
"wiki_page_events": { "type": "boolean" },
"job_events": { "type": "boolean" }
},
"additionalProperties": false
}
{
"type": "array",
"items": {
"type": "object",
"properties" : {
"$ref": "./group_hook.json"
}
}
}
# frozen_string_literal: true
require 'spec_helper'
describe API::GroupHooks do
let(:group_admin) { create(:user) }
let(:non_admin_user) { create(:user) }
let(:group) { create(:group) }
let(:hook_params) { { url: "http://example.com" } }
let!(:hook) do
create(:group_hook,
:all_events_enabled,
group: group,
url: 'http://example.com',
enable_ssl_verification: true)
end
def make_all_hooks_request(group_id, user)
get api("/groups/#{group_id}/hooks", user)
end
def make_single_hook_request(group_id, hook_id, user)
get api("/groups/#{group_id}/hooks/#{hook_id}", user)
end
def make_post_group_hook_request(group_id, user, params)
post api("/groups/#{group_id}/hooks", user), params: params
end
def make_put_group_hook_request(group_id, hook_id, user, params)
put api("/groups/#{group_id}/hooks/#{hook_id}", user), params: params
end
def make_delete_group_hook_request(group_id, hook_id, user)
delete api("/groups/#{group_id}/hooks/#{hook_id}", user)
end
before do
group.add_owner(group_admin)
end
describe "GET /groups/:id/hooks" do
context "authorized user" do
it "returns group hooks" do
make_all_hooks_request(group.id, group_admin)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/group_hooks', dir: 'ee')
end
it "returns 404 if group does not exist" do
make_all_hooks_request(1234, group_admin)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context "authenticated as non admin user" do
it "does not allow access to group hooks" do
make_all_hooks_request(group.id, non_admin_user)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context "unauthenticated user" do
it "does not access group hooks" do
make_all_hooks_request(group.id, nil)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe "GET /groups/:id/hooks/:hook_id" do
context "authorized user" do
it "returns a group hook" do
make_single_hook_request(group.id, hook.id, group_admin)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/group_hook', dir: 'ee')
end
it "returns 404 if hook id is invalid" do
make_single_hook_request(group.id, 1234, group_admin)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context "authenticated as non admin user" do
it "does not allow to read single hook" do
make_single_hook_request(group.id, hook.id, non_admin_user)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context "unauthenticated user" do
it "does not allow to read single hook" do
make_single_hook_request(group.id, hook.id, nil)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe "POST /groups/:id/hooks" do
context "authorized user" do
it "adds a new hook to group" do
expect do
make_post_group_hook_request(group.id, group_admin, hook_params)
end.to change { group.hooks.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('public_api/v4/group_hook', dir: 'ee')
end
it "returns 400 if url is not given" do
make_post_group_hook_request(group.id, group_admin, nil)
expect(response).to have_gitlab_http_status(:bad_request)
end
it "returns 422 if url is not valid" do
make_post_group_hook_request(group.id, group_admin, { url: "ftp://example.com" })
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['error']).to eq('Invalid url given')
end
it "returns 404 if group is not found" do
make_post_group_hook_request(1234, group_admin, hook_params)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context "authenticated as non admin user" do
it "returns forbidden to create a hook" do
make_post_group_hook_request(group.id, non_admin_user, hook_params)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context "unauthenticated user" do
it "does not allow to create a hook" do
make_post_group_hook_request(group.id, nil, nil)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe "PUT /groups/:id/hooks/:hook_id" do
context "authorized user" do
it "updates the hook" do
make_put_group_hook_request(group.id, hook.id, group_admin, hook_params.merge({ push_events: false }))
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/group_hook', dir: 'ee')
expect(json_response['push_events']).to eq(false)
end
it "returns 422 if url is not valid" do
make_put_group_hook_request(group.id, hook.id, group_admin, hook_params.merge({ url: "ftp://example.com" }))
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['error']).to eq('Invalid url given')
end
it "returns 400 if url is not given" do
make_put_group_hook_request(group.id, hook.id, group_admin, { push_events: false })
expect(response).to have_gitlab_http_status(:bad_request)
end
it "returns 404 if hook_id is not found" do
make_put_group_hook_request(group.id, 1234, group_admin, hook_params)
expect(response).to have_gitlab_http_status(:not_found)
end
it "returns 404 if group id is not found" do
make_put_group_hook_request(1234, hook.id, group_admin, hook_params)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context "authenticated as non admin user" do
it "returns forbidden to update a hook" do
make_put_group_hook_request(group.id, hook.id, non_admin_user, hook_params)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context "unauthorized user" do
it "does not allow to update a hook" do
make_put_group_hook_request(group.id, hook.id, nil, hook_params)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
describe "DELETE /groups/:id/hooks/:hook_id" do
context "authorized user" do
it "deletes the hook from the group" do
expect do
make_delete_group_hook_request(group.id, hook.id, group_admin)
end.to change { group.hooks.count }.by(-1)
expect(response).to have_gitlab_http_status(:no_content)
end
it "returns 404 when hook id is not given" do
make_delete_group_hook_request(group.id, nil, group_admin)
expect(response).to have_gitlab_http_status(:not_found)
end
it "returns 404 if hook id is invalid" do
make_delete_group_hook_request(group.id, 1234, group_admin)
expect(response).to have_gitlab_http_status(:not_found)
end
it "returns 404 if group id is invalid" do
make_delete_group_hook_request(1234, hook.id, group_admin)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context "authenticated as non-admin user" do
it "returns forbidden" do
make_delete_group_hook_request(group.id, hook.id, non_admin_user)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context "unauthorized user" do
it "returns unauthorized" do
make_delete_group_hook_request(group.id, hook.id, nil)
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment