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:
- 2FA enforcement/grace period 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)**
NOTE: **Note:**
......@@ -107,6 +109,8 @@ the filter drop-down. You can further filter by specific group, project or user
![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
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).
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
......@@ -83,7 +85,7 @@ Example response:
]
```
## Retrieve single instance audit event
### Retrieve single instance audit event
```
GET /audit_events/:id
......@@ -113,3 +115,109 @@ Example response:
"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
]
```
## 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)**
Syncs the group with its linked LDAP group. Only available to group owners and administrators.
......
......@@ -12,6 +12,7 @@ class AuditLogFinder
audit_events = AuditEvent.order(id: :desc) # rubocop: disable CodeReuse/ActiveRecord
audit_events = by_entity(audit_events)
audit_events = by_created_at(audit_events)
audit_events = by_id(audit_events)
audit_events
end
......@@ -32,6 +33,12 @@ class AuditLogFinder
audit_events
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?
VALID_ENTITY_TYPES.include? params[:entity_type]
end
......
......@@ -45,6 +45,15 @@ module EE
super
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
resource :groups, requirements: ::API::API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
......@@ -61,6 +70,40 @@ module EE
status 202
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
......
......@@ -18,7 +18,7 @@ describe AuditLogFinder do
end
end
context 'filtering by ID' do
context 'filtering by entity_id' do
context 'no entity_type provided' do
let(:params) { { entity_id: 1 } }
......@@ -66,7 +66,7 @@ describe AuditLogFinder do
end
end
context 'filtering by type' do
context 'filtering by entity_type' do
let(:entity_types) { subject.map(&:entity_type) }
context 'User Event' do
......@@ -137,5 +137,23 @@ describe AuditLogFinder do
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
{
"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
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)
Sidekiq::Testing.send(sidekiq_testing_method) do
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