Commit 2f6b1b58 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'tc-namespace-license-checks--audit-events' into 'master'

Namespace license checks Audit Events & Admin Audit Log

Closes #2583

See merge request !2326
parents 4a5bc019 32db5174
class Admin::AuditLogsController < Admin::ApplicationController class Admin::AuditLogsController < Admin::ApplicationController
before_action :check_license_admin_audit_log_available!
def index def index
@events = LogFinder.new(audit_logs_params).execute @events = LogFinder.new(audit_logs_params).execute
@entity = case audit_logs_params[:event_type] @entity = case audit_logs_params[:event_type]
...@@ -16,4 +18,8 @@ class Admin::AuditLogsController < Admin::ApplicationController ...@@ -16,4 +18,8 @@ class Admin::AuditLogsController < Admin::ApplicationController
def audit_logs_params def audit_logs_params
params.permit(:page, :event_type, :user_id, :project_id, :group_id) params.permit(:page, :event_type, :user_id, :project_id, :group_id)
end end
def check_license_admin_audit_log_available!
render_404 unless License.feature_available?(:admin_audit_log)
end
end end
class Groups::AuditEventsController < Groups::ApplicationController class Groups::AuditEventsController < Groups::ApplicationController
before_action :authorize_admin_group! before_action :authorize_admin_group!
before_action :check_audit_events_available!
layout 'group_settings' layout 'group_settings'
......
class Projects::AuditEventsController < Projects::ApplicationController class Projects::AuditEventsController < Projects::ApplicationController
before_action :authorize_admin_project! before_action :authorize_admin_project!
before_action :check_audit_events_available!
layout 'project_settings' layout 'project_settings'
......
class License < ActiveRecord::Base class License < ActiveRecord::Base
include ActionView::Helpers::NumberHelper include ActionView::Helpers::NumberHelper
ADMIN_AUDIT_LOG_FEATURE = 'GitLab_AdminAuditLog'.freeze
AUDIT_EVENTS_FEATURE = 'GitLab_AuditEvents'.freeze
AUDITOR_USER_FEATURE = 'GitLab_Auditor_User'.freeze AUDITOR_USER_FEATURE = 'GitLab_Auditor_User'.freeze
BURNDOWN_CHARTS_FEATURE = 'GitLab_BurndownCharts'.freeze BURNDOWN_CHARTS_FEATURE = 'GitLab_BurndownCharts'.freeze
CONTRIBUTION_ANALYTICS_FEATURE = 'GitLab_ContributionAnalytics'.freeze CONTRIBUTION_ANALYTICS_FEATURE = 'GitLab_ContributionAnalytics'.freeze
...@@ -28,6 +30,7 @@ class License < ActiveRecord::Base ...@@ -28,6 +30,7 @@ class License < ActiveRecord::Base
VARIABLE_ENVIRONMENT_SCOPE_FEATURE = 'GitLab_VariableEnvironmentScope'.freeze VARIABLE_ENVIRONMENT_SCOPE_FEATURE = 'GitLab_VariableEnvironmentScope'.freeze
FEATURE_CODES = { FEATURE_CODES = {
admin_audit_log: ADMIN_AUDIT_LOG_FEATURE,
auditor_user: AUDITOR_USER_FEATURE, auditor_user: AUDITOR_USER_FEATURE,
elastic_search: ELASTIC_SEARCH_FEATURE, elastic_search: ELASTIC_SEARCH_FEATURE,
geo: GEO_FEATURE, geo: GEO_FEATURE,
...@@ -37,6 +40,7 @@ class License < ActiveRecord::Base ...@@ -37,6 +40,7 @@ class License < ActiveRecord::Base
variable_environment_scope: VARIABLE_ENVIRONMENT_SCOPE_FEATURE, variable_environment_scope: VARIABLE_ENVIRONMENT_SCOPE_FEATURE,
# Features that make sense to Namespace: # Features that make sense to Namespace:
audit_events: AUDIT_EVENTS_FEATURE,
burndown_charts: BURNDOWN_CHARTS_FEATURE, burndown_charts: BURNDOWN_CHARTS_FEATURE,
contribution_analytics: CONTRIBUTION_ANALYTICS_FEATURE, contribution_analytics: CONTRIBUTION_ANALYTICS_FEATURE,
deploy_board: DEPLOY_BOARD_FEATURE, deploy_board: DEPLOY_BOARD_FEATURE,
...@@ -63,6 +67,7 @@ class License < ActiveRecord::Base ...@@ -63,6 +67,7 @@ class License < ActiveRecord::Base
EARLY_ADOPTER_PLAN = 'early_adopter'.freeze EARLY_ADOPTER_PLAN = 'early_adopter'.freeze
EES_FEATURES = [ EES_FEATURES = [
{ AUDIT_EVENTS_FEATURE => 1 },
{ BURNDOWN_CHARTS_FEATURE => 1 }, { BURNDOWN_CHARTS_FEATURE => 1 },
{ CONTRIBUTION_ANALYTICS_FEATURE => 1 }, { CONTRIBUTION_ANALYTICS_FEATURE => 1 },
{ ELASTIC_SEARCH_FEATURE => 1 }, { ELASTIC_SEARCH_FEATURE => 1 },
...@@ -85,6 +90,7 @@ class License < ActiveRecord::Base ...@@ -85,6 +90,7 @@ class License < ActiveRecord::Base
EEP_FEATURES = [ EEP_FEATURES = [
*EES_FEATURES, *EES_FEATURES,
{ ADMIN_AUDIT_LOG_FEATURE => 1 },
{ AUDITOR_USER_FEATURE => 1 }, { AUDITOR_USER_FEATURE => 1 },
{ DEPLOY_BOARD_FEATURE => 1 }, { DEPLOY_BOARD_FEATURE => 1 },
{ FILE_LOCKS_FEATURE => 1 }, { FILE_LOCKS_FEATURE => 1 },
...@@ -106,6 +112,7 @@ class License < ActiveRecord::Base ...@@ -106,6 +112,7 @@ class License < ActiveRecord::Base
# Early adopters should not earn new features as they're # Early adopters should not earn new features as they're
# introduced. # introduced.
EARLY_ADOPTER_FEATURES = [ EARLY_ADOPTER_FEATURES = [
{ AUDIT_EVENTS_FEATURE => 1 },
{ AUDITOR_USER_FEATURE => 1 }, { AUDITOR_USER_FEATURE => 1 },
{ BURNDOWN_CHARTS_FEATURE => 1 }, { BURNDOWN_CHARTS_FEATURE => 1 },
{ CONTRIBUTION_ANALYTICS_FEATURE => 1 }, { CONTRIBUTION_ANALYTICS_FEATURE => 1 },
......
class AuditEventService class AuditEventService
prepend EE::AuditEventService
def initialize(author, entity, details = {}) def initialize(author, entity, details = {})
@author, @entity, @details = author, entity, details @author, @entity, @details = author, entity, details
end end
def for_member(member)
action = @details[:action]
old_access_level = @details[:old_access_level]
author_name = @author.name
user_id = member.id
user_name = member.user ? member.user.name : 'Deleted User'
@details =
case action
when :destroy
{
remove: "user_access",
author_name: author_name,
target_id: user_id,
target_type: "User",
target_details: user_name
}
when :create
{
add: "user_access",
as: Gitlab::Access.options_with_owner.key(member.access_level.to_i),
author_name: author_name,
target_id: user_id,
target_type: "User",
target_details: user_name
}
when :update, :override
{
change: "access_level",
from: old_access_level,
to: member.human_access,
author_name: author_name,
target_id: user_id,
target_type: "User",
target_details: user_name
}
end
self
end
def for_deploy_key(key_title)
action = @details[:action]
author_name = @author.name
@details =
case action
when :destroy
{
remove: "deploy_key",
author_name: author_name,
target_id: key_title,
target_type: "DeployKey",
target_details: key_title
}
when :create
{
add: "deploy_key",
author_name: author_name,
target_id: key_title,
target_type: "DeployKey",
target_details: key_title
}
end
self
end
def for_authentication def for_authentication
@details = { @details = {
with: @details[:with], with: @details[:with],
...@@ -87,8 +21,7 @@ class AuditEventService ...@@ -87,8 +21,7 @@ class AuditEventService
author_id: @author.id, author_id: @author.id,
entity_id: @entity.id, entity_id: @entity.id,
entity_type: @entity.class.name, entity_type: @entity.class.name,
details: @details.merge(ip_address: @author.current_sign_in_ip, details: @details
entity_path: @entity.full_path)
) )
end end
end end
module EE
module AuditEventService
def for_member(member)
action = @details[:action]
old_access_level = @details[:old_access_level]
author_name = @author.name
user_id = member.id
user_name = member.user ? member.user.name : 'Deleted User'
@details =
case action
when :destroy
{
remove: "user_access",
author_name: author_name,
target_id: user_id,
target_type: "User",
target_details: user_name
}
when :create
{
add: "user_access",
as: ::Gitlab::Access.options_with_owner.key(member.access_level.to_i),
author_name: author_name,
target_id: user_id,
target_type: "User",
target_details: user_name
}
when :update, :override
{
change: "access_level",
from: old_access_level,
to: member.human_access,
author_name: author_name,
target_id: user_id,
target_type: "User",
target_details: user_name
}
end
self
end
def for_deploy_key(key_title)
action = @details[:action]
author_name = @author.name
@details =
case action
when :destroy
{
remove: "deploy_key",
author_name: author_name,
target_id: key_title,
target_type: "DeployKey",
target_details: key_title
}
when :create
{
add: "deploy_key",
author_name: author_name,
target_id: key_title,
target_type: "DeployKey",
target_details: key_title
}
end
self
end
def security_event
if admin_audit_log_enabled?
add_security_event_admin_details!
return super
end
super if audit_events_enabled?
end
def add_security_event_admin_details!
@details.merge!(ip_address: @author.current_sign_in_ip,
entity_path: @entity.full_path)
end
def audit_events_enabled?
return true unless @entity.respond_to?(:feature_available?)
@entity.feature_available?(:audit_events)
end
def admin_audit_log_enabled?
License.feature_available?(:admin_audit_log)
end
end
end
...@@ -27,7 +27,4 @@ ...@@ -27,7 +27,4 @@
= link_to admin_requests_profiles_path, title: 'Requests Profiles' do = link_to admin_requests_profiles_path, title: 'Requests Profiles' do
%span %span
Requests Profiles Requests Profiles
= nav_link path: 'audit_logs#index' do = render 'admin/monitoring/ee/nav'
= link_to admin_audit_logs_path, title: 'Audit Log' do
%span
Audit Log
- if License.feature_available?(:admin_audit_log)
= nav_link path: 'audit_logs#index' do
= link_to admin_audit_logs_path, title: 'Audit Log' do
%span
Audit Log
...@@ -10,10 +10,11 @@ ...@@ -10,10 +10,11 @@
%span %span
Webhooks Webhooks
= nav_link(path: 'audit_events#index') do - if @group.feature_available?(:audit_events)
= link_to group_audit_events_path(@group), title: 'Audit Events' do = nav_link(path: 'audit_events#index') do
%span = link_to group_audit_events_path(@group), title: 'Audit Events' do
Audit Events %span
Audit Events
- if @group.shared_runners_enabled? && @group.shared_runners_minutes_limit_enabled? - if @group.shared_runners_enabled? && @group.shared_runners_minutes_limit_enabled?
= nav_link(path: 'pipeline_quota#index') do = nav_link(path: 'pipeline_quota#index') do
......
...@@ -29,7 +29,4 @@ ...@@ -29,7 +29,4 @@
%span %span
Pages Pages
= nav_link(controller: :audit_events) do = render 'projects/settings/ee/nav'
= link_to project_audit_events_path(@project), title: "Audit Events" do
%span
Audit Events
- if @project.feature_available?(:audit_events)
= nav_link(controller: :audit_events) do
= link_to project_audit_events_path(@project), title: "Audit Events" do
%span
Audit Events
---
title: Namespace license checks Audit Events & Admin Audit Log
merge_request: 2326
author:
...@@ -9,75 +9,99 @@ describe 'Admin::AuditLogs', feature: true, js: true do ...@@ -9,75 +9,99 @@ describe 'Admin::AuditLogs', feature: true, js: true do
sign_in(create(:admin)) sign_in(create(:admin))
end end
describe 'user events' do context 'unlicensed' do
before do before do
AuditEventService.new(user, user, with: :ldap) stub_licensed_features(admin_audit_log: false)
.for_authentication.security_event end
it 'returns 404' do
visit admin_audit_logs_path visit admin_audit_logs_path
expect(page.status_code).to eq(404)
end end
end
it 'filters by user' do context 'licensed' do
filter_by_type('User Events') before do
stub_licensed_features(admin_audit_log: true)
end
click_button 'User' it 'has Audit Log button in head nav bar' do
wait_for_requests visit admin_audit_logs_path
within '.dropdown-menu-user' do expect(page).to have_link('Audit Log', href: admin_audit_logs_path)
click_link user.name end
describe 'user events' do
before do
AuditEventService.new(user, user, with: :ldap)
.for_authentication.security_event
visit admin_audit_logs_path
end end
wait_for_requests it 'filters by user' do
filter_by_type('User Events')
expect(page).to have_content('Signed in with LDAP authentication') click_button 'User'
end wait_for_requests
end
describe 'group events' do within '.dropdown-menu-user' do
let(:group_member) { create(:group_member, user: user) } click_link user.name
end
before do wait_for_requests
AuditEventService.new(user, group_member.group, { action: :create })
.for_member(group_member).security_event
visit admin_audit_logs_path expect(page).to have_content('Signed in with LDAP authentication')
end
end end
it 'filters by group' do describe 'group events' do
filter_by_type('Group Events') let(:group_member) { create(:group_member, user: user) }
click_button 'Group' before do
find('.group-item-select').click AuditEventService.new(user, group_member.group, { action: :create })
wait_for_requests .for_member(group_member).security_event
find('.select2-results').click
find('#events-table td', match: :first) visit admin_audit_logs_path
end
expect(page).to have_content('Added user access as Owner') it 'filters by group' do
end filter_by_type('Group Events')
end
describe 'project events' do click_button 'Group'
let(:project_member) { create(:project_member, user: user) } find('.group-item-select').click
wait_for_requests
find('.select2-results').click
before do find('#events-table td', match: :first)
AuditEventService.new(user, project_member.project, { action: :destroy })
.for_member(project_member).security_event
visit admin_audit_logs_path expect(page).to have_content('Added user access as Owner')
end
end end
it 'filters by project' do describe 'project events' do
filter_by_type('Project Events') let(:project_member) { create(:project_member, user: user) }
before do
AuditEventService.new(user, project_member.project, { action: :destroy })
.for_member(project_member).security_event
visit admin_audit_logs_path
end
it 'filters by project' do
filter_by_type('Project Events')
click_button 'Project' click_button 'Project'
find('.project-item-select').click find('.project-item-select').click
wait_for_requests wait_for_requests
find('.select2-results').click find('.select2-results').click
find('#events-table td', match: :first) find('#events-table td', match: :first)
expect(page).to have_content('Removed user access') expect(page).to have_content('Removed user access')
end
end end
end end
......
...@@ -11,6 +11,30 @@ feature 'Groups > Audit Events', js: true, feature: true do ...@@ -11,6 +11,30 @@ feature 'Groups > Audit Events', js: true, feature: true do
sign_in(user) sign_in(user)
end end
context 'unlicensed' do
before do
stub_licensed_features(audit_events: false)
end
it 'returns 404' do
visit group_audit_events_path(group)
expect(page.status_code).to eq(404)
end
it 'does not have Audit Events button in head nav bar' do
visit edit_group_path(group)
expect(page).not_to have_link('Audit Events')
end
end
it 'has Audit Events button in head nav bar' do
visit edit_group_path(group)
expect(page).to have_link('Audit Events')
end
describe 'changing a user access level' do describe 'changing a user access level' do
it "appears in the group's audit events" do it "appears in the group's audit events" do
visit group_group_members_path(group) visit group_group_members_path(group)
......
...@@ -10,8 +10,34 @@ feature 'Projects > Audit Events', js: true, feature: true do ...@@ -10,8 +10,34 @@ feature 'Projects > Audit Events', js: true, feature: true do
sign_in(user) sign_in(user)
end end
context 'unlicensed' do
before do
stub_licensed_features(audit_events: false)
end
it 'returns 404' do
visit project_audit_events_path(project)
expect(page.status_code).to eq(404)
end
it 'does not have Audit Events button in head nav bar' do
visit edit_project_path(project)
expect(page).not_to have_link('Audit Events')
end
end
it 'has Audit Events button in head nav bar' do
visit edit_project_path(project)
expect(page).to have_link('Audit Events')
end
describe 'adding an SSH key' do describe 'adding an SSH key' do
it "appears in the project's audit events" do it "appears in the project's audit events" do
stub_licensed_features(audit_events: true)
visit new_project_deploy_key_path(project) visit new_project_deploy_key_path(project)
fill_in 'deploy_key_title', with: 'laptop' fill_in 'deploy_key_title', with: 'laptop'
......
...@@ -18,15 +18,86 @@ describe AuditEventService, services: true do ...@@ -18,15 +18,86 @@ describe AuditEventService, services: true do
event = service.for_member(project_member).security_event event = service.for_member(project_member).security_event
expect(event[:details][:target_details]).to eq('Deleted User') expect(event[:details][:target_details]).to eq('Deleted User')
end end
it 'has the IP address' do it 'has the IP address' do
event = service.for_member(project_member).security_event event = service.for_member(project_member).security_event
expect(event[:details][:ip_address]).to eq(user.current_sign_in_ip) expect(event[:details][:ip_address]).to eq(user.current_sign_in_ip)
end end
it 'has the entity full path' do context 'admin audit log licensed' do
event = service.for_member(project_member).security_event before do
expect(event[:details][:entity_path]).to eq(project.full_path) stub_licensed_features(admin_audit_log: true)
end
it 'has the entity full path' do
event = service.for_member(project_member).security_event
expect(event[:details][:entity_path]).to eq(project.full_path)
end
end
end
describe '#security_event' do
context 'unlicensed' do
before do
stub_licensed_features(audit_events: false)
end
it 'does not create an event' do
expect(SecurityEvent).not_to receive(:create)
service.security_event
end
end
context 'licensed' do
it 'creates an event' do
expect { service.security_event }.to change(SecurityEvent, :count).by(1)
end
end
end
describe '#audit_events_enabled?' do
context 'entity is a project' do
let(:service) { described_class.new(user, project, { action: :destroy }) }
it 'returns false when project is unlicensed' do
stub_licensed_features(audit_events: false)
expect(service.audit_events_enabled?).to be_falsy
end
it 'returns true when project is licensed' do
stub_licensed_features(audit_events: true)
expect(service.audit_events_enabled?).to be_truthy
end
end
context 'entity is a group' do
let(:group) { create(:group) }
let(:service) { described_class.new(user, group, { action: :destroy }) }
it 'returns false when group is unlicensed' do
stub_licensed_features(audit_events: false)
expect(service.audit_events_enabled?).to be_falsy
end
it 'returns true when group is licensed' do
stub_licensed_features(audit_events: true)
expect(service.audit_events_enabled?).to be_truthy
end
end
context 'entity is a user' do
let(:service) { described_class.new(user, user, { action: :destroy }) }
it 'returns true when unlicensed' do
stub_licensed_features(audit_events: false, admin_audit_log: false)
expect(service.audit_events_enabled?).to be_truthy
end
end end
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