Commit a65f92f9 authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch 'add-audit-events-project-api' into 'master'

Add project audit events API

Closes #219238

See merge request gitlab-org/gitlab!33155
parents 37a14ea4 0a41a2b7
......@@ -97,6 +97,8 @@ From there, you can see the following actions:
- Permission to approve merge requests by authors was updated ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7531) in GitLab 12.9)
- Number of required approvals was updated ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7531) in GitLab 12.9)
Project events can also be accessed via the [Project Audit Events API](../api/audit_events.md#project-audit-events-starter)
### Instance events **(PREMIUM ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2336) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.3.
......
......@@ -225,3 +225,115 @@ Example response:
"created_at": "2019-08-28T19:36:44.162Z"
}
```
## Project Audit Events **(STARTER)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/219238) in GitLab 13.1.
The Project Audit Events API allows you to retrieve [project audit events](../administration/audit_events.md#project-events-starter).
To retrieve project audit events using the API, you must [authenticate yourself](README.md#authentication) as a Maintainer or an Owner of the project.
### Retrieve all project audit events
```plaintext
GET /projects/:id/audit_events
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `created_after` | string | no | Return project audit events created on or after the given time. Format: ISO 8601 YYYY-MM-DDTHH:MM:SSZ |
| `created_before` | string | no | Return project 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).
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/projects/7/audit_events
```
Example response:
```json
[
{
"id": 5,
"author_id": 1,
"entity_id": 7,
"entity_type": "Project",
"details": {
"change": "prevent merge request approval from reviewers",
"from": "",
"to": "true",
"author_name": "Administrator",
"target_id": 7,
"target_type": "Project",
"target_details": "twitter/typeahead-js",
"ip_address": "127.0.0.1",
"entity_path": "twitter/typeahead-js"
},
"created_at": "2020-05-26T22:55:04.230Z"
},
{
"id": 4,
"author_id": 1,
"entity_id": 7,
"entity_type": "Project",
"details": {
"change": "prevent merge request approval from authors",
"from": "false",
"to": "true",
"author_name": "Administrator",
"target_id": 7,
"target_type": "Project",
"target_details": "twitter/typeahead-js",
"ip_address": "127.0.0.1",
"entity_path": "twitter/typeahead-js"
},
"created_at": "2020-05-26T22:55:04.218Z"
}
]
```
### Retrieve a specific project audit event
Only available to project maintainers or owners.
```plaintext
GET /projects/:id/audit_events/:audit_event_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `audit_event_id` | integer | yes | The ID of the audit event |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/projects/7/audit_events/5
```
Example response:
```json
{
"id": 5,
"author_id": 1,
"entity_id": 7,
"entity_type": "Project",
"details": {
"change": "prevent merge request approval from reviewers",
"from": "",
"to": "true",
"author_name": "Administrator",
"target_id": 7,
"target_type": "Project",
"target_details": "twitter/typeahead-js",
"ip_address": "127.0.0.1",
"entity_path": "twitter/typeahead-js"
},
"created_at": "2020-05-26T22:55:04.230Z"
}
```
---
title: Add project audit events API
merge_request: 33155
author: Julien Millau
type: added
......@@ -21,6 +21,48 @@ module EE
render_api_error!(result[:message], 400)
end
end
segment ':id/audit_events' do
before do
authorize! :admin_project, user_project
check_audit_events_available!(user_project)
end
desc 'Get a list of audit events in this project.' 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
level = ::Gitlab::Audit::Levels::Project.new(project: user_project)
audit_events = AuditLogFinder.new(
level: level,
params: audit_log_finder_params
).execute
present paginate(audit_events), with: EE::API::Entities::AuditEvent
end
desc 'Get a specific audit event in this project.' do
success EE::API::Entities::AuditEvent
end
params do
requires :audit_event_id, type: Integer, desc: 'The ID of the audit event'
end
get '/:audit_event_id' do
level = ::Gitlab::Audit::Levels::Project.new(project: user_project)
# rubocop: disable CodeReuse/ActiveRecord
# This is not `find_by!` from ActiveRecord
audit_event = AuditLogFinder.new(level: level, params: audit_log_finder_params)
.find_by!(id: params[:audit_event_id])
# rubocop: enable CodeReuse/ActiveRecord
present audit_event, with: EE::API::Entities::AuditEvent
end
end
end
helpers do
......@@ -49,6 +91,14 @@ module EE
end
end
def check_audit_events_available!(project)
forbidden! unless project.feature_available?(:audit_events)
end
def audit_log_finder_params
params.slice(:created_after, :created_before)
end
override :delete_project
def delete_project(user_project)
return super unless user_project.adjourned_deletion?
......
......@@ -523,6 +523,178 @@ RSpec.describe API::Projects do
end
end
describe 'GET projects/:id/audit_events' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, namespace: user.namespace) }
let(:path) { "/projects/#{project.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 project 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(:project_audit_event_1) { create(:project_audit_event, created_at: Date.new(2000, 1, 10), entity_id: project.id) }
let_it_be(:project_audit_event_2) { create(:project_audit_event, created_at: Date.new(2000, 1, 15), entity_id: project.id) }
let_it_be(:project_audit_event_3) { create(:project_audit_event, created_at: Date.new(2000, 1, 20), entity_id: project.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(:ok)
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 project' do
project = create(:project)
audit_event = create(:project_audit_event, created_at: Date.new(2000, 1, 20), entity_id: project.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(project_audit_event_3.id)
expect(json_response.last["id"]).to eq(project_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(project_audit_event_3.id)
expect(json_response.last["id"]).to eq(project_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 projects/:id/audit_events/:audit_event_id' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, namespace: user.namespace) }
let(:path) { "/projects/#{project.id}/audit_events/#{project_audit_event.id}" }
let_it_be(:project_audit_event) { create(:project_audit_event, created_at: Date.new(2000, 1, 10), entity_id: project.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 project 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(:ok)
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 'invalid audit_event_id' do
let(:path) { "/projects/#{project.id}/audit_events/an-invalid-id" }
it_behaves_like '400 response' do
let(:request) { get api(path, user) }
end
end
context 'non existent audit event' do
context 'non existent audit event of a project' do
let(:path) { "/projects/#{project.id}/audit_events/666777" }
it_behaves_like '404 response' do
let(:request) { get api(path, user) }
end
end
context 'existing audit event of a different project' do
let(:new_project) { create(:project) }
let(:audit_event) { create(:project_audit_event, created_at: Date.new(2000, 1, 10), entity_id: new_project.id) }
let(:path) { "/projects/#{project.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
describe 'PUT /projects/:id' do
let(:project) { create(:project, namespace: user.namespace) }
......
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