summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/projects/git_http_controller.rb6
-rw-r--r--app/controllers/sessions_controller.rb5
-rw-r--r--app/models/user.rb2
-rw-r--r--app/models/user_activity.rb19
-rw-r--r--app/services/event_create_service.rb2
-rw-r--r--app/services/users/activity_service.rb26
-rw-r--r--db/migrate/20161007073613_create_user_activities.rb27
-rw-r--r--db/schema.rb8
-rw-r--r--lib/api/internal.rb12
-rw-r--r--spec/controllers/sessions_controller_spec.rb6
-rw-r--r--spec/factories/user_activities.rb6
-rw-r--r--spec/requests/api/internal_spec.rb13
-rw-r--r--spec/requests/git_http_spec.rb6
-rw-r--r--spec/services/event_create_service_spec.rb13
-rw-r--r--spec/services/users/activity_service_spec.rb26
15 files changed, 177 insertions, 0 deletions
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index 37f6f637ff0..10adddb4636 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -5,6 +5,8 @@ class Projects::GitHttpController < Projects::GitHttpClientController
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
def info_refs
if upload_pack? && upload_pack_allowed?
+ log_user_activity
+
render_ok
elsif receive_pack? && receive_pack_allowed?
render_ok
@@ -106,4 +108,8 @@ class Projects::GitHttpController < Projects::GitHttpClientController
def access_klass
@access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
end
+
+ def log_user_activity
+ Users::ActivityService.new(user, 'pull').execute
+ end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index d3091a4f8e9..8c6ba4915cd 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -35,6 +35,7 @@ class SessionsController < Devise::SessionsController
# hide the signed-in notification
flash[:notice] = nil
log_audit_event(current_user, with: authentication_method)
+ log_user_activity(current_user)
end
end
@@ -123,6 +124,10 @@ class SessionsController < Devise::SessionsController
for_authentication.security_event
end
+ def log_user_activity(user)
+ Users::ActivityService.new(user, 'login').execute
+ end
+
def load_recaptcha
Gitlab::Recaptcha.load_configurations!
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 457ba05fb04..1dde5c89699 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -101,6 +101,7 @@ class User < ActiveRecord::Base
has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue"
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
+ has_one :user_activity, dependent: :destroy
# Issues that a user owns are expected to be moved to the "ghost" user before
# the user is destroyed. If the user owns any issues during deletion, this
@@ -158,6 +159,7 @@ class User < ActiveRecord::Base
alias_attribute :private_token, :authentication_token
delegate :path, to: :namespace, allow_nil: true, prefix: true
+ delegate :last_activity_at, to: :user_activity, allow_nil: true
state_machine :state, initial: :active do
event :block do
diff --git a/app/models/user_activity.rb b/app/models/user_activity.rb
new file mode 100644
index 00000000000..c5fdbff0feb
--- /dev/null
+++ b/app/models/user_activity.rb
@@ -0,0 +1,19 @@
+class UserActivity < ActiveRecord::Base
+ belongs_to :user, inverse_of: :user_activity
+
+ validates :user, uniqueness: true, presence: true
+ validates :last_activity_at, presence: true
+
+ # Updated version of http://apidock.com/rails/ActiveRecord/Timestamp/touch
+ # That accepts a new record.
+ def touch
+ current_time = current_time_from_proper_timezone
+
+ if persisted?
+ update_column(:last_activity_at, current_time)
+ else
+ self.last_activity_at = current_time
+ save!(validate: false)
+ end
+ end
+end
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index e24cc66e0fe..0f3a485a3fd 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -72,6 +72,8 @@ class EventCreateService
def push(project, current_user, push_data)
create_event(project, current_user, Event::PUSHED, data: push_data)
+
+ Users::ActivityService.new(current_user, 'push').execute
end
private
diff --git a/app/services/users/activity_service.rb b/app/services/users/activity_service.rb
new file mode 100644
index 00000000000..b81f947cd01
--- /dev/null
+++ b/app/services/users/activity_service.rb
@@ -0,0 +1,26 @@
+module Users
+ class ActivityService
+ def initialize(author, activity)
+ @author = author.respond_to?(:user) ? author.user : author
+ @activity = activity
+ end
+
+ def execute
+ return unless @author && @author.is_a?(User)
+
+ record_activity
+ end
+
+ private
+
+ def record_activity
+ user_activity.touch
+
+ Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username}")
+ end
+
+ def user_activity
+ UserActivity.find_or_initialize_by(user: @author)
+ end
+ end
+end
diff --git a/db/migrate/20161007073613_create_user_activities.rb b/db/migrate/20161007073613_create_user_activities.rb
new file mode 100644
index 00000000000..4239cebd8f6
--- /dev/null
+++ b/db/migrate/20161007073613_create_user_activities.rb
@@ -0,0 +1,27 @@
+class CreateUserActivities < ActiveRecord::Migration
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = true
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ DOWNTIME_REASON = 'Adding foreign key'
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ create_table :user_activities do |t|
+ t.belongs_to :user, index: { unique: true }, foreign_key: { on_delete: :cascade }
+ t.datetime :last_activity_at, null: false
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 1c592dd5d6d..3ed28a02de2 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -1230,6 +1230,13 @@ ActiveRecord::Schema.define(version: 20170408033905) do
add_index "uploads", ["model_id", "model_type"], name: "index_uploads_on_model_id_and_model_type", using: :btree
add_index "uploads", ["path"], name: "index_uploads_on_path", using: :btree
+ create_table "user_activities", force: :cascade do |t|
+ t.integer "user_id"
+ t.datetime "last_activity_at", null: false
+ end
+
+ add_index "user_activities", ["user_id"], name: "index_user_activities_on_user_id", unique: true, using: :btree
+
create_table "user_agent_details", force: :cascade do |t|
t.string "user_agent", null: false
t.string "ip_address", null: false
@@ -1389,4 +1396,5 @@ ActiveRecord::Schema.define(version: 20170408033905) do
add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
+ add_foreign_key "user_activities", "users", on_delete: :cascade
end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 215bc03d0e9..d374d183dcd 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -15,6 +15,16 @@ module API
# project - project path with namespace
# action - git action (git-upload-pack or git-receive-pack)
# changes - changes as "oldrev newrev ref", see Gitlab::ChangesList
+ helpers do
+ def log_user_activity(actor)
+ commands = Gitlab::GitAccess::DOWNLOAD_COMMANDS +
+ Gitlab::GitAccess::PUSH_COMMANDS +
+ Gitlab::GitAccess::GIT_ANNEX_COMMANDS
+
+ ::Users::ActivityService.new(actor, 'Git SSH').execute if commands.include?(params[:action])
+ end
+ end
+
post "/allowed" do
status 200
@@ -40,6 +50,8 @@ module API
response = { status: access_status.status, message: access_status.message }
if access_status.status
+ log_user_activity(actor)
+
# Return the repository full path so that gitlab-shell has it when
# handling ssh commands
response[:repository_path] =
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 9c16a7bc08b..3459f30ef42 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -37,6 +37,12 @@ describe SessionsController do
subject.sign_out user
end
end
+
+ it 'updates the user activity' do
+ expect do
+ post(:create, user: { login: user.username, password: user.password })
+ end.to change { user.reload.last_activity_at }.from(nil)
+ end
end
end
diff --git a/spec/factories/user_activities.rb b/spec/factories/user_activities.rb
new file mode 100644
index 00000000000..32ad8c6a3b2
--- /dev/null
+++ b/spec/factories/user_activities.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :user_activity do
+ last_activity_at { Time.now }
+ user
+ end
+end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 4be67df5a00..63f566da7a8 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -151,6 +151,11 @@ describe API::Internal, api: true do
context "access granted" do
before do
project.team << [user, :developer]
+ Timecop.freeze
+ end
+
+ after do
+ Timecop.return
end
context 'with env passed as a JSON' do
@@ -176,6 +181,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
+ expect(key.user.reload.last_activity_at.to_i).to eq(Time.now.to_i)
end
end
@@ -186,6 +192,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
+ expect(key.user.reload.last_activity_at.to_i).to eq(Time.now.to_i)
end
end
@@ -196,6 +203,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ expect(key.user.reload.last_activity_at.to_i).to eq(Time.now.to_i)
end
end
@@ -206,6 +214,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ expect(key.user.reload.last_activity_at.to_i).to eq(Time.now.to_i)
end
context 'project as /namespace/project' do
@@ -241,6 +250,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
+ expect(key.user.reload.last_activity_at).to be_nil
end
end
@@ -250,6 +260,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
+ expect(key.user.reload.last_activity_at).to be_nil
end
end
end
@@ -267,6 +278,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
+ expect(key.user.reload.last_activity_at).to be_nil
end
end
@@ -276,6 +288,7 @@ describe API::Internal, api: true do
expect(response).to have_http_status(200)
expect(json_response["status"]).to be_falsey
+ expect(key.user.reload.last_activity_at).to be_nil
end
end
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 006d6a6af1c..9f2857ce2e7 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -157,6 +157,12 @@ describe 'Git HTTP requests', lib: true do
expect(response).to have_http_status(:ok)
end
end
+
+ it 'updates the user last activity' do
+ download(path, env) do |response|
+ expect(user.reload.last_activity_at).not_to be_nil
+ end
+ end
end
context 'but only project members are allowed' do
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index f2c2009bcbf..54e5c0b236b 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -129,4 +129,17 @@ describe EventCreateService, services: true do
it { expect { subject }.to change { Event.count }.from(0).to(1) }
end
end
+
+ describe '#push' do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ it 'creates a new event' do
+ expect { service.push(project, user, {}) }.to change { Event.count }
+ end
+
+ it 'updates user last activity' do
+ expect { service.push(project, user, {}) }.to change { user.last_activity_at }
+ end
+ end
end
diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb
new file mode 100644
index 00000000000..68399118579
--- /dev/null
+++ b/spec/services/users/activity_service_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Users::ActivityService, services: true do
+ let(:user) { create(:user) }
+ subject(:service) { described_class.new(user, 'type') }
+
+ describe '#execute' do
+ context 'when last activity is nil' do
+ it 'sets the last activity timestamp' do
+ service.execute
+
+ expect(user.last_activity_at).not_to be_nil
+ end
+ end
+
+ context 'when activity_at is not nil' do
+ it 'updates the activity multiple times' do
+ activity = create(:user_activity, user: user)
+
+ Timecop.travel(activity.last_activity_at + 1.minute) do
+ expect { service.execute }.to change { user.reload.last_activity_at }
+ end
+ end
+ end
+ end
+end