From 9b33e3d36fcd46072b9fe83f1121fb0fd87c0fd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20Reigel=20=28=20=F0=9F=8C=B4=20may=202nd=20-=20may?= =?UTF-8?q?=209th=20=F0=9F=8C=B4=20=29?= <mail@koffeinfrei.org> Date: Wed, 2 May 2018 08:08:16 +0000 Subject: [PATCH] Display and revoke active sessions --- Gemfile | 3 + Gemfile.lock | 2 + app/assets/stylesheets/framework/common.scss | 1 + app/assets/stylesheets/framework/images.scss | 35 +-- .../profiles/active_sessions_controller.rb | 14 ++ app/helpers/active_sessions_helper.rb | 23 ++ app/models/active_session.rb | 110 +++++++++ .../layouts/nav/sidebar/_profile.html.haml | 11 + .../active_sessions/_active_session.html.haml | 31 +++ .../profiles/active_sessions/index.html.haml | 14 ++ .../feature-display-active-sessions.yml | 5 + config/initializers/session_store.rb | 26 +-- config/initializers/warden.rb | 12 + config/routes/profile.rb | 1 + doc/development/fe_guide/icons.md | 2 +- doc/user/profile/active_sessions.md | 20 ++ doc/user/profile/img/active_sessions_list.png | Bin 0 -> 41649 bytes doc/user/profile/index.md | 1 + lib/gitlab/redis/shared_state.rb | 2 + .../projects/clusters/gcp_controller_spec.rb | 2 +- .../features/profiles/active_sessions_spec.rb | 89 ++++++++ spec/features/users/active_sessions_spec.rb | 69 ++++++ spec/models/active_session_spec.rb | 216 ++++++++++++++++++ 23 files changed, 642 insertions(+), 47 deletions(-) create mode 100644 app/controllers/profiles/active_sessions_controller.rb create mode 100644 app/helpers/active_sessions_helper.rb create mode 100644 app/models/active_session.rb create mode 100644 app/views/profiles/active_sessions/_active_session.html.haml create mode 100644 app/views/profiles/active_sessions/index.html.haml create mode 100644 changelogs/unreleased/feature-display-active-sessions.yml create mode 100644 doc/user/profile/active_sessions.md create mode 100644 doc/user/profile/img/active_sessions_list.png create mode 100644 spec/features/profiles/active_sessions_spec.rb create mode 100644 spec/features/users/active_sessions_spec.rb create mode 100644 spec/models/active_session_spec.rb diff --git a/Gemfile b/Gemfile index caeaae9616453..a68b044b39e86 100644 --- a/Gemfile +++ b/Gemfile @@ -184,6 +184,9 @@ gem 're2', '~> 1.1.1' gem 'version_sorter', '~> 2.1.0' +# User agent parsing +gem 'device_detector' + # Cache gem 'redis-rails', '~> 5.0.2' diff --git a/Gemfile.lock b/Gemfile.lock index 9b2c47587ee13..f11df6a283e05 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -161,6 +161,7 @@ GEM activerecord (>= 3.2.0, < 5.1) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) + device_detector (1.0.0) devise (4.2.0) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -1026,6 +1027,7 @@ DEPENDENCIES database_cleaner (~> 1.5.0) deckar01-task_list (= 2.0.0) default_value_for (~> 3.0.0) + device_detector devise (~> 4.2) devise-two-factor (~> 3.0.0) diffy (~> 3.1.0) diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index e058a0b35b723..2faea55a5f5ac 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -452,6 +452,7 @@ img.emoji { /** COMMON CLASSES **/ .prepend-top-0 { margin-top: 0; } +.prepend-top-2 { margin-top: 2px; } .prepend-top-5 { margin-top: 5px; } .prepend-top-8 { margin-top: $grid-size; } .prepend-top-10 { margin-top: 10px; } diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index 62a0fba3da393..ab3cceceae924 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -39,35 +39,10 @@ svg { fill: currentColor; - &.s8 { - @include svg-size(8px); - } - - &.s12 { - @include svg-size(12px); - } - - &.s16 { - @include svg-size(16px); - } - - &.s18 { - @include svg-size(18px); - } - - &.s24 { - @include svg-size(24px); - } - - &.s32 { - @include svg-size(32px); - } - - &.s48 { - @include svg-size(48px); - } - - &.s72 { - @include svg-size(72px); + $svg-sizes: 8 12 16 18 24 32 48 72; + @each $svg-size in $svg-sizes { + &.s#{$svg-size} { + @include svg-size(#{$svg-size}px); + } } } diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/profiles/active_sessions_controller.rb new file mode 100644 index 0000000000000..f0cdc22836613 --- /dev/null +++ b/app/controllers/profiles/active_sessions_controller.rb @@ -0,0 +1,14 @@ +class Profiles::ActiveSessionsController < Profiles::ApplicationController + def index + @sessions = ActiveSession.list(current_user) + end + + def destroy + ActiveSession.destroy(current_user, params[:id]) + + respond_to do |format| + format.html { redirect_to profile_active_sessions_url, status: 302 } + format.js { head :ok } + end + end +end diff --git a/app/helpers/active_sessions_helper.rb b/app/helpers/active_sessions_helper.rb new file mode 100644 index 0000000000000..97b6dac67c504 --- /dev/null +++ b/app/helpers/active_sessions_helper.rb @@ -0,0 +1,23 @@ +module ActiveSessionsHelper + # Maps a device type as defined in `ActiveSession` to an svg icon name and + # outputs the icon html. + # + # see `DeviceDetector::Device::DEVICE_NAMES` about the available device types + def active_session_device_type_icon(active_session) + icon_name = + case active_session.device_type + when 'smartphone', 'feature phone', 'phablet' + 'mobile' + when 'tablet' + 'tablet' + when 'tv', 'smart display', 'camera', 'portable media player', 'console' + 'media' + when 'car browser' + 'car' + else + 'monitor-o' + end + + sprite_icon(icon_name, size: 16, css_class: 'prepend-top-2') + end +end diff --git a/app/models/active_session.rb b/app/models/active_session.rb new file mode 100644 index 0000000000000..b4a86dbb3315d --- /dev/null +++ b/app/models/active_session.rb @@ -0,0 +1,110 @@ +class ActiveSession + include ActiveModel::Model + + attr_accessor :created_at, :updated_at, + :session_id, :ip_address, + :browser, :os, :device_name, :device_type + + def current?(session) + return false if session_id.nil? || session.id.nil? + + session_id == session.id + end + + def human_device_type + device_type&.titleize + end + + def self.set(user, request) + Gitlab::Redis::SharedState.with do |redis| + session_id = request.session.id + client = DeviceDetector.new(request.user_agent) + timestamp = Time.current + + active_user_session = new( + ip_address: request.ip, + browser: client.name, + os: client.os_name, + device_name: client.device_name, + device_type: client.device_type, + created_at: user.current_sign_in_at || timestamp, + updated_at: timestamp, + session_id: session_id + ) + + redis.pipelined do + redis.setex( + key_name(user.id, session_id), + Settings.gitlab['session_expire_delay'] * 60, + Marshal.dump(active_user_session) + ) + + redis.sadd( + lookup_key_name(user.id), + session_id + ) + end + end + end + + def self.list(user) + Gitlab::Redis::SharedState.with do |redis| + cleaned_up_lookup_entries(redis, user.id).map do |entry| + # rubocop:disable Security/MarshalLoad + Marshal.load(entry) + # rubocop:enable Security/MarshalLoad + end + end + end + + def self.destroy(user, session_id) + Gitlab::Redis::SharedState.with do |redis| + redis.srem(lookup_key_name(user.id), session_id) + + deleted_keys = redis.del(key_name(user.id, session_id)) + + # only allow deleting the devise session if we could actually find a + # related active session. this prevents another user from deleting + # someone else's session. + if deleted_keys > 0 + redis.del("#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}") + end + end + end + + def self.cleanup(user) + Gitlab::Redis::SharedState.with do |redis| + cleaned_up_lookup_entries(redis, user.id) + end + end + + def self.key_name(user_id, session_id = '*') + "#{Gitlab::Redis::SharedState::USER_SESSIONS_NAMESPACE}:#{user_id}:#{session_id}" + end + + def self.lookup_key_name(user_id) + "#{Gitlab::Redis::SharedState::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user_id}" + end + + def self.cleaned_up_lookup_entries(redis, user_id) + lookup_key = lookup_key_name(user_id) + + session_ids = redis.smembers(lookup_key) + + entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) } + return [] if entry_keys.empty? + + entries = redis.mget(entry_keys) + + session_ids_and_entries = session_ids.zip(entries) + + # remove expired keys. + # only the single key entries are automatically expired by redis, the + # lookup entries in the set need to be removed manually. + session_ids_and_entries.reject { |_session_id, entry| entry }.each do |session_id, _entry| + redis.srem(lookup_key, session_id) + end + + session_ids_and_entries.select { |_session_id, entry| entry }.map { |_session_id, entry| entry } + end +end diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml index c878fcf28089b..6cbd163dd41b9 100644 --- a/app/views/layouts/nav/sidebar/_profile.html.haml +++ b/app/views/layouts/nav/sidebar/_profile.html.haml @@ -129,6 +129,17 @@ = link_to profile_preferences_path do %strong.fly-out-top-item-name #{ _('Preferences') } + = nav_link(controller: :active_sessions) do + = link_to profile_active_sessions_path do + .nav-icon-container + = sprite_icon('monitor-lines') + %span.nav-item-name + Active Sessions + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :active_sessions, html_options: { class: "fly-out-top-item" } ) do + = link_to profile_active_sessions_path do + %strong.fly-out-top-item-name + #{ _('Active Sessions') } = nav_link(path: 'profiles#audit_log') do = link_to audit_log_profile_path do .nav-icon-container diff --git a/app/views/profiles/active_sessions/_active_session.html.haml b/app/views/profiles/active_sessions/_active_session.html.haml new file mode 100644 index 0000000000000..d40b771f48bd9 --- /dev/null +++ b/app/views/profiles/active_sessions/_active_session.html.haml @@ -0,0 +1,31 @@ +- is_current_session = active_session.current?(session) + +%li + .pull-left.append-right-10{ data: { toggle: 'tooltip' }, title: active_session.human_device_type } + = active_session_device_type_icon(active_session) + + .description.pull-left + %div + %strong= active_session.ip_address + - if is_current_session + %div This is your current session + - else + %div + Last accessed on + = l(active_session.updated_at, format: :short) + + %div + %strong= active_session.browser + on + %strong= active_session.os + + %div + %strong Signed in + on + = l(active_session.created_at, format: :short) + + - unless is_current_session + .pull-right + = link_to profile_active_session_path(active_session.session_id), data: { confirm: 'Are you sure? The device will be signed out of GitLab.' }, method: :delete, class: "btn btn-danger prepend-left-10" do + %span.sr-only Revoke + Revoke diff --git a/app/views/profiles/active_sessions/index.html.haml b/app/views/profiles/active_sessions/index.html.haml new file mode 100644 index 0000000000000..d0250bb4eab31 --- /dev/null +++ b/app/views/profiles/active_sessions/index.html.haml @@ -0,0 +1,14 @@ +- page_title 'Active Sessions' +- @content_class = "limit-container-width" unless fluid_layout + +.row.prepend-top-default + .col-lg-4.profile-settings-sidebar + %h4.prepend-top-0 + = page_title + %p + This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize. + .col-lg-8 + .append-bottom-default + + %ul.well-list + = render partial: 'profiles/active_sessions/active_session', collection: @sessions diff --git a/changelogs/unreleased/feature-display-active-sessions.yml b/changelogs/unreleased/feature-display-active-sessions.yml new file mode 100644 index 0000000000000..14cfa66953eb4 --- /dev/null +++ b/changelogs/unreleased/feature-display-active-sessions.yml @@ -0,0 +1,5 @@ +--- +title: Display active sessions and allow the user to revoke any of it +merge_request: 17867 +author: Alexis Reigel +type: added diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index f2fde1e0048f0..da24881885ec2 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -15,19 +15,15 @@ "_gitlab_session" end -if Rails.env.test? - Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session" -else - sessions_config = Gitlab::Redis::SharedState.params - sessions_config[:namespace] = Gitlab::Redis::SharedState::SESSION_NAMESPACE +sessions_config = Gitlab::Redis::SharedState.params +sessions_config[:namespace] = Gitlab::Redis::SharedState::SESSION_NAMESPACE - Gitlab::Application.config.session_store( - :redis_store, # Using the cookie_store would enable session replay attacks. - servers: sessions_config, - key: cookie_key, - secure: Gitlab.config.gitlab.https, - httponly: true, - expires_in: Settings.gitlab['session_expire_delay'] * 60, - path: Rails.application.config.relative_url_root.nil? ? '/' : Gitlab::Application.config.relative_url_root - ) -end +Gitlab::Application.config.session_store( + :redis_store, # Using the cookie_store would enable session replay attacks. + servers: sessions_config, + key: cookie_key, + secure: Gitlab.config.gitlab.https, + httponly: true, + expires_in: Settings.gitlab['session_expire_delay'] * 60, + path: Rails.application.config.relative_url_root.nil? ? '/' : Gitlab::Application.config.relative_url_root +) diff --git a/config/initializers/warden.rb b/config/initializers/warden.rb index ee034d21eaedd..bf079f8e1a7e2 100644 --- a/config/initializers/warden.rb +++ b/config/initializers/warden.rb @@ -6,4 +6,16 @@ Warden::Manager.before_failure do |env, opts| Gitlab::Auth::BlockedUserTracker.log_if_user_blocked(env) end + + Warden::Manager.after_authentication do |user, auth, opts| + ActiveSession.cleanup(user) + end + + Warden::Manager.after_set_user only: :fetch do |user, auth, opts| + ActiveSession.set(user, auth.request) + end + + Warden::Manager.before_logout do |user, auth, opts| + ActiveSession.destroy(user || auth.user, auth.request.session.id) + end end diff --git a/config/routes/profile.rb b/config/routes/profile.rb index bcfc17a5f667c..a9ba5ac2c0b69 100644 --- a/config/routes/profile.rb +++ b/config/routes/profile.rb @@ -30,6 +30,7 @@ put :revoke end end + resources :active_sessions, only: [:index, :destroy] resources :emails, only: [:index, :create, :destroy] do member do put :resend_confirmation_instructions diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md index b288ee9572254..b469a9c6aef4a 100644 --- a/doc/development/fe_guide/icons.md +++ b/doc/development/fe_guide/icons.md @@ -49,7 +49,7 @@ Please use the following function inside JS to render an icon : All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency. -To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs` and then run `yarn run svg`. This task will copy the svg sprite and all illustrations in the correct folders. The updated files should be tracked in Git as those are referenced. +To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs`. # SVG Illustrations diff --git a/doc/user/profile/active_sessions.md b/doc/user/profile/active_sessions.md new file mode 100644 index 0000000000000..5119c0e30d056 --- /dev/null +++ b/doc/user/profile/active_sessions.md @@ -0,0 +1,20 @@ +# Active Sessions + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17867) +> in GitLab 10.8. + +GitLab lists all devices that have logged into your account. This allows you to +review the sessions and revoke any of it that you don't recognize. + +## Listing all active sessions + +1. On the upper right corner, click on your avatar and go to your **Settings**. +1. Navigate to the **Active Sessions** tab. + + + +## Revoking a session + +1. Navigate to your [profile's](#profile-settings) **Settings > Active Sessions**. +1. Click on **Revoke** besides a session. The current session cannot be + revoked, as this would sign you out of GitLab. diff --git a/doc/user/profile/img/active_sessions_list.png b/doc/user/profile/img/active_sessions_list.png new file mode 100644 index 0000000000000000000000000000000000000000..76a52220bcd12ddbee6339961497015b1647b633 GIT binary patch literal 41649 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 literal 0 HcmV?d00001 diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md index ab16f8d14c125..91cdef8d1dd79 100644 --- a/doc/user/profile/index.md +++ b/doc/user/profile/index.md @@ -39,6 +39,7 @@ From there, you can: - Manage [SSH keys](../../ssh/README.md#ssh) to access your account via SSH - Manage your [preferences](preferences.md#syntax-highlighting-theme) to customize your own GitLab experience +- [View your active sessions](active_sessions.md) and revoke any of them if necessary - Access your audit log, a security log of important events involving your account ## Changing your username diff --git a/lib/gitlab/redis/shared_state.rb b/lib/gitlab/redis/shared_state.rb index 10bec7a90dafe..e5a0fdae7ef4d 100644 --- a/lib/gitlab/redis/shared_state.rb +++ b/lib/gitlab/redis/shared_state.rb @@ -5,6 +5,8 @@ module Gitlab module Redis class SharedState < ::Gitlab::Redis::Wrapper SESSION_NAMESPACE = 'session:gitlab'.freeze + USER_SESSIONS_NAMESPACE = 'session:user:gitlab'.freeze + USER_SESSIONS_LOOKUP_NAMESPACE = 'session:lookup:user:gitlab'.freeze DEFAULT_REDIS_SHARED_STATE_URL = 'redis://localhost:6382'.freeze REDIS_SHARED_STATE_CONFIG_ENV_VAR_NAME = 'GITLAB_REDIS_SHARED_STATE_CONFIG_FILE'.freeze diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb index e14ba29fa70ab..715bb9f5e5218 100644 --- a/spec/controllers/projects/clusters/gcp_controller_spec.rb +++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb @@ -142,7 +142,7 @@ def go context 'when google project billing is enabled' do before do - redis_double = double + redis_double = double.as_null_object allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double) allow(redis_double).to receive(:get).with(CheckGcpProjectBillingWorker.redis_shared_state_key_for('token')).and_return('true') end diff --git a/spec/features/profiles/active_sessions_spec.rb b/spec/features/profiles/active_sessions_spec.rb new file mode 100644 index 0000000000000..4045cfd21c452 --- /dev/null +++ b/spec/features/profiles/active_sessions_spec.rb @@ -0,0 +1,89 @@ +require 'rails_helper' + +feature 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do + let(:user) do + create(:user).tap do |user| + user.current_sign_in_at = Time.current + end + end + + around do |example| + Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do + example.run + end + end + + scenario 'User sees their active sessions' do + Capybara::Session.new(:session1) + Capybara::Session.new(:session2) + + # note: headers can only be set on the non-js (aka. rack-test) driver + using_session :session1 do + Capybara.page.driver.header( + 'User-Agent', + 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:58.0) Gecko/20100101 Firefox/58.0' + ) + + gitlab_sign_in(user) + end + + # set an additional session on another device + using_session :session2 do + Capybara.page.driver.header( + 'User-Agent', + 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Mobile/12B466 [FBDV/iPhone7,2]' + ) + + gitlab_sign_in(user) + end + + using_session :session1 do + visit profile_active_sessions_path + + expect(page).to have_content( + '127.0.0.1 ' \ + 'This is your current session ' \ + 'Firefox on Ubuntu ' \ + 'Signed in on 12 Mar 09:06' + ) + + expect(page).to have_selector '[title="Desktop"]', count: 1 + + expect(page).to have_content( + '127.0.0.1 ' \ + 'Last accessed on 12 Mar 09:06 ' \ + 'Mobile Safari on iOS ' \ + 'Signed in on 12 Mar 09:06' + ) + + expect(page).to have_selector '[title="Smartphone"]', count: 1 + end + end + + scenario 'User can revoke a session', :js, :redis_session_store do + Capybara::Session.new(:session1) + Capybara::Session.new(:session2) + + # set an additional session in another browser + using_session :session2 do + gitlab_sign_in(user) + end + + using_session :session1 do + gitlab_sign_in(user) + visit profile_active_sessions_path + + expect(page).to have_link('Revoke', count: 1) + + accept_confirm { click_on 'Revoke' } + + expect(page).not_to have_link('Revoke') + end + + using_session :session2 do + visit profile_active_sessions_path + + expect(page).to have_content('You need to sign in or sign up before continuing.') + end + end +end diff --git a/spec/features/users/active_sessions_spec.rb b/spec/features/users/active_sessions_spec.rb new file mode 100644 index 0000000000000..631d7e3bcedd8 --- /dev/null +++ b/spec/features/users/active_sessions_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +feature 'Active user sessions', :clean_gitlab_redis_shared_state do + scenario 'Successful login adds a new active user login' do + now = Time.zone.parse('2018-03-12 09:06') + Timecop.freeze(now) do + user = create(:user) + gitlab_sign_in(user) + expect(current_path).to eq root_path + + sessions = ActiveSession.list(user) + expect(sessions.count).to eq 1 + + # refresh the current page updates the updated_at + Timecop.freeze(now + 1.minute) do + visit current_path + + sessions = ActiveSession.list(user) + expect(sessions.first).to have_attributes( + created_at: Time.zone.parse('2018-03-12 09:06'), + updated_at: Time.zone.parse('2018-03-12 09:07') + ) + end + end + end + + scenario 'Successful login cleans up obsolete entries' do + user = create(:user) + + Gitlab::Redis::SharedState.with do |redis| + redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d') + end + + gitlab_sign_in(user) + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).not_to include '59822c7d9fcdfa03725eff41782ad97d' + end + end + + scenario 'Sessionless login does not clean up obsolete entries' do + user = create(:user) + personal_access_token = create(:personal_access_token, user: user) + + Gitlab::Redis::SharedState.with do |redis| + redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d') + end + + visit user_path(user, :atom, private_token: personal_access_token.token) + expect(page.status_code).to eq 200 + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to include '59822c7d9fcdfa03725eff41782ad97d' + end + end + + scenario 'Logout deletes the active user login' do + user = create(:user) + gitlab_sign_in(user) + expect(current_path).to eq root_path + + expect(ActiveSession.list(user).count).to eq 1 + + gitlab_sign_out + expect(current_path).to eq new_user_session_path + + expect(ActiveSession.list(user)).to be_empty + end +end diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb new file mode 100644 index 0000000000000..129b2f926831b --- /dev/null +++ b/spec/models/active_session_spec.rb @@ -0,0 +1,216 @@ +require 'rails_helper' + +RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do + let(:user) do + create(:user).tap do |user| + user.current_sign_in_at = Time.current + end + end + + let(:session) { double(:session, id: '6919a6f1bb119dd7396fadc38fd18d0d') } + + let(:request) do + double(:request, { + user_agent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 8_1_3 like Mac OS X) AppleWebKit/600.1.4 ' \ + '(KHTML, like Gecko) Mobile/12B466 [FBDV/iPhone7,2]', + ip: '127.0.0.1', + session: session + }) + end + + describe '#current?' do + it 'returns true if the active session matches the current session' do + active_session = ActiveSession.new(session_id: '6919a6f1bb119dd7396fadc38fd18d0d') + + expect(active_session.current?(session)).to be true + end + + it 'returns false if the active session does not match the current session' do + active_session = ActiveSession.new(session_id: '59822c7d9fcdfa03725eff41782ad97d') + + expect(active_session.current?(session)).to be false + end + + it 'returns false if the session id is nil' do + active_session = ActiveSession.new(session_id: nil) + session = double(:session, id: nil) + + expect(active_session.current?(session)).to be false + end + end + + describe '.list' do + it 'returns all sessions by user' do + Gitlab::Redis::SharedState.with do |redis| + redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' })) + redis.set("session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d", Marshal.dump({ session_id: 'b' })) + redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '') + + redis.sadd( + "session:lookup:user:gitlab:#{user.id}", + %w[ + 6919a6f1bb119dd7396fadc38fd18d0d + 59822c7d9fcdfa03725eff41782ad97d + ] + ) + end + + expect(ActiveSession.list(user)).to match_array [{ session_id: 'a' }, { session_id: 'b' }] + end + + it 'does not return obsolete entries and cleans them up' do + Gitlab::Redis::SharedState.with do |redis| + redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ session_id: 'a' })) + + redis.sadd( + "session:lookup:user:gitlab:#{user.id}", + %w[ + 6919a6f1bb119dd7396fadc38fd18d0d + 59822c7d9fcdfa03725eff41782ad97d + ] + ) + end + + expect(ActiveSession.list(user)).to eq [{ session_id: 'a' }] + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.sscan_each("session:lookup:user:gitlab:#{user.id}").to_a).to eq ['6919a6f1bb119dd7396fadc38fd18d0d'] + end + end + + it 'returns an empty array if the use does not have any active session' do + expect(ActiveSession.list(user)).to eq [] + end + end + + describe '.set' do + it 'sets a new redis entry for the user session and a lookup entry' do + ActiveSession.set(user, request) + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.scan_each.to_a).to match_array [ + "session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", + "session:lookup:user:gitlab:#{user.id}" + ] + end + end + + it 'adds timestamps and information from the request' do + Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do + ActiveSession.set(user, request) + + session = ActiveSession.list(user) + + expect(session.count).to eq 1 + expect(session.first).to have_attributes( + ip_address: '127.0.0.1', + browser: 'Mobile Safari', + os: 'iOS', + device_name: 'iPhone 6', + device_type: 'smartphone', + created_at: Time.zone.parse('2018-03-12 09:06'), + updated_at: Time.zone.parse('2018-03-12 09:06'), + session_id: '6919a6f1bb119dd7396fadc38fd18d0d' + ) + end + end + + it 'keeps the created_at from the login on consecutive requests' do + now = Time.zone.parse('2018-03-12 09:06') + + Timecop.freeze(now) do + ActiveSession.set(user, request) + + Timecop.freeze(now + 1.minute) do + ActiveSession.set(user, request) + + session = ActiveSession.list(user) + + expect(session.first).to have_attributes( + created_at: Time.zone.parse('2018-03-12 09:06'), + updated_at: Time.zone.parse('2018-03-12 09:07') + ) + end + end + end + end + + describe '.destroy' do + it 'removes the entry associated with the currently killed user session' do + Gitlab::Redis::SharedState.with do |redis| + redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '') + redis.set("session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d", '') + redis.set("session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358", '') + end + + ActiveSession.destroy(user, request.session.id) + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.scan_each(match: "session:user:gitlab:*")).to match_array [ + "session:user:gitlab:#{user.id}:59822c7d9fcdfa03725eff41782ad97d", + "session:user:gitlab:9999:5c8611e4f9c69645ad1a1492f4131358" + ] + end + end + + it 'removes the lookup entry' do + Gitlab::Redis::SharedState.with do |redis| + redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '') + redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d') + end + + ActiveSession.destroy(user, request.session.id) + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.scan_each(match: "session:lookup:user:gitlab:#{user.id}").to_a).to be_empty + end + end + + it 'removes the devise session' do + Gitlab::Redis::SharedState.with do |redis| + redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '') + redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", '') + end + + ActiveSession.destroy(user, request.session.id) + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.scan_each(match: "session:gitlab:*").to_a).to be_empty + end + end + + it 'does not remove the devise session if the active session could not be found' do + Gitlab::Redis::SharedState.with do |redis| + redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", '') + end + + other_user = create(:user) + + ActiveSession.destroy(other_user, request.session.id) + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.scan_each(match: "session:gitlab:*").to_a).not_to be_empty + end + end + end + + describe '.cleanup' do + it 'removes obsolete lookup entries' do + Gitlab::Redis::SharedState.with do |redis| + redis.set("session:user:gitlab:#{user.id}:6919a6f1bb119dd7396fadc38fd18d0d", '') + redis.sadd("session:lookup:user:gitlab:#{user.id}", '6919a6f1bb119dd7396fadc38fd18d0d') + redis.sadd("session:lookup:user:gitlab:#{user.id}", '59822c7d9fcdfa03725eff41782ad97d') + end + + ActiveSession.cleanup(user) + + Gitlab::Redis::SharedState.with do |redis| + expect(redis.smembers("session:lookup:user:gitlab:#{user.id}")).to eq ['6919a6f1bb119dd7396fadc38fd18d0d'] + end + end + + it 'does not bail if there are no lookup entries' do + ActiveSession.cleanup(user) + end + end +end -- GitLab