summaryrefslogtreecommitdiff
path: root/lib/gitlab/auth/current_user_mode.rb
blob: a6d706c2a495a2e7340f9913d3ca31e7f6d96bc7 (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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# frozen_string_literal: true

module Gitlab
  module Auth
    # Keeps track of the current session user mode
    #
    # In order to perform administrative tasks over some interfaces,
    # an administrator must have explicitly enabled admin-mode
    # e.g. on web access require re-authentication
    class CurrentUserMode
      NotRequestedError = Class.new(StandardError)

      # RequestStore entries
      CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY = { res: :current_user_mode, data: :bypass_session_admin_id }.freeze
      CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY =         { res: :current_user_mode, data: :current_admin }.freeze

      # SessionStore entries
      SESSION_STORE_KEY = :current_user_mode
      ADMIN_MODE_START_TIME_KEY = :admin_mode
      ADMIN_MODE_REQUESTED_TIME_KEY = :admin_mode_requested
      MAX_ADMIN_MODE_TIME = 6.hours
      ADMIN_MODE_REQUESTED_GRACE_PERIOD = 5.minutes

      class << self
        # Admin mode activation requires storing a flag in the user session. Using this
        # method when scheduling jobs in sessionless environments (e.g. Sidekiq, API)
        # will bypass the session check for a user that was already in admin mode
        #
        # If passed a block, it will surround the block execution and reset the session
        # bypass at the end; otherwise use manually '.reset_bypass_session!'
        def bypass_session!(admin_id)
          Gitlab::SafeRequestStore[CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY] = admin_id

          Gitlab::AppLogger.debug("Bypassing session in admin mode for: #{admin_id}")

          if block_given?
            begin
              yield
            ensure
              reset_bypass_session!
            end
          end
        end

        def reset_bypass_session!
          Gitlab::SafeRequestStore.delete(CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY)
        end

        def bypass_session_admin_id
          Gitlab::SafeRequestStore[CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY]
        end

        # Store in the current request the provided user model (only if in admin mode)
        # and yield
        def with_current_admin(admin)
          return yield unless self.new(admin).admin_mode?

          Gitlab::SafeRequestStore[CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY] = admin

          Gitlab::AppLogger.debug("Admin mode active for: #{admin.username}")

          yield
        ensure
          Gitlab::SafeRequestStore.delete(CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY)
        end

        def current_admin
          Gitlab::SafeRequestStore[CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY]
        end
      end

      def initialize(user)
        @user = user
      end

      def admin_mode?
        return false unless user

        Gitlab::SafeRequestStore.fetch(admin_mode_rs_key) do
          user.admin? && (privileged_runtime? || session_with_admin_mode?)
        end
      end

      def admin_mode_requested?
        return false unless user

        Gitlab::SafeRequestStore.fetch(admin_mode_requested_rs_key) do
          user.admin? && admin_mode_requested_in_grace_period?
        end
      end

      def enable_admin_mode!(password: nil, skip_password_validation: false)
        return unless user&.admin?
        return unless skip_password_validation || user&.valid_password?(password)

        raise NotRequestedError unless admin_mode_requested?

        reset_request_store_cache_entries

        current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
        current_session_data[ADMIN_MODE_START_TIME_KEY] = Time.now
      end

      def disable_admin_mode!
        return unless user&.admin?

        reset_request_store_cache_entries

        current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
        current_session_data[ADMIN_MODE_START_TIME_KEY] = nil
      end

      def request_admin_mode!
        return unless user&.admin?

        reset_request_store_cache_entries

        current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = Time.now
      end

      private

      attr_reader :user

      # RequestStore entry to cache #admin_mode? result
      def admin_mode_rs_key
        @admin_mode_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode? }
      end

      # RequestStore entry to cache #admin_mode_requested? result
      def admin_mode_requested_rs_key
        @admin_mode_requested_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode_requested? }
      end

      def current_session_data
        @current_session ||= Gitlab::NamespacedSessionStore.new(SESSION_STORE_KEY)
      end

      def session_with_admin_mode?
        return true if bypass_session?

        current_session_data.initiated? && current_session_data[ADMIN_MODE_START_TIME_KEY].to_i > MAX_ADMIN_MODE_TIME.ago.to_i
      end

      def admin_mode_requested_in_grace_period?
        current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY].to_i > ADMIN_MODE_REQUESTED_GRACE_PERIOD.ago.to_i
      end

      def bypass_session?
        user&.id && user.id == self.class.bypass_session_admin_id
      end

      def reset_request_store_cache_entries
        Gitlab::SafeRequestStore.delete(admin_mode_rs_key)
        Gitlab::SafeRequestStore.delete(admin_mode_requested_rs_key)
      end

      # Runtimes which imply shell access get admin mode automatically, see Gitlab::Runtime
      def privileged_runtime?
        Gitlab::Runtime.rake? || Gitlab::Runtime.rails_runner? || Gitlab::Runtime.console?
      end
    end
  end
end