Class: ActiveSession
- Inherits:
-
Object
- Object
- ActiveSession
- Includes:
- ActiveModel::Model
- Defined in:
- app/models/active_session.rb
Constant Summary collapse
- SESSION_BATCH_SIZE =
200
- ALLOWED_NUMBER_OF_ACTIVE_SESSIONS =
100
Instance Attribute Summary collapse
-
#browser ⇒ Object
Returns the value of attribute browser.
-
#created_at ⇒ Object
Returns the value of attribute created_at.
-
#device_name ⇒ Object
Returns the value of attribute device_name.
-
#device_type ⇒ Object
Returns the value of attribute device_type.
-
#ip_address ⇒ Object
Returns the value of attribute ip_address.
-
#is_impersonated ⇒ Object
Returns the value of attribute is_impersonated.
-
#os ⇒ Object
Returns the value of attribute os.
-
#session_id ⇒ Object
Returns the value of attribute session_id.
-
#updated_at ⇒ Object
Returns the value of attribute updated_at.
Class Method Summary collapse
- .active_session_entries(session_ids, user_id, redis) ⇒ Object
- .clean_up_old_sessions(redis, user) ⇒ Object
-
.cleaned_up_lookup_entries(redis, user) ⇒ Object
Cleans up the lookup set by removing any session IDs that are no longer present.
- .cleanup(user) ⇒ Object
- .destroy(user, session_id) ⇒ Object
- .destroy_all_but_current(user, current_session) ⇒ Object
- .destroy_sessions(redis, user, session_ids) ⇒ Object
- .destroy_with_public_id(user, public_id) ⇒ Object
- .key_name(user_id, session_id = '*') ⇒ Object
- .list(user) ⇒ Object
- .list_sessions(user) ⇒ Object
-
.load_raw_session(raw_session) ⇒ Object
Deserializes a session Hash object from Redis.
- .lookup_key_name(user_id) ⇒ Object
- .not_impersonated(user) ⇒ Object
- .rack_session_keys(session_ids) ⇒ Object
- .raw_active_session_entries(redis, session_ids, user_id) ⇒ Object
-
.session_ids_for_user(user_id) ⇒ Object
Lists the relevant session IDs for the user.
-
.sessions_from_ids(session_ids) ⇒ Object
Lists the session Hash objects for the given session IDs.
- .set(user, request) ⇒ Object
Instance Method Summary collapse
- #current?(session) ⇒ Boolean
- #human_device_type ⇒ Object
-
#public_id ⇒ Object
This is not the same as Rack::Session::SessionId#public_id, but we need to preserve this for backwards compatibility.
Instance Attribute Details
#browser ⇒ Object
Returns the value of attribute browser
9 10 11 |
# File 'app/models/active_session.rb', line 9 def browser @browser end |
#created_at ⇒ Object
Returns the value of attribute created_at
9 10 11 |
# File 'app/models/active_session.rb', line 9 def created_at @created_at end |
#device_name ⇒ Object
Returns the value of attribute device_name
9 10 11 |
# File 'app/models/active_session.rb', line 9 def device_name @device_name end |
#device_type ⇒ Object
Returns the value of attribute device_type
9 10 11 |
# File 'app/models/active_session.rb', line 9 def device_type @device_type end |
#ip_address ⇒ Object
Returns the value of attribute ip_address
9 10 11 |
# File 'app/models/active_session.rb', line 9 def ip_address @ip_address end |
#is_impersonated ⇒ Object
Returns the value of attribute is_impersonated
9 10 11 |
# File 'app/models/active_session.rb', line 9 def is_impersonated @is_impersonated end |
#os ⇒ Object
Returns the value of attribute os
9 10 11 |
# File 'app/models/active_session.rb', line 9 def os @os end |
#session_id ⇒ Object
Returns the value of attribute session_id
9 10 11 |
# File 'app/models/active_session.rb', line 9 def session_id @session_id end |
#updated_at ⇒ Object
Returns the value of attribute updated_at
9 10 11 |
# File 'app/models/active_session.rb', line 9 def updated_at @updated_at end |
Class Method Details
.active_session_entries(session_ids, user_id, redis) ⇒ Object
204 205 206 207 208 209 210 211 212 |
# File 'app/models/active_session.rb', line 204 def self.active_session_entries(session_ids, user_id, redis) return [] if session_ids.empty? entry_keys = raw_active_session_entries(redis, session_ids, user_id) entry_keys.compact.map do |raw_session| load_raw_session(raw_session) end end |
.clean_up_old_sessions(redis, user) ⇒ Object
214 215 216 217 218 219 220 221 222 223 224 225 |
# File 'app/models/active_session.rb', line 214 def self.clean_up_old_sessions(redis, user) session_ids = session_ids_for_user(user.id) return if session_ids.count <= 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.sort_by! {|session| session.updated_at }.reverse! destroyable_sessions = sessions.drop(ALLOWED_NUMBER_OF_ACTIVE_SESSIONS) destroyable_session_ids = destroyable_sessions.map { |session| session.session_id } destroy_sessions(redis, user, destroyable_session_ids) if destroyable_session_ids.any? end |
.cleaned_up_lookup_entries(redis, user) ⇒ Object
Cleans up the lookup set by removing any session IDs that are no longer present.
Returns an array of marshalled ActiveModel objects that are still active.
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 |
# File 'app/models/active_session.rb', line 230 def self.cleaned_up_lookup_entries(redis, user) session_ids = session_ids_for_user(user.id) entries = raw_active_session_entries(redis, session_ids, user.id) # 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 = session_ids.zip(entries) redis.pipelined do session_ids_and_entries.reject { |_session_id, entry| entry }.each do |session_id, _entry| redis.srem(lookup_key_name(user.id), session_id) end end entries.compact end |
.cleanup(user) ⇒ Object
101 102 103 104 105 106 |
# File 'app/models/active_session.rb', line 101 def self.cleanup(user) Gitlab::Redis::SharedState.with do |redis| clean_up_old_sessions(redis, user) cleaned_up_lookup_entries(redis, user) end end |
.destroy(user, session_id) ⇒ Object
73 74 75 76 77 78 79 |
# File 'app/models/active_session.rb', line 73 def self.destroy(user, session_id) return unless session_id Gitlab::Redis::SharedState.with do |redis| destroy_sessions(redis, user, [session_id]) end end |
.destroy_all_but_current(user, current_session) ⇒ Object
108 109 110 111 112 113 114 115 |
# File 'app/models/active_session.rb', line 108 def self.destroy_all_but_current(user, current_session) session_ids = not_impersonated(user) session_ids.reject! { |session| session.current?(current_session) } if current_session Gitlab::Redis::SharedState.with do |redis| destroy_sessions(redis, user, session_ids.map(&:session_id)) if session_ids.any? end end |
.destroy_sessions(redis, user, session_ids) ⇒ Object
90 91 92 93 94 95 96 97 98 99 |
# File 'app/models/active_session.rb', line 90 def self.destroy_sessions(redis, user, session_ids) key_names = session_ids.map { |session_id| key_name(user.id, session_id.public_id) } redis.srem(lookup_key_name(user.id), session_ids.map(&:public_id)) Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do redis.del(key_names) redis.del(rack_session_keys(session_ids)) end end |
.destroy_with_public_id(user, public_id) ⇒ Object
81 82 83 84 85 86 87 88 |
# File 'app/models/active_session.rb', line 81 def self.destroy_with_public_id(user, public_id) decrypted_id = decrypt_public_id(public_id) return if decrypted_id.nil? session_id = Rack::Session::SessionId.new(decrypted_id) destroy(user, session_id) end |
.key_name(user_id, session_id = '*') ⇒ Object
121 122 123 |
# File 'app/models/active_session.rb', line 121 def self.key_name(user_id, session_id = '*') "#{Gitlab::Redis::SharedState::USER_SESSIONS_NAMESPACE}:#{user_id}:#{session_id}" end |
.list(user) ⇒ Object
65 66 67 68 69 70 71 |
# File 'app/models/active_session.rb', line 65 def self.list(user) Gitlab::Redis::SharedState.with do |redis| cleaned_up_lookup_entries(redis, user).map do |raw_session| load_raw_session(raw_session) end end end |
.list_sessions(user) ⇒ Object
129 130 131 |
# File 'app/models/active_session.rb', line 129 def self.list_sessions(user) sessions_from_ids(session_ids_for_user(user.id)) end |
.load_raw_session(raw_session) ⇒ Object
Deserializes a session Hash object from Redis.
raw_session - Raw bytes from Redis
Returns an ActiveSession object
169 170 171 172 173 174 175 176 177 178 179 |
# File 'app/models/active_session.rb', line 169 def self.load_raw_session(raw_session) # rubocop:disable Security/MarshalLoad session = Marshal.load(raw_session) # rubocop:enable Security/MarshalLoad # Older ActiveSession models serialize `session_id` as strings, To # avoid breaking older sessions, we keep backwards compatibility # with older Redis keys and initiate Rack::Session::SessionId here. session.session_id = Rack::Session::SessionId.new(session.session_id) if session.try(:session_id).is_a?(String) session end |
.lookup_key_name(user_id) ⇒ Object
125 126 127 |
# File 'app/models/active_session.rb', line 125 def self.lookup_key_name(user_id) "#{Gitlab::Redis::SharedState::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user_id}" end |
.not_impersonated(user) ⇒ Object
117 118 119 |
# File 'app/models/active_session.rb', line 117 def self.not_impersonated(user) list(user).reject(&:is_impersonated) end |
.rack_session_keys(session_ids) ⇒ Object
181 182 183 184 185 186 187 188 189 190 191 192 |
# File 'app/models/active_session.rb', line 181 def self.rack_session_keys(session_ids) session_ids.each_with_object([]) do |session_id, arr| # This is a redis-rack implementation detail # (https://github.com/redis-store/redis-rack/blob/master/lib/rack/session/redis.rb#L88) # # We need to delete session keys based on the legacy public key name # and the newer private ID keys, but there's no well-defined interface # so we have to do it directly. arr << "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id.public_id}" arr << "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id.private_id}" end end |
.raw_active_session_entries(redis, session_ids, user_id) ⇒ Object
194 195 196 197 198 199 200 201 202 |
# File 'app/models/active_session.rb', line 194 def self.raw_active_session_entries(redis, session_ids, user_id) return [] if session_ids.empty? entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) } Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do redis.mget(entry_keys) end end |
.session_ids_for_user(user_id) ⇒ Object
Lists the relevant session IDs for the user.
Returns an array of Rack::Session::SessionId objects
136 137 138 139 140 141 |
# File 'app/models/active_session.rb', line 136 def self.session_ids_for_user(user_id) Gitlab::Redis::SharedState.with do |redis| session_ids = redis.smembers(lookup_key_name(user_id)) session_ids.map { |id| Rack::Session::SessionId.new(id) } end end |
.sessions_from_ids(session_ids) ⇒ Object
Lists the session Hash objects for the given session IDs.
session_ids - An array of Rack::Session::SessionId objects
Returns an array of ActiveSession objects
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'app/models/active_session.rb', line 148 def self.sessions_from_ids(session_ids) return [] if session_ids.empty? Gitlab::Redis::SharedState.with do |redis| session_keys = rack_session_keys(session_ids) session_keys.each_slice(SESSION_BATCH_SIZE).flat_map do |session_keys_batch| Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do redis.mget(session_keys_batch).compact.map do |raw_session| load_raw_session(raw_session) end end end end end |
.set(user, request) ⇒ Object
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'app/models/active_session.rb', line 32 def self.set(user, request) Gitlab::Redis::SharedState.with do |redis| session_id = request.session.id.public_id client = DeviceDetector.new(request.user_agent) = Time.current active_user_session = new( ip_address: request.remote_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 || , updated_at: , session_id: session_id, is_impersonated: request.session[:impersonator_id].present? ) 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 |
Instance Method Details
#current?(session) ⇒ Boolean
14 15 16 17 18 19 20 |
# File 'app/models/active_session.rb', line 14 def current?(session) return false if session_id.nil? || session.id.nil? # Rack v2.0.8+ added private_id, which uses the hash of the # public_id to avoid timing attacks. session_id.private_id == session.id.private_id end |
#human_device_type ⇒ Object
22 23 24 |
# File 'app/models/active_session.rb', line 22 def human_device_type device_type&.titleize end |
#public_id ⇒ Object
This is not the same as Rack::Session::SessionId#public_id, but we need to preserve this for backwards compatibility.
28 29 30 |
# File 'app/models/active_session.rb', line 28 def public_id Gitlab::CryptoHelper.aes256_gcm_encrypt(session_id.public_id) end |