1# frozen_string_literal: true
2
3module Mattermost
4  class NoSessionError < ::Mattermost::Error
5    def message
6      'No session could be set up, is Mattermost configured with Single Sign On?'
7    end
8  end
9
10  ConnectionError = Class.new(::Mattermost::Error)
11
12  # This class' prime objective is to obtain a session token on a Mattermost
13  # instance with SSO configured where this GitLab instance is the provider.
14  #
15  # The process depends on OAuth, but skips a step in the authentication cycle.
16  # For example, usually a user would click the 'login in GitLab' button on
17  # Mattermost, which would yield a 302 status code and redirects you to GitLab
18  # to approve the use of your account on Mattermost. Which would trigger a
19  # callback so Mattermost knows this request is approved and gets the required
20  # data to create the user account etc.
21  #
22  # This class however skips the button click, and also the approval phase to
23  # speed up the process and keep it without manual action and get a session
24  # going.
25  class Session
26    include Doorkeeper::Helpers::Controller
27
28    LEASE_TIMEOUT = 60
29
30    attr_accessor :current_resource_owner, :token, :base_uri
31
32    def initialize(current_user)
33      @current_resource_owner = current_user
34      @base_uri = Settings.mattermost.host
35    end
36
37    def with_session
38      with_lease do
39        create
40
41        begin
42          yield self
43        rescue Errno::ECONNREFUSED => e
44          Gitlab::AppLogger.error(e.message + "\n" + e.backtrace.join("\n"))
45          raise ::Mattermost::NoSessionError
46        ensure
47          destroy
48        end
49      end
50    end
51
52    # Next methods are needed for Doorkeeper
53    def pre_auth
54      @pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new(
55        Doorkeeper.configuration, params)
56    end
57
58    def authorization
59      @authorization ||= strategy.request
60    end
61
62    def strategy
63      @strategy ||= server.authorization_request(pre_auth.response_type)
64    end
65
66    def request
67      @request ||= OpenStruct.new(parameters: params)
68    end
69
70    def params
71      Rack::Utils.parse_query(oauth_uri.query).symbolize_keys
72    end
73
74    def get(path, options = {})
75      handle_exceptions do
76        Gitlab::HTTP.get(path, build_options(options))
77      end
78    end
79
80    def post(path, options = {})
81      handle_exceptions do
82        Gitlab::HTTP.post(path, build_options(options))
83      end
84    end
85
86    def delete(path, options = {})
87      handle_exceptions do
88        Gitlab::HTTP.delete(path, build_options(options))
89      end
90    end
91
92    private
93
94    def build_options(options)
95      options.tap do |hash|
96        hash[:headers] = @headers
97        hash[:allow_local_requests] = true
98        hash[:base_uri] = base_uri if base_uri.presence
99      end
100    end
101
102    def create
103      raise ::Mattermost::NoSessionError unless oauth_uri
104      raise ::Mattermost::NoSessionError unless token_uri
105
106      @token = request_token
107      raise ::Mattermost::NoSessionError unless @token
108
109      @headers = {
110        Authorization: "Bearer #{@token}"
111      }
112
113      @token
114    end
115
116    def destroy
117      post('/api/v4/users/logout')
118    end
119
120    def oauth_uri
121      return @oauth_uri if defined?(@oauth_uri)
122
123      @oauth_uri = nil
124
125      response = get('/oauth/gitlab/login', follow_redirects: false)
126      return unless (300...400) === response.code
127
128      redirect_uri = response.headers['location']
129      return unless redirect_uri
130
131      oauth_cookie = parse_cookie(response)
132      @headers = {
133        Cookie: oauth_cookie.to_cookie_string
134      }
135
136      @oauth_uri = URI.parse(redirect_uri)
137    end
138
139    def token_uri
140      @token_uri ||=
141        if oauth_uri
142          authorization.authorize.redirect_uri if pre_auth.authorizable?
143        end
144    end
145
146    def request_token
147      response = get(token_uri, follow_redirects: false)
148
149      if (200...400) === response.code
150        response.headers['token']
151      end
152    end
153
154    def with_lease
155      lease_uuid = lease_try_obtain
156      raise NoSessionError unless lease_uuid
157
158      begin
159        yield
160      ensure
161        Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid)
162      end
163    end
164
165    def lease_key
166      "mattermost:session"
167    end
168
169    def lease_try_obtain
170      lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
171      lease.try_obtain
172    end
173
174    def handle_exceptions
175      yield
176    rescue Gitlab::HTTP::Error => e
177      raise ::Mattermost::ConnectionError, e.message
178    rescue Errno::ECONNREFUSED => e
179      raise ::Mattermost::ConnectionError, e.message
180    end
181
182    def parse_cookie(response)
183      cookie_hash = Gitlab::HTTP::CookieHash.new
184      response.get_fields('Set-Cookie').each { |c| cookie_hash.add_cookies(c) }
185      cookie_hash
186    end
187  end
188end
189