summaryrefslogtreecommitdiff
path: root/lib/gitlab/api_authentication/token_resolver.rb
blob: afada05592857d5fa5d6588323f34dc8cd47fbe5 (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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
# frozen_string_literal: true

module Gitlab
  module APIAuthentication
    class TokenResolver
      include ActiveModel::Validations

      attr_reader :token_type

      validates :token_type, inclusion: {
        in: %i[
          personal_access_token_with_username
          job_token_with_username
          deploy_token_with_username
          personal_access_token
          job_token
          deploy_token
          personal_access_token_from_jwt
          deploy_token_from_jwt
          job_token_from_jwt
        ]
      }

      UsernameAndPassword = ::Gitlab::APIAuthentication::TokenLocator::UsernameAndPassword

      def initialize(token_type)
        @token_type = token_type
        validate!
      end

      # Existing behavior is known to be inconsistent across authentication
      # methods with regards to whether to silently ignore present but invalid
      # credentials or to raise an error/respond with 401.
      #
      # If a token can be located from the provided credentials, but the token
      # or credentials are in some way invalid, this implementation opts to
      # raise an error.
      #
      # For example, if the raw credentials include a username and password, and
      # a token is resolved from the password, but the username does not match
      # the token, an error will be raised.
      #
      # See https://gitlab.com/gitlab-org/gitlab/-/issues/246569

      def resolve(raw)
        case @token_type
        when :personal_access_token
          resolve_personal_access_token raw

        when :job_token
          resolve_job_token raw

        when :deploy_token
          resolve_deploy_token raw

        when :personal_access_token_with_username
          resolve_personal_access_token_with_username raw

        when :job_token_with_username
          resolve_job_token_with_username raw

        when :deploy_token_with_username
          resolve_deploy_token_with_username raw

        when :personal_access_token_from_jwt
          resolve_personal_access_token_from_jwt raw

        when :deploy_token_from_jwt
          resolve_deploy_token_from_jwt raw

        when :job_token_from_jwt
          resolve_job_token_from_jwt raw
        end
      end

      private

      def resolve_personal_access_token_with_username(raw)
        raise ::Gitlab::Auth::UnauthorizedError unless raw.username

        with_personal_access_token(raw) do |pat|
          break unless pat

          # Ensure that the username matches the token. This check is a subtle
          # departure from the existing behavior of #find_personal_access_token_from_http_basic_auth.
          # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38627#note_435907856
          raise ::Gitlab::Auth::UnauthorizedError unless pat.user.username == raw.username

          pat
        end
      end

      def resolve_job_token_with_username(raw)
        # Only look for a job if the username is correct
        return if ::Gitlab::Auth::CI_JOB_USER != raw.username

        with_job_token(raw) do |job|
          job
        end
      end

      def resolve_deploy_token_with_username(raw)
        with_deploy_token(raw) do |token|
          break unless token

          # Ensure that the username matches the token. This check is a subtle
          # departure from the existing behavior of #deploy_token_from_request.
          # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38627#note_474826205
          raise ::Gitlab::Auth::UnauthorizedError unless token.username == raw.username

          token
        end
      end

      def resolve_personal_access_token(raw)
        with_personal_access_token(raw) do |pat|
          pat
        end
      end

      def resolve_job_token(raw)
        with_job_token(raw) do |job|
          job
        end
      end

      def resolve_deploy_token(raw)
        with_deploy_token(raw) do |token|
          token
        end
      end

      def resolve_personal_access_token_from_jwt(raw)
        with_jwt_token(raw) do |jwt_token|
          break unless jwt_token['token'].is_a?(Integer)

          pat = ::PersonalAccessToken.find(jwt_token['token'])
          break unless pat

          pat
        end
      end

      def resolve_deploy_token_from_jwt(raw)
        with_jwt_token(raw) do |jwt_token|
          break unless jwt_token['token'].is_a?(String)

          resolve_deploy_token(UsernameAndPassword.new(nil, jwt_token['token']))
        end
      end

      def resolve_job_token_from_jwt(raw)
        with_jwt_token(raw) do |jwt_token|
          break unless jwt_token['token'].is_a?(String)

          resolve_job_token(UsernameAndPassword.new(nil, jwt_token['token']))
        end
      end

      def with_personal_access_token(raw, &block)
        pat = ::PersonalAccessToken.find_by_token(raw.password)
        return unless pat

        yield(pat)
      end

      def with_deploy_token(raw, &block)
        raise ::Gitlab::Auth::UnauthorizedError if Gitlab::ExternalAuthorization.enabled?

        token = ::DeployToken.active.find_by_token(raw.password)
        return unless token

        yield(token)
      end

      def with_job_token(raw, &block)
        job = ::Ci::AuthJobFinder.new(token: raw.password).execute
        raise ::Gitlab::Auth::UnauthorizedError unless job

        yield(job)
      end

      def with_jwt_token(raw, &block)
        jwt_token = ::Gitlab::JWTToken.decode(raw.password)
        raise ::Gitlab::Auth::UnauthorizedError unless jwt_token

        yield(jwt_token)
      end
    end
  end
end