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,7 +10,8 @@ ...@@ -10,7 +10,8 @@
%span %span
Webhooks Webhooks
= nav_link(path: 'audit_events#index') do - if @group.feature_available?(:audit_events)
= nav_link(path: 'audit_events#index') do
= link_to group_audit_events_path(@group), title: 'Audit Events' do = link_to group_audit_events_path(@group), title: 'Audit Events' do
%span %span
Audit Events Audit Events
......
...@@ -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,6 +9,29 @@ describe 'Admin::AuditLogs', feature: true, js: true do ...@@ -9,6 +9,29 @@ describe 'Admin::AuditLogs', feature: true, js: true do
sign_in(create(:admin)) sign_in(create(:admin))
end end
context 'unlicensed' do
before do
stub_licensed_features(admin_audit_log: false)
end
it 'returns 404' do
visit admin_audit_logs_path
expect(page.status_code).to eq(404)
end
end
context 'licensed' do
before do
stub_licensed_features(admin_audit_log: true)
end
it 'has Audit Log button in head nav bar' do
visit admin_audit_logs_path
expect(page).to have_link('Audit Log', href: admin_audit_logs_path)
end
describe 'user events' do describe 'user events' do
before do before do
AuditEventService.new(user, user, with: :ldap) AuditEventService.new(user, user, with: :ldap)
...@@ -80,6 +103,7 @@ describe 'Admin::AuditLogs', feature: true, js: true do ...@@ -80,6 +103,7 @@ describe 'Admin::AuditLogs', feature: true, js: true do
expect(page).to have_content('Removed user access') expect(page).to have_content('Removed user access')
end end
end end
end
def filter_by_type(type) def filter_by_type(type)
click_button 'Events' click_button 'Events'
......
...@@ -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'
......
...@@ -24,9 +24,80 @@ describe AuditEventService, services: true do ...@@ -24,9 +24,80 @@ describe AuditEventService, services: true do
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
context 'admin audit log licensed' do
before do
stub_licensed_features(admin_audit_log: true)
end
it 'has the entity full path' do it 'has the entity full path' do
event = service.for_member(project_member).security_event event = service.for_member(project_member).security_event
expect(event[:details][:entity_path]).to eq(project.full_path) expect(event[:details][:entity_path]).to eq(project.full_path)
end end
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
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