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

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#browserObject

Returns the value of attribute browser.



28
29
30
# File 'app/models/active_session.rb', line 28

def browser
  @browser
end

#created_atObject

Returns the value of attribute created_at.



32
33
34
# File 'app/models/active_session.rb', line 32

def created_at
  @created_at
end

#device_nameObject

Returns the value of attribute device_name.



28
29
30
# File 'app/models/active_session.rb', line 28

def device_name
  @device_name
end

#device_typeObject

Returns the value of attribute device_type.



28
29
30
# File 'app/models/active_session.rb', line 28

def device_type
  @device_type
end

#ip_addressObject

Returns the value of attribute ip_address.



28
29
30
# File 'app/models/active_session.rb', line 28

def ip_address
  @ip_address
end

#is_impersonatedObject

Returns the value of attribute is_impersonated.



28
29
30
# File 'app/models/active_session.rb', line 28

def is_impersonated
  @is_impersonated
end

#osObject

Returns the value of attribute os.



28
29
30
# File 'app/models/active_session.rb', line 28

def os
  @os
end

#session_idObject

Returns the value of attribute session_id.



28
29
30
# File 'app/models/active_session.rb', line 28

def session_id
  @session_id
end

#session_private_idObject

Returns the value of attribute session_private_id.



28
29
30
# File 'app/models/active_session.rb', line 28

def session_private_id
  @session_private_id
end

#updated_atObject

Returns the value of attribute updated_at.



32
33
34
# File 'app/models/active_session.rb', line 32

def updated_at
  @updated_at
end

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.



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'app/models/active_session.rb', line 296

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)
      removed << session_id
    end
  end

  session_ids_and_entries.values.compact
end

.cleanup(user) ⇒ Object



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

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



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

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



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

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



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

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)

  Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do
    redis.del(key_names)
    redis.del(rack_session_keys(session_ids))
  end
end

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



170
171
172
# File 'app/models/active_session.rb', line 170

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



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

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

.list(user) ⇒ Object



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

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



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

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

.lookup_key_name(user_id) ⇒ Object



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

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



190
191
192
193
194
# File 'app/models/active_session.rb', line 190

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



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'app/models/active_session.rb', line 201

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
        redis.mget(session_keys_batch).compact.map do |raw_session|
          load_raw_session(raw_session)
        end
      end
    end
  end
end

.set(user, request) ⇒ Object



67
68
69
70
71
72
73
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
# File 'app/models/active_session.rb', line 67

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?
    )

    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

set marketing cookie when user has active session



104
105
106
107
108
109
110
111
112
113
# File 'app/models/active_session.rb', line 104

def self.set_active_user_cookie(auth)
  expiration_time = 2.weeks.from_now

  auth.cookies[:gitlab_user] =
    {
      value: true,
      domain: Gitlab.config.gitlab.host,
      expires: expiration_time
    }
end

Instance Method Details

#current?(rack_session) ⇒ Boolean

Returns:

  • (Boolean)


42
43
44
45
46
47
48
# File 'app/models/active_session.rb', line 42

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



217
218
219
# File 'app/models/active_session.rb', line 217

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

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

Returns:

  • (Boolean)


50
51
52
# File 'app/models/active_session.rb', line 50

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

#human_device_typeObject



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

def human_device_type
  device_type&.titleize
end

#idObject



55
56
57
# File 'app/models/active_session.rb', line 55

def id
  session_private_id.presence || session_id
end

#idsObject



59
60
61
# File 'app/models/active_session.rb', line 59

def ids
  [session_private_id, session_id].compact
end