Commit 05347706 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Merge branch '27518-revoke-active-sessions' into 'master'

Feature for #27518, users can revoke active sessions again.

Closes #27518

See merge request gitlab-org/gitlab!17462
parents 26f35821 85c52c73
...@@ -4,4 +4,13 @@ class Profiles::ActiveSessionsController < Profiles::ApplicationController ...@@ -4,4 +4,13 @@ class Profiles::ActiveSessionsController < Profiles::ApplicationController
def index def index
@sessions = ActiveSession.list(current_user).reject(&:is_impersonated) @sessions = ActiveSession.list(current_user).reject(&:is_impersonated)
end end
def destroy
ActiveSession.destroy_with_public_id(current_user, params[:id])
respond_to do |format|
format.html { redirect_to profile_active_sessions_url, status: :found }
format.js { head :ok }
end
end
end end
...@@ -6,9 +6,11 @@ class ActiveSession ...@@ -6,9 +6,11 @@ class ActiveSession
SESSION_BATCH_SIZE = 200 SESSION_BATCH_SIZE = 200
ALLOWED_NUMBER_OF_ACTIVE_SESSIONS = 100 ALLOWED_NUMBER_OF_ACTIVE_SESSIONS = 100
attr_writer :session_id
attr_accessor :created_at, :updated_at, attr_accessor :created_at, :updated_at,
:session_id, :ip_address, :ip_address, :browser, :os,
:browser, :os, :device_name, :device_type, :device_name, :device_type,
:is_impersonated :is_impersonated
def current?(session) def current?(session)
...@@ -21,6 +23,11 @@ class ActiveSession ...@@ -21,6 +23,11 @@ class ActiveSession
device_type&.titleize device_type&.titleize
end end
def public_id
encrypted_id = Gitlab::CryptoHelper.aes256_gcm_encrypt(session_id)
CGI.escape(encrypted_id)
end
def self.set(user, request) def self.set(user, request)
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
session_id = request.session.id session_id = request.session.id
...@@ -70,6 +77,11 @@ class ActiveSession ...@@ -70,6 +77,11 @@ class ActiveSession
end end
end end
def self.destroy_with_public_id(user, public_id)
session_id = decrypt_public_id(public_id)
destroy(user, session_id) unless session_id.nil?
end
def self.destroy_sessions(redis, user, session_ids) def self.destroy_sessions(redis, user, session_ids)
key_names = session_ids.map {|session_id| key_name(user.id, session_id) } key_names = session_ids.map {|session_id| key_name(user.id, session_id) }
session_names = session_ids.map {|session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" } session_names = session_ids.map {|session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" }
...@@ -146,9 +158,9 @@ class ActiveSession ...@@ -146,9 +158,9 @@ class ActiveSession
# remove sessions if there are more than ALLOWED_NUMBER_OF_ACTIVE_SESSIONS. # remove sessions if there are more than ALLOWED_NUMBER_OF_ACTIVE_SESSIONS.
sessions = active_session_entries(session_ids, user.id, redis) sessions = active_session_entries(session_ids, user.id, redis)
sessions.sort_by! {|session| session.updated_at }.reverse! sessions.sort_by! {|session| session.updated_at }.reverse!
sessions = sessions.drop(ALLOWED_NUMBER_OF_ACTIVE_SESSIONS) destroyable_sessions = sessions.drop(ALLOWED_NUMBER_OF_ACTIVE_SESSIONS)
sessions = sessions.map { |session| session.session_id } destroyable_session_ids = destroyable_sessions.map { |session| session.send :session_id } # rubocop:disable GitlabSecurity/PublicSend
destroy_sessions(redis, user, sessions) if sessions.any? destroy_sessions(redis, user, destroyable_session_ids) if destroyable_session_ids.any?
end end
def self.cleaned_up_lookup_entries(redis, user) def self.cleaned_up_lookup_entries(redis, user)
...@@ -167,4 +179,15 @@ class ActiveSession ...@@ -167,4 +179,15 @@ class ActiveSession
entries.compact entries.compact
end end
private_class_method def self.decrypt_public_id(public_id)
decoded_id = CGI.unescape(public_id)
Gitlab::CryptoHelper.aes256_gcm_decrypt(decoded_id)
rescue
nil
end
private
attr_reader :session_id
end end
...@@ -24,3 +24,9 @@ ...@@ -24,3 +24,9 @@
%strong= _('Signed in') %strong= _('Signed in')
= s_('ProfileSession|on') = s_('ProfileSession|on')
= l(active_session.created_at, format: :short) = l(active_session.created_at, format: :short)
- unless is_current_session
.float-right
= link_to profile_active_session_path(active_session.public_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')
---
title: Restores user's ability to revoke sessions from the active sessions
page.
merge_request: 17462
author: Jesse Hall @jessehall3
type: changed
...@@ -24,6 +24,11 @@ review the sessions, and revoke any you don't recognize. ...@@ -24,6 +24,11 @@ review the sessions, and revoke any you don't recognize.
GitLab allows users to have up to 100 active sessions at once. If the number of active sessions GitLab allows users to have up to 100 active sessions at once. If the number of active sessions
exceeds 100, the oldest ones are deleted. exceeds 100, the oldest ones are deleted.
## Revoking a session
1. Use the previous steps to navigate to **Active Sessions**.
1. Click on **Revoke** besides a session. The current session cannot be revoked, as this would sign you out of GitLab.
<!-- ## Troubleshooting <!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues Include any troubleshooting steps that you can foresee. If you know beforehand what issues
......
...@@ -2152,6 +2152,9 @@ msgstr "" ...@@ -2152,6 +2152,9 @@ msgstr ""
msgid "Are you sure? Removing this GPG key does not affect already signed commits." msgid "Are you sure? Removing this GPG key does not affect already signed commits."
msgstr "" msgstr ""
msgid "Are you sure? The device will be signed out of GitLab."
msgstr ""
msgid "Are you sure? This will invalidate your registered applications and U2F devices." msgid "Are you sure? This will invalidate your registered applications and U2F devices."
msgstr "" msgstr ""
......
...@@ -84,4 +84,31 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do ...@@ -84,4 +84,31 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
expect(page).not_to have_content('Chrome on Windows') expect(page).not_to have_content('Chrome on Windows')
end end
end end
it '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 end
...@@ -44,6 +44,19 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do ...@@ -44,6 +44,19 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
end end
end end
describe '#public_id' do
it 'returns an encrypted, url-encoded session id' do
original_session_id = "!*'();:@&\n=+$,/?%abcd#123[4567]8"
active_session = ActiveSession.new(session_id: original_session_id)
encrypted_encoded_id = active_session.public_id
encrypted_id = CGI.unescape(encrypted_encoded_id)
derived_session_id = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_id)
expect(original_session_id).to eq derived_session_id
end
end
describe '.list' do describe '.list' do
it 'returns all sessions by user' do it 'returns all sessions by user' do
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
...@@ -173,8 +186,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do ...@@ -173,8 +186,7 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
device_name: 'iPhone 6', device_name: 'iPhone 6',
device_type: 'smartphone', device_type: 'smartphone',
created_at: Time.zone.parse('2018-03-12 09:06'), created_at: Time.zone.parse('2018-03-12 09:06'),
updated_at: Time.zone.parse('2018-03-12 09:06'), updated_at: Time.zone.parse('2018-03-12 09:06')
session_id: '6919a6f1bb119dd7396fadc38fd18d0d'
) )
end end
end end
...@@ -244,6 +256,40 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do ...@@ -244,6 +256,40 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
end end
end end
describe '.destroy_with_public_id' do
it 'receives a user and public id and destroys the associated session' do
ActiveSession.set(user, request)
session = ActiveSession.list(user).first
ActiveSession.destroy_with_public_id(user, session.public_id)
total_sessions = ActiveSession.list(user).count
expect(total_sessions).to eq 0
end
it 'handles invalid input for public id' do
expect do
ActiveSession.destroy_with_public_id(user, nil)
end.not_to raise_error
expect do
ActiveSession.destroy_with_public_id(user, "")
end.not_to raise_error
expect do
ActiveSession.destroy_with_public_id(user, "aaaaaaaa")
end.not_to raise_error
end
it 'does not attempt to destroy session when given invalid input for public id' do
expect(ActiveSession).not_to receive(:destroy)
ActiveSession.destroy_with_public_id(user, nil)
ActiveSession.destroy_with_public_id(user, "")
ActiveSession.destroy_with_public_id(user, "aaaaaaaa")
end
end
describe '.cleanup' do describe '.cleanup' do
before do before do
stub_const("ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS", 5) stub_const("ActiveSession::ALLOWED_NUMBER_OF_ACTIVE_SESSIONS", 5)
......
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