Commit 75b28a42 authored by Thong Kuah's avatar Thong Kuah

Merge branch '34078-extend-audit-events-api-for-gitlab-com' into 'master'

Resolve "Extend audit events API for GitLab.com"

See merge request gitlab-org/gitlab!19868
parents 3a2fe047 8eb6cb2f
---
title: Add Group Audit Events API
merge_request: 19868
author:
type: added
...@@ -59,6 +59,8 @@ From there, you can see the following actions: ...@@ -59,6 +59,8 @@ From there, you can see the following actions:
- 2FA enforcement/grace period changed - 2FA enforcement/grace period changed
- Roles allowed to create project changed - Roles allowed to create project changed
Group events can also be accessed via the [Group Audit Events API](../api/audit_events.md#group-audit-events-starter)
### Project events **(STARTER)** ### Project events **(STARTER)**
NOTE: **Note:** NOTE: **Note:**
...@@ -107,6 +109,8 @@ the filter drop-down. You can further filter by specific group, project or user ...@@ -107,6 +109,8 @@ the filter drop-down. You can further filter by specific group, project or user
![audit log](img/audit_log.png) ![audit log](img/audit_log.png)
Instance events can also be accessed via the [Instance Audit Events API](../api/audit_events.md#instance-audit-events-premium-only)
### Missing events ### Missing events
Some events are not being tracked in Audit Events. Please see the following Some events are not being tracked in Audit Events. Please see the following
......
# Audit Events API **(PREMIUM ONLY)** # Audit Events API
## Instance Audit Events **(PREMIUM ONLY)**
The Audit Events API allows you to retrieve [instance audit events](../administration/audit_events.md#instance-events-premium-only). The Audit Events API allows you to retrieve [instance audit events](../administration/audit_events.md#instance-events-premium-only).
To retrieve audit events using the API, you must [authenticate yourself](README.html#authentication) as an Administrator. To retrieve audit events using the API, you must [authenticate yourself](README.html#authentication) as an Administrator.
## Retrieve all instance audit events ### Retrieve all instance audit events
``` ```
GET /audit_events GET /audit_events
...@@ -83,7 +85,7 @@ Example response: ...@@ -83,7 +85,7 @@ Example response:
] ]
``` ```
## Retrieve single instance audit event ### Retrieve single instance audit event
``` ```
GET /audit_events/:id GET /audit_events/:id
...@@ -113,3 +115,109 @@ Example response: ...@@ -113,3 +115,109 @@ Example response:
"created_at": "2019-08-30T07:00:41.885Z" "created_at": "2019-08-30T07:00:41.885Z"
} }
``` ```
## Group Audit Events **(STARTER)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/34078) in GitLab 12.5.
The Group Audit Events API allows you to retrieve [group audit events](../administration/audit_events.html#group-events-starter).
To retrieve group audit events using the API, you must [authenticate yourself](README.html#authentication) as an Administrator or an owner of the group.
### Retrieve all group audit events
```
GET /groups/:id/audit_events
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `created_after` | string | no | Return group audit events created on or after the given time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ |
| `created_before` | string | no | Return group audit events created on or before the given time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ |
By default, `GET` requests return 20 results at a time because the API results
are paginated.
Read more on [pagination](README.md#pagination).
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/groups/60/audit_events
```
Example response:
```json
[
{
"id": 2,
"author_id": 1,
"entity_id": 60,
"entity_type": "Group",
"details": {
"custom_message": "Group marked for deletion",
"author_name": "Administrator",
"target_id": "flightjs",
"target_type": "Group",
"target_details": "flightjs",
"ip_address": "127.0.0.1",
"entity_path": "flightjs"
},
"created_at": "2019-08-28T19:36:44.162Z"
},
{
"id": 1,
"author_id": 1,
"entity_id": 60,
"entity_type": "Group",
"details": {
"add": "group",
"author_name": "Administrator",
"target_id": "flightjs",
"target_type": "Group",
"target_details": "flightjs",
"ip_address": "127.0.0.1",
"entity_path": "flightjs"
},
"created_at": "2019-08-27T18:36:44.162Z"
}
]
```
### Retrieve a specific group audit event
Only available to group owners and administrators.
```
GET /groups/:id/audit_events/:audit_event_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) |
| `audit_event_id` | integer | yes | ID of the audit event |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/groups/60/audit_events/2
```
Example response:
```json
{
"id": 2,
"author_id": 1,
"entity_id": 60,
"entity_type": "Group",
"details": {
"custom_message": "Group marked for deletion",
"author_name": "Administrator",
"target_id": "flightjs",
"target_type": "Group",
"target_details": "flightjs",
"ip_address": "127.0.0.1",
"entity_path": "flightjs"
},
"created_at": "2019-08-28T19:36:44.162Z"
}
```
...@@ -611,6 +611,10 @@ GET /groups?search=foobar ...@@ -611,6 +611,10 @@ GET /groups?search=foobar
] ]
``` ```
## Group Audit Events **(STARTER)**
Group audit events can be accessed via the [Group Audit Events API](audit_events.md#group-audit-events-starter)
## Sync group with LDAP **(CORE ONLY)** ## Sync group with LDAP **(CORE ONLY)**
Syncs the group with its linked LDAP group. Only available to group owners and administrators. Syncs the group with its linked LDAP group. Only available to group owners and administrators.
......
...@@ -12,6 +12,7 @@ class AuditLogFinder ...@@ -12,6 +12,7 @@ class AuditLogFinder
audit_events = AuditEvent.order(id: :desc) # rubocop: disable CodeReuse/ActiveRecord audit_events = AuditEvent.order(id: :desc) # rubocop: disable CodeReuse/ActiveRecord
audit_events = by_entity(audit_events) audit_events = by_entity(audit_events)
audit_events = by_created_at(audit_events) audit_events = by_created_at(audit_events)
audit_events = by_id(audit_events)
audit_events audit_events
end end
...@@ -32,6 +33,12 @@ class AuditLogFinder ...@@ -32,6 +33,12 @@ class AuditLogFinder
audit_events audit_events
end end
def by_id(audit_events)
return audit_events unless params[:id].present?
audit_events.find_by_id(params[:id])
end
def valid_entity_type? def valid_entity_type?
VALID_ENTITY_TYPES.include? params[:entity_type] VALID_ENTITY_TYPES.include? params[:entity_type]
end end
......
...@@ -45,6 +45,15 @@ module EE ...@@ -45,6 +45,15 @@ module EE
super super
end end
def check_audit_events_available!(group)
forbidden! unless group.feature_available?(:audit_events)
end
def audit_log_finder_params(group)
audit_log_finder_params = params.slice(:created_after, :created_before)
audit_log_finder_params.merge(entity_type: group.class.name, entity_id: group.id)
end
end end
resource :groups, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do resource :groups, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
...@@ -61,6 +70,40 @@ module EE ...@@ -61,6 +70,40 @@ module EE
status 202 status 202
end end
segment ':id/audit_events' do
before do
authorize! :admin_group, user_group
check_audit_events_available!(user_group)
end
desc 'Get a list of audit events in this group.' do
success EE::API::Entities::AuditEvent
end
params do
optional :created_after, type: DateTime, desc: 'Return audit events created after the specified time'
optional :created_before, type: DateTime, desc: 'Return audit events created before the specified time'
use :pagination
end
get '/' do
audit_events = AuditLogFinder.new(audit_log_finder_params(user_group)).execute
present paginate(audit_events), with: EE::API::Entities::AuditEvent
end
desc 'Get a specific audit event in this group.' do
success EE::API::Entities::AuditEvent
end
get '/:audit_event_id' do
audit_log_finder_params = audit_log_finder_params(user_group)
audit_event = AuditLogFinder.new(audit_log_finder_params.merge(id: params[:audit_event_id])).execute
not_found!('Audit Event') unless audit_event
present audit_event, with: EE::API::Entities::AuditEvent
end
end
end end
end end
end end
......
...@@ -18,7 +18,7 @@ describe AuditLogFinder do ...@@ -18,7 +18,7 @@ describe AuditLogFinder do
end end
end end
context 'filtering by ID' do context 'filtering by entity_id' do
context 'no entity_type provided' do context 'no entity_type provided' do
let(:params) { { entity_id: 1 } } let(:params) { { entity_id: 1 } }
...@@ -66,7 +66,7 @@ describe AuditLogFinder do ...@@ -66,7 +66,7 @@ describe AuditLogFinder do
end end
end end
context 'filtering by type' do context 'filtering by entity_type' do
let(:entity_types) { subject.map(&:entity_type) } let(:entity_types) { subject.map(&:entity_type) }
context 'User Event' do context 'User Event' do
...@@ -137,5 +137,23 @@ describe AuditLogFinder do ...@@ -137,5 +137,23 @@ describe AuditLogFinder do
end end
end end
end end
context 'filtering by id' do
context 'non-existent id provided' do
let(:params) { { id: 'non-existent-id' } }
it 'returns nil' do
expect(subject).to be_nil
end
end
context 'existent id provided' do
let(:params) { { id: user_audit_event.id } }
it 'returns the specific audit events with the id' do
expect(subject).to eql(user_audit_event)
end
end
end
end end
end end
{
"type": "object",
"required" : [
"id",
"created_at",
"author_id",
"entity_id",
"entity_type",
"details"
],
"properties" : {
"id": { "type": "integer" },
"created_at": { "type": ["string", "null"] },
"author_id": { "type": "integer" },
"entity_id": { "type": "integer" },
"entity_type": { "type": "string" },
"details": {
"type": "object",
"required": ["author_name", "entity_path", "target_details", "target_id", "target_type"],
"properties" : {
"author_name": { "type": "string" },
"entity_path": { "type": "string" },
"target_details": { "type": "string" },
"target_id": { "type": "integer" },
"target_type": { "type": "string" }
},
"additionalProperties": true
}
},
"additionalProperties": false
}
{
"type": "array",
"items": { "$ref": "audit_event.json" }
}
...@@ -300,6 +300,166 @@ describe API::Groups do ...@@ -300,6 +300,166 @@ describe API::Groups do
end end
end end
describe 'GET group/:id/audit_events' do
let(:path) { "/groups/#{group.id}/audit_events" }
context 'when authenticated, as a user' do
it_behaves_like '403 response' do
let(:request) { get api(path, create(:user)) }
end
end
context 'when authenticated, as a group owner' do
context 'audit events feature is not available' do
before do
stub_licensed_features(audit_events: false)
end
it_behaves_like '403 response' do
let(:request) { get api(path, user) }
end
end
context 'audit events feature is available' do
let_it_be(:group_audit_event_1) { create(:group_audit_event, created_at: Date.new(2000, 1, 10), entity_id: group.id) }
let_it_be(:group_audit_event_2) { create(:group_audit_event, created_at: Date.new(2000, 1, 15), entity_id: group.id) }
let_it_be(:group_audit_event_3) { create(:group_audit_event, created_at: Date.new(2000, 1, 20), entity_id: group.id) }
before do
stub_licensed_features(audit_events: true)
end
it 'returns 200 response' do
get api(path, user)
expect(response).to have_gitlab_http_status(200)
end
it 'includes the correct pagination headers' do
audit_events_counts = 3
get api(path, user)
expect(response).to include_pagination_headers
expect(response.headers['X-Total']).to eq(audit_events_counts.to_s)
expect(response.headers['X-Page']).to eq('1')
end
it 'does not include audit events of a different group' do
group = create(:group)
audit_event = create(:group_audit_event, created_at: Date.new(2000, 1, 20), entity_id: group.id)
get api(path, user)
audit_event_ids = json_response.map { |audit_event| audit_event['id'] }
expect(audit_event_ids).not_to include(audit_event.id)
end
context 'parameters' do
context 'created_before parameter' do
it "returns audit events created before the given parameter" do
created_before = '2000-01-20T00:00:00.060Z'
get api(path, user), params: { created_before: created_before }
expect(json_response.size).to eq 3
expect(json_response.first["id"]).to eq(group_audit_event_3.id)
expect(json_response.last["id"]).to eq(group_audit_event_1.id)
end
end
context 'created_after parameter' do
it "returns audit events created after the given parameter" do
created_after = '2000-01-12T00:00:00.060Z'
get api(path, user), params: { created_after: created_after }
expect(json_response.size).to eq 2
expect(json_response.first["id"]).to eq(group_audit_event_3.id)
expect(json_response.last["id"]).to eq(group_audit_event_2.id)
end
end
end
context 'response schema' do
it 'matches the response schema' do
get api(path, user)
expect(response).to match_response_schema('public_api/v4/audit_events', dir: 'ee')
end
end
end
end
end
describe 'GET group/:id/audit_events/:audit_event_id' do
let(:path) { "/groups/#{group.id}/audit_events/#{group_audit_event.id}" }
let_it_be(:group_audit_event) { create(:group_audit_event, created_at: Date.new(2000, 1, 10), entity_id: group.id) }
context 'when authenticated, as a user' do
it_behaves_like '403 response' do
let(:request) { get api(path, create(:user)) }
end
end
context 'when authenticated, as a group owner' do
context 'audit events feature is not available' do
before do
stub_licensed_features(audit_events: false)
end
it_behaves_like '403 response' do
let(:request) { get api(path, user) }
end
end
context 'audit events feature is available' do
before do
stub_licensed_features(audit_events: true)
end
context 'existent audit event' do
it 'returns 200 response' do
get api(path, user)
expect(response).to have_gitlab_http_status(200)
end
context 'response schema' do
it 'matches the response schema' do
get api(path, user)
expect(response).to match_response_schema('public_api/v4/audit_event', dir: 'ee')
end
end
context 'non existent audit event' do
context 'non existent audit event of a group' do
let(:path) { "/groups/#{group.id}/audit_events/non-existent-id" }
it_behaves_like '404 response' do
let(:request) { get api(path, user) }
end
end
context 'existing audit event of a different group' do
let(:new_group) { create(:group) }
let(:audit_event) { create(:group_audit_event, created_at: Date.new(2000, 1, 10), entity_id: new_group.id) }
let(:path) { "/groups/#{group.id}/audit_events/#{audit_event.id}" }
it_behaves_like '404 response' do
let(:request) { get api(path, user) }
end
end
end
end
end
end
end
def ldap_sync(group_id, user, sidekiq_testing_method) def ldap_sync(group_id, user, sidekiq_testing_method)
Sidekiq::Testing.send(sidekiq_testing_method) do Sidekiq::Testing.send(sidekiq_testing_method) do
post api("/groups/#{group_id}/ldap_sync", user) post api("/groups/#{group_id}/ldap_sync", user)
......
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