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, :step_up_authenticated
].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.



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

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



122
123
124
125
126
127
# File 'app/models/active_session.rb', line 122

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



157
158
159
160
161
162
163
164
165
# File 'app/models/active_session.rb', line 157

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



149
150
151
152
153
154
155
# File 'app/models/active_session.rb', line 149

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



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

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



175
176
177
# File 'app/models/active_session.rb', line 175

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



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

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

.list(user) ⇒ Object



114
115
116
117
118
119
120
# File 'app/models/active_session.rb', line 114

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



188
189
190
# File 'app/models/active_session.rb', line 188

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

.lookup_key_name(user_id) ⇒ Object



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

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



195
196
197
198
199
# File 'app/models/active_session.rb', line 195

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



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

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
110
111
112
# 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?,
      step_up_authenticated:
        Feature.enabled?(:omniauth_step_up_auth_for_admin_mode, user) &&
          ::Gitlab::Auth::Oidc::StepUpAuthentication.succeeded?(request.session)
    )

    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



228
229
230
# File 'app/models/active_session.rb', line 228

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