Commit f3590c83 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'audit_log' into 'master'

Audit log for user authentication

https://dev.gitlab.org/gitlab/gitlabhq/issues/2318

See merge request !931
parents 5f52d6a0 411829fd
...@@ -40,6 +40,8 @@ v 7.13.0 (unreleased) ...@@ -40,6 +40,8 @@ v 7.13.0 (unreleased)
v 7.12.2 v 7.12.2
- Correctly show anonymous authorized applications under Profile > Applications. - Correctly show anonymous authorized applications under Profile > Applications.
- Faster automerge check and merge itself when source and target branches are in same repository
- Audit log for user authentication
v 7.12.1 v 7.12.1
- Fix error when deleting a user who has projects (Stan Hu) - Fix error when deleting a user who has projects (Stan Hu)
......
...@@ -28,6 +28,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -28,6 +28,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
# Do additional LDAP checks for the user filter and EE features # Do additional LDAP checks for the user filter and EE features
if @user.allowed? if @user.allowed?
log_audit_event(gl_user, with: :ldap)
sign_in_and_redirect(gl_user) sign_in_and_redirect(gl_user)
else else
flash[:alert] = "Access denied for your LDAP account." flash[:alert] = "Access denied for your LDAP account."
...@@ -47,6 +48,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -47,6 +48,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
if current_user if current_user
# Add new authentication method # Add new authentication method
current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider']) current_user.identities.find_or_create_by(extern_uid: oauth['uid'], provider: oauth['provider'])
log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated' redirect_to profile_account_path, notice: 'Authentication method updated'
else else
@user = Gitlab::OAuth::User.new(oauth) @user = Gitlab::OAuth::User.new(oauth)
...@@ -54,6 +56,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -54,6 +56,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
# Only allow properly saved users to login. # Only allow properly saved users to login.
if @user.persisted? && @user.valid? if @user.persisted? && @user.valid?
log_audit_event(@user.gl_user, with: oauth['provider'])
sign_in_and_redirect(@user.gl_user) sign_in_and_redirect(@user.gl_user)
else else
error_message = error_message =
...@@ -83,4 +86,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -83,4 +86,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def oauth def oauth
@oauth ||= request.env['omniauth.auth'] @oauth ||= request.env['omniauth.auth']
end end
def log_audit_event(user, options = {})
AuditEventService.new(user, user, options).
for_authentication.security_event
end
end end
...@@ -38,8 +38,11 @@ class ProfilesController < Profiles::ApplicationController ...@@ -38,8 +38,11 @@ class ProfilesController < Profiles::ApplicationController
redirect_to profile_account_path redirect_to profile_account_path
end end
def history def audit_log
@events = current_user.recent_events.page(params[:page]).per(PER_PAGE) @events = AuditEvent.where(entity_type: "User", entity_id: current_user.id).
order("created_at DESC").
page(params[:page]).
per(PER_PAGE)
end end
def update_username def update_username
......
...@@ -37,6 +37,8 @@ class SessionsController < Devise::SessionsController ...@@ -37,6 +37,8 @@ class SessionsController < Devise::SessionsController
resource.update_attributes(reset_password_token: nil, resource.update_attributes(reset_password_token: nil,
reset_password_sent_at: nil) reset_password_sent_at: nil)
end end
authenticated_with = user_params[:otp_attempt] ? "two-factor" : "standard"
log_audit_event(current_user, with: authenticated_with)
end end
end end
...@@ -95,4 +97,9 @@ class SessionsController < Devise::SessionsController ...@@ -95,4 +97,9 @@ class SessionsController < Devise::SessionsController
user.valid_otp?(user_params[:otp_attempt]) || user.valid_otp?(user_params[:otp_attempt]) ||
user.invalidate_otp_backup_code!(user_params[:otp_attempt]) user.invalidate_otp_backup_code!(user_params[:otp_attempt])
end end
def log_audit_event(user, options = {})
AuditEventService.new(user, user, options).
for_authentication.security_event
end
end end
class AuditEvent < ActiveRecord::Base
serialize :details, Hash
belongs_to :user, foreign_key: :author_id
validates :author_id, presence: true
validates :entity_id, presence: true
validates :entity_type, presence: true
after_initialize :initialize_details
def initialize_details
self.details = {} if details.nil?
end
def author_name
self.user.name
end
end
class SecurityEvent < AuditEvent
end
class AuditEventService
def initialize(author, entity, details = {})
@author, @entity, @details = author, entity, details
end
def for_authentication
@details = {
with: @details[:with],
target_id: @author.id,
target_type: "User",
target_details: @author.name,
}
self
end
def security_event
SecurityEvent.create(
author_id: @author.id,
entity_id: @entity.id,
entity_type: @entity.class.name,
details: @details
)
end
end
...@@ -44,8 +44,8 @@ ...@@ -44,8 +44,8 @@
= icon('image fw') = icon('image fw')
%span %span
Preferences Preferences
= nav_link(path: 'profiles#history') do = nav_link(path: 'profiles#audit_log') do
= link_to history_profile_path, title: 'History', data: {placement: 'right'} do = link_to audit_log_profile_path, title: 'Audit Log', data: {placement: 'right'} do
= icon('history fw') = icon('history fw')
%span %span
History Audit Log
%table.table#audits
%thead
%tr
%th Action
%th When
%tbody
- events.each do |event|
%tr
%td
%span
Signed in with
%b= event.details[:with]
authentication
%td #{time_ago_in_words event.created_at} ago
= paginate events, theme: "gitlab"
- page_title "Audit Log"
%h3.page-title Audit Log
%p.light History of authentications
= render 'event_table', events: @events
\ No newline at end of file
- page_title "History"
%h3.page-title
Your Account History
%p.light
All events created by your account are listed below.
%hr
.profile_history
= render @events
%hr
= paginate @events, theme: "gitlab"
...@@ -208,7 +208,7 @@ Gitlab::Application.routes.draw do ...@@ -208,7 +208,7 @@ Gitlab::Application.routes.draw do
# #
resource :profile, only: [:show, :update] do resource :profile, only: [:show, :update] do
member do member do
get :history get :audit_log
get :applications get :applications
put :reset_private_token put :reset_private_token
......
class AddAuditEvent < ActiveRecord::Migration
def change
create_table :audit_events do |t|
t.integer :author_id, null: false
t.string :type, null: false
# "Namespace" where the change occurs
# eg. On a project, group or user
t.integer :entity_id, null: false
t.string :entity_type, null: false
# Details for the event
t.text :details
t.timestamps
end
add_index :audit_events, :author_id
add_index :audit_events, :type
add_index :audit_events, [:entity_id, :entity_type]
end
end
...@@ -28,16 +28,30 @@ ActiveRecord::Schema.define(version: 20150620233230) do ...@@ -28,16 +28,30 @@ ActiveRecord::Schema.define(version: 20150620233230) do
t.integer "default_branch_protection", default: 2 t.integer "default_branch_protection", default: 2
t.boolean "twitter_sharing_enabled", default: true t.boolean "twitter_sharing_enabled", default: true
t.text "restricted_visibility_levels" t.text "restricted_visibility_levels"
t.boolean "version_check_enabled", default: true
t.integer "max_attachment_size", default: 10, null: false t.integer "max_attachment_size", default: 10, null: false
t.integer "default_project_visibility" t.integer "default_project_visibility"
t.integer "default_snippet_visibility" t.integer "default_snippet_visibility"
t.text "restricted_signup_domains" t.text "restricted_signup_domains"
t.boolean "version_check_enabled", default: true
t.boolean "user_oauth_applications", default: true t.boolean "user_oauth_applications", default: true
t.string "after_sign_out_path" t.string "after_sign_out_path"
t.integer "session_expire_delay", default: 10080, null: false t.integer "session_expire_delay", default: 10080, null: false
end end
create_table "audit_events", force: true do |t|
t.integer "author_id", null: false
t.string "type", null: false
t.integer "entity_id", null: false
t.string "entity_type", null: false
t.text "details"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "audit_events", ["author_id"], name: "index_audit_events_on_author_id", using: :btree
add_index "audit_events", ["entity_id", "entity_type"], name: "index_audit_events_on_entity_id_and_entity_type", using: :btree
add_index "audit_events", ["type"], name: "index_audit_events_on_type", using: :btree
create_table "broadcast_messages", force: true do |t| create_table "broadcast_messages", force: true do |t|
t.text "message", null: false t.text "message", null: false
t.datetime "starts_at" t.datetime "starts_at"
...@@ -496,12 +510,12 @@ ActiveRecord::Schema.define(version: 20150620233230) do ...@@ -496,12 +510,12 @@ ActiveRecord::Schema.define(version: 20150620233230) do
t.string "bitbucket_access_token" t.string "bitbucket_access_token"
t.string "bitbucket_access_token_secret" t.string "bitbucket_access_token_secret"
t.string "location" t.string "location"
t.string "public_email", default: "", null: false
t.string "encrypted_otp_secret" t.string "encrypted_otp_secret"
t.string "encrypted_otp_secret_iv" t.string "encrypted_otp_secret_iv"
t.string "encrypted_otp_secret_salt" t.string "encrypted_otp_secret_salt"
t.boolean "otp_required_for_login", default: false, null: false t.boolean "otp_required_for_login", default: false, null: false
t.text "otp_backup_codes" t.text "otp_backup_codes"
t.string "public_email", default: "", null: false
t.integer "dashboard", default: 0 t.integer "dashboard", default: 0
end end
......
...@@ -23,7 +23,7 @@ Feature: Profile Active Tab ...@@ -23,7 +23,7 @@ Feature: Profile Active Tab
Then the active main tab should be Preferences Then the active main tab should be Preferences
And no other main tabs should be active And no other main tabs should be active
Scenario: On Profile History Scenario: On Profile Audit Log
Given I visit profile history page Given I visit Audit Log page
Then the active main tab should be History Then the active main tab should be Audit Log
And no other main tabs should be active And no other main tabs should be active
...@@ -63,7 +63,7 @@ Feature: Profile ...@@ -63,7 +63,7 @@ Feature: Profile
Scenario: I visit history tab Scenario: I visit history tab
Given I have activity Given I have activity
When I visit profile history page When I visit Audit Log page
Then I should see my activity Then I should see my activity
Scenario: I visit my user page Scenario: I visit my user page
......
...@@ -19,7 +19,7 @@ class Spinach::Features::ProfileActiveTab < Spinach::FeatureSteps ...@@ -19,7 +19,7 @@ class Spinach::Features::ProfileActiveTab < Spinach::FeatureSteps
ensure_active_main_tab('Preferences') ensure_active_main_tab('Preferences')
end end
step 'the active main tab should be History' do step 'the active main tab should be Audit Log' do
ensure_active_main_tab('History') ensure_active_main_tab('Audit Log')
end end
end end
...@@ -115,7 +115,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps ...@@ -115,7 +115,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end end
step 'I should see my activity' do step 'I should see my activity' do
expect(page).to have_content "#{current_user.name} closed issue" expect(page).to have_content "Signed in with standard authentication"
end end
step 'my password is expired' do step 'my password is expired' do
......
...@@ -127,8 +127,8 @@ module SharedPaths ...@@ -127,8 +127,8 @@ module SharedPaths
visit profile_preferences_path visit profile_preferences_path
end end
step 'I visit profile history page' do step 'I visit Audit Log page' do
visit history_profile_path visit audit_log_profile_path
end end
# ---------------------------------------- # ----------------------------------------
......
...@@ -45,8 +45,8 @@ describe "Profile access", feature: true do ...@@ -45,8 +45,8 @@ describe "Profile access", feature: true do
it { is_expected.to be_denied_for :visitor } it { is_expected.to be_denied_for :visitor }
end end
describe "GET /profile/history" do describe "GET /profile/audit_log" do
subject { history_profile_path } subject { audit_log_profile_path }
it { is_expected.to be_allowed_for @u1 } it { is_expected.to be_allowed_for @u1 }
it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for :admin }
......
...@@ -108,8 +108,8 @@ describe ProfilesController, "routing" do ...@@ -108,8 +108,8 @@ describe ProfilesController, "routing" do
expect(get("/profile/account")).to route_to('profiles/accounts#show') expect(get("/profile/account")).to route_to('profiles/accounts#show')
end end
it "to #history" do it "to #audit_log" do
expect(get("/profile/history")).to route_to('profiles#history') expect(get("/profile/audit_log")).to route_to('profiles#audit_log')
end end
it "to #reset_private_token" do it "to #reset_private_token" do
......
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