Class: ActiveSession

Inherits:
Object
  • Object
show all
Includes:
ActiveModel::Model
Defined in:
app/models/active_session.rb

Overview

Backing store for GitLab session data.

The raw session information is stored by the Rails session store (config/initializers/session_store.rb). These entries are accessible by the rack_key_name class method and constitute the base of the session data entries. All other entries in the session store can be traced back to these entries.

After a user logs in (config/initializers/warden.rb) a further entry is made in Redis. This entry holds a record of the user’s logged in session. These are accessible with the key_name(user_id, session_id) class method. These entries will expire. Lookups to these entries are lazilly cleaned on future user access.

There is a reference to all sessions that belong to a specific user. A user may login through multiple browsers/devices and thus record multiple login sessions. These are accessible through the lookup_key_name(user_id) class method.

Constant Summary collapse

SESSION_BATCH_SIZE =
200
ALLOWED_NUMBER_OF_ACTIVE_SESSIONS =
100
ATTR_ACCESSOR_LIST =
[
  :ip_address, :browser, :os,
  :device_name, :device_type,
  :is_impersonated, :session_id, :session_private_id,
  :admin_mode
].freeze
ATTR_READER_LIST =
[
  :created_at, :updated_at
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.cleaned_up_lookup_entries(redis, user, removed = []) ⇒ 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. Records removed keys in the optional ‘removed` argument array.



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'app/models/active_session.rb', line 318

def self.cleaned_up_lookup_entries(redis, user, removed = [])
  lookup_key = lookup_key_name(user.id)
  session_ids = session_ids_for_user(user.id)
  session_ids_and_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.
  redis.pipelined do |pipeline|
    session_ids_and_entries.each do |session_id, entry|
      next if entry

      pipeline.srem?(lookup_key, session_id)
    end
  end

  removed.concat(session_ids_and_entries.select { |_, v| v.nil? }.keys)

  session_ids_and_entries.values.compact
end

.cleanup(user) ⇒ Object



119
120
121
122
123
124
# File 'app/models/active_session.rb', line 119

def self.cleanup(user)
  Gitlab::Redis::Sessions.with do |redis|
    clean_up_old_sessions(redis, user)
    cleaned_up_lookup_entries(redis, user)
  end
end

.destroy_all_but_current(user, current_rack_session) ⇒ Object



154
155
156
157
158
159
160
161
162
# File 'app/models/active_session.rb', line 154

def self.destroy_all_but_current(user, current_rack_session)
  sessions = not_impersonated(user)
  sessions.reject! { |session| session.current?(current_rack_session) } if current_rack_session

  Gitlab::Redis::Sessions.with do |redis|
    session_ids = sessions.flat_map(&:ids)
    destroy_sessions(redis, user, session_ids) if session_ids.any?
  end
end

.destroy_session(user, session_id) ⇒ Object



146
147
148
149
150
151
152
# File 'app/models/active_session.rb', line 146

def self.destroy_session(user, session_id)
  return unless session_id

  Gitlab::Redis::Sessions.with do |redis|
    destroy_sessions(redis, user, [session_id].compact)
  end
end

.destroy_sessions(redis, user, session_ids) ⇒ Object



126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'app/models/active_session.rb', line 126

def self.destroy_sessions(redis, user, session_ids)
  return if session_ids.empty?

  key_names = session_ids.map { |session_id| key_name(user.id, session_id) }
  key_names += session_ids.map { |session_id| key_name_v1(user.id, session_id) }

  redis.srem(lookup_key_name(user.id), session_ids)

  session_keys = rack_session_keys(session_ids)
  Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
    if Gitlab::Redis::ClusterUtil.cluster?(redis)
      Gitlab::Redis::ClusterUtil.batch_unlink(key_names, redis)
      Gitlab::Redis::ClusterUtil.batch_unlink(session_keys, redis)
    else
      redis.del(key_names)
      redis.del(session_keys)
    end
  end
end

.key_name(user_id, session_id = '*') ⇒ Object



172
173
174
# File 'app/models/active_session.rb', line 172

def self.key_name(user_id, session_id = '*')
  "#{Gitlab::Redis::Sessions::USER_SESSIONS_NAMESPACE}::v2:#{user_id}:#{session_id}"
end

.key_name_v1(user_id, session_id = '*') ⇒ Object

Deprecated



177
178
179
# File 'app/models/active_session.rb', line 177

def self.key_name_v1(user_id, session_id = '*')
  "#{Gitlab::Redis::Sessions::USER_SESSIONS_NAMESPACE}:#{user_id}:#{session_id}"
end

.list(user) ⇒ Object



111
112
113
114
115
116
117
# File 'app/models/active_session.rb', line 111

def self.list(user)
  Gitlab::Redis::Sessions.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



185
186
187
# File 'app/models/active_session.rb', line 185

def self.list_sessions(user)
  sessions_from_ids(session_ids_for_user(user.id))
end

.lookup_key_name(user_id) ⇒ Object



181
182
183
# File 'app/models/active_session.rb', line 181

def self.lookup_key_name(user_id)
  "#{Gitlab::Redis::Sessions::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user_id}"
end

.session_ids_for_user(user_id) ⇒ Object

Lists the relevant session IDs for the user.

Returns an array of strings



192
193
194
195
196
# File 'app/models/active_session.rb', line 192

def self.session_ids_for_user(user_id)
  Gitlab::Redis::Sessions.with do |redis|
    redis.smembers(lookup_key_name(user_id))
  end
end

.sessions_from_ids(session_ids) ⇒ Object

Lists the session Hash objects for the given session IDs.

session_ids - An array of strings

Returns an array of ActiveSession objects



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'app/models/active_session.rb', line 203

def self.sessions_from_ids(session_ids)
  return [] if session_ids.empty?

  Gitlab::Redis::Sessions.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
        raw_sessions = if Gitlab::Redis::ClusterUtil.cluster?(redis)
                         Gitlab::Redis::ClusterUtil.batch_get(session_keys_batch, redis)
                       else
                         redis.mget(session_keys_batch)
                       end

        raw_sessions.compact.map do |raw_session|
          load_raw_session(raw_session)
        end
      end
    end
  end
end

.set(user, request) ⇒ Object



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'app/models/active_session.rb', line 74

def self.set(user, request)
  Gitlab::Redis::Sessions.with do |redis|
    session_private_id = request.session.id.private_id
    client = Gitlab::SafeDeviceDetector.new(request.user_agent)
    timestamp = Time.current
    expiry = Settings.gitlab['session_expire_delay'] * 60

    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. || timestamp,
      updated_at: timestamp,
      session_private_id: session_private_id,
      is_impersonated: request.session[:impersonator_id].present?,
      admin_mode: Gitlab::Auth::CurrentUserMode.new(user, request.session).admin_mode?
    )

    Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
      redis.pipelined do |pipeline|
        pipeline.setex(
          key_name(user.id, session_private_id),
          expiry,
          active_user_session.dump
        )

        pipeline.sadd?(
          lookup_key_name(user.id),
          session_private_id
        )
      end
    end
  end
end

Instance Method Details

#created_at=(time) ⇒ Object



41
42
43
# File 'app/models/active_session.rb', line 41

def created_at=(time)
  @created_at = time.is_a?(String) ? Time.zone.parse(time) : time
end

#current?(rack_session) ⇒ Boolean

Returns:

  • (Boolean)


49
50
51
52
53
54
55
# File 'app/models/active_session.rb', line 49

def current?(rack_session)
  return false if session_private_id.nil? || rack_session.id.nil?

  # Rack v2.0.8+ added private_id, which uses the hash of the
  # public_id to avoid timing attacks.
  session_private_id == rack_session.id.private_id
end

#dumpObject



225
226
227
# File 'app/models/active_session.rb', line 225

def dump
  "v2:#{Gitlab::Json.dump(self)}"
end

#eql?(other) ⇒ Boolean Also known as: ==

Returns:

  • (Boolean)


57
58
59
# File 'app/models/active_session.rb', line 57

def eql?(other)
  other.is_a?(self.class) && id == other.id
end

#human_device_typeObject



70
71
72
# File 'app/models/active_session.rb', line 70

def human_device_type
  device_type&.titleize
end

#idObject



62
63
64
# File 'app/models/active_session.rb', line 62

def id
  session_private_id.presence || session_id
end

#idsObject



66
67
68
# File 'app/models/active_session.rb', line 66

def ids
  [session_private_id, session_id].compact
end

#updated_at=(time) ⇒ Object



45
46
47
# File 'app/models/active_session.rb', line 45

def updated_at=(time)
  @updated_at = time.is_a?(String) ? Time.zone.parse(time) : time
end