Audit log for user authentication

This commit is contained in:
Valery Sizov 2015-07-03 14:54:50 +03:00
parent 8ba83cbab8
commit 411829fdb5
21 changed files with 144 additions and 33 deletions

View file

@ -28,7 +28,8 @@ v 7.13.0 (unreleased)
- Users with guest access level can not set assignee, labels or milestones for issue and merge request - Users with guest access level can not set assignee, labels or milestones for issue and merge request
- Reporter role can manage issue tracker now: edit any issue, set assignee or milestone and manage labels - Reporter role can manage issue tracker now: edit any issue, set assignee or milestone and manage labels
- Better performance for pages with events list, issues list and commits list - Better performance for pages with events list, issues list and commits list
- Faster automerge check and merge itself when source and target branches are in same repository - 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)

View file

@ -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
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
# 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
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

View file

@ -37,8 +37,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

View file

@ -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
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

19
app/models/audit_event.rb Normal file
View file

@ -0,0 +1,19 @@
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

View file

@ -0,0 +1,2 @@
class SecurityEvent < AuditEvent
end

View file

@ -0,0 +1,25 @@
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

View file

@ -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

View file

@ -0,0 +1,16 @@
%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"

View file

@ -0,0 +1,5 @@
- page_title "Audit Log"
%h3.page-title Audit Log
%p.light History of authentications
= render 'event_table', events: @events

View file

@ -1,11 +0,0 @@
- 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"

View file

@ -207,7 +207,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

View file

@ -0,0 +1,22 @@
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

View file

@ -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
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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
# ---------------------------------------- # ----------------------------------------

View file

@ -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 }

View file

@ -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