summaryrefslogtreecommitdiff
path: root/app/models/active_session.rb
blob: 0d9c6a4a1f01a4dac40138763a554f0b63599ffb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
64
65
66
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
102
103
104
105
106
107
108
109
110
111
112
# frozen_string_literal: true

class ActiveSession
  include ActiveModel::Model

  attr_accessor :created_at, :updated_at,
    :session_id, :ip_address,
    :browser, :os, :device_name, :device_type

  def current?(session)
    return false if session_id.nil? || session.id.nil?

    session_id == session.id
  end

  def human_device_type
    device_type&.titleize
  end

  def self.set(user, request)
    Gitlab::Redis::SharedState.with do |redis|
      session_id = request.session.id
      client = DeviceDetector.new(request.user_agent)
      timestamp = Time.current

      active_user_session = new(
        ip_address: request.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 || timestamp,
        updated_at: timestamp,
        session_id: session_id
      )

      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

  def self.list(user)
    Gitlab::Redis::SharedState.with do |redis|
      cleaned_up_lookup_entries(redis, user.id).map do |entry|
        # rubocop:disable Security/MarshalLoad
        Marshal.load(entry)
        # rubocop:enable Security/MarshalLoad
      end
    end
  end

  def self.destroy(user, session_id)
    Gitlab::Redis::SharedState.with do |redis|
      redis.srem(lookup_key_name(user.id), session_id)

      deleted_keys = redis.del(key_name(user.id, session_id))

      # only allow deleting the devise session if we could actually find a
      # related active session. this prevents another user from deleting
      # someone else's session.
      if deleted_keys > 0
        redis.del("#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}")
      end
    end
  end

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

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

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

  def self.cleaned_up_lookup_entries(redis, user_id)
    lookup_key = lookup_key_name(user_id)

    session_ids = redis.smembers(lookup_key)

    entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
    return [] if entry_keys.empty?

    entries = redis.mget(entry_keys)

    session_ids_and_entries = session_ids.zip(entries)

    # 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.reject { |_session_id, entry| entry }.each do |session_id, _entry|
      redis.srem(lookup_key, session_id)
    end

    session_ids_and_entries.select { |_session_id, entry| entry }.map { |_session_id, entry| entry }
  end
end