base.rb 9.8 KB
Newer Older
1 2 3 4 5
# frozen_string_literal: true

module API
  # Internal access API
  module Internal
6
    class Base < Grape::API::Instance
7 8
      before { authenticate_by_gitlab_shell_token! }

9 10 11
      before do
        Gitlab::ApplicationContext.push(
          user: -> { actor&.user },
12 13
          project: -> { project },
          caller_id: route.origin
14 15 16
        )
      end

17 18 19 20
      helpers ::API::Helpers::InternalHelpers

      UNKNOWN_CHECK_RESULT_ERROR = 'Unknown check result'.freeze

21 22 23 24
      VALID_PAT_SCOPES = Set.new(
        Gitlab::Auth::API_SCOPES + Gitlab::Auth::REPOSITORY_SCOPES + Gitlab::Auth::REGISTRY_SCOPES
      ).freeze

25 26 27 28 29 30 31 32 33 34 35
      helpers do
        def response_with_status(code: 200, success: true, message: nil, **extra_options)
          status code
          { status: success, message: message }.merge(extra_options).compact
        end

        def lfs_authentication_url(project)
          # This is a separate method so that EE can alter its behaviour more
          # easily.
          project.http_url_to_repo
        end
36

37 38 39 40
        def check_allowed(params)
          # This is a separate method so that EE can alter its behaviour more
          # easily.

41 42
          # Stores some Git-specific env thread-safely
          env = parse_env
43
          Gitlab::Git::HookEnv.set(gl_repository, env) if container
44

45
          actor.update_last_used_at!
46 47

          check_result = begin
48
                           access_check!(actor, params)
49
                         rescue Gitlab::GitAccess::ForbiddenError => e
50 51 52 53 54
                           # The return code needs to be 401. If we return 403
                           # the custom message we return won't be shown to the user
                           # and, instead, the default message 'GitLab: API is not accessible'
                           # will be displayed
                           return response_with_status(code: 401, success: false, message: e.message)
55
                         rescue Gitlab::GitAccess::TimeoutError => e
56
                           return response_with_status(code: 503, success: false, message: e.message)
57
                         rescue Gitlab::GitAccess::NotFoundError => e
58
                           return response_with_status(code: 404, success: false, message: e.message)
59 60
                         end

61
          log_user_activity(actor.user)
62 63 64 65 66

          case check_result
          when ::Gitlab::GitAccessResult::Success
            payload = {
              gl_repository: gl_repository,
67
              gl_project_path: gl_repository_path,
68 69
              gl_id: Gitlab::GlId.gl_id(actor.user),
              gl_username: actor.username,
70 71
              git_config_options: ["uploadpack.allowFilter=true",
                                   "uploadpack.allowAnySHA1InWant=true"],
72 73
              gitaly: gitaly_payload(params[:action]),
              gl_console_messages: check_result.console_messages
74
            }.merge!(actor.key_details)
75 76

            # Custom option for git-receive-pack command
77

78
            receive_max_input_size = Gitlab::CurrentSettings.receive_max_input_size.to_i
79

80 81 82 83 84 85
            if receive_max_input_size > 0
              payload[:git_config_options] << "receive.maxInputSize=#{receive_max_input_size.megabytes}"
            end

            response_with_status(**payload)
          when ::Gitlab::GitAccessResult::CustomAction
86
            response_with_status(code: 300, payload: check_result.payload, gl_console_messages: check_result.console_messages)
87 88 89 90
          else
            response_with_status(code: 500, success: false, message: UNKNOWN_CHECK_RESULT_ERROR)
          end
        end
91 92 93 94 95 96 97 98

        def access_check!(actor, params)
          access_checker = access_checker_for(actor, params[:protocol])
          access_checker.check(params[:action], params[:changes]).tap do |result|
            break result if @project || !repo_type.project?

            # If we have created a project directly from a git push
            # we have to assign its value to both @project and @container
99
            @project = @container = access_checker.container
100 101
          end
        end
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
      end

      namespace 'internal' do
        # Check if git command is allowed for project
        #
        # Params:
        #   key_id - ssh key id for Git over SSH
        #   user_id - user id for Git over HTTP or over SSH in keyless SSH CERT mode
        #   username - user name for Git over SSH in keyless SSH cert mode
        #   protocol - Git access protocol being used, e.g. HTTP or SSH
        #   project - project full_path (not path on disk)
        #   action - git action (git-upload-pack or git-receive-pack)
        #   changes - changes as "oldrev newrev ref", see Gitlab::ChangesList
        #   check_ip - optional, only in EE version, may limit access to
        #     group resources based on its IP restrictions
        post "/allowed" do
          # It was moved to a separate method so that EE can alter its behaviour more
          # easily.
          check_allowed(params)
        end
122 123 124 125

        post "/lfs_authenticate" do
          status 200

126 127
          unless actor.key_or_user
            raise ActiveRecord::RecordNotFound.new('User not found!')
128 129
          end

130 131
          actor.update_last_used_at!

132
          Gitlab::LfsToken
133
            .new(actor.key_or_user)
134 135 136 137 138 139 140
            .authentication_payload(lfs_authentication_url(project))
        end

        #
        # Get a ssh key using the fingerprint
        #
        # rubocop: disable CodeReuse/ActiveRecord
141
        get '/authorized_keys' do
142 143 144 145
          fingerprint = params.fetch(:fingerprint) do
            Gitlab::InsecureKeyFingerprint.new(params.fetch(:key)).fingerprint
          end
          key = Key.find_by(fingerprint: fingerprint)
146
          not_found!('Key') if key.nil?
147 148 149 150 151 152 153
          present key, with: Entities::SSHKey
        end
        # rubocop: enable CodeReuse/ActiveRecord

        #
        # Discover user by ssh key, user id or username
        #
154
        get '/discover' do
155
          present actor.user, with: Entities::UserSafe
156 157
        end

158
        get '/check' do
159 160 161 162 163 164 165 166 167 168
          {
            api_version: API.version,
            gitlab_version: Gitlab::VERSION,
            gitlab_rev: Gitlab.revision,
            redis: redis_ping
          }
        end
        post '/two_factor_recovery_codes' do
          status 200

169 170
          actor.update_last_used_at!
          user = actor.user
171

172 173 174
          if params[:key_id]
            unless actor.key
              break { success: false, message: 'Could not find the given key' }
175 176
            end

177
            if actor.key.is_a?(DeployKey)
178 179 180 181 182 183
              break { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' }
            end

            unless user
              break { success: false, message: 'Could not find a user for the given key' }
            end
184 185
          elsif params[:user_id] && user.nil?
            break { success: false, message: 'Could not find the given user' }
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200
          end

          unless user.two_factor_enabled?
            break { success: false, message: 'Two-factor authentication is not enabled for this user' }
          end

          codes = nil

          ::Users::UpdateService.new(current_user, user: user).execute! do |user|
            codes = user.generate_otp_backup_codes!
          end

          { success: true, recovery_codes: codes }
        end

201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
        post '/personal_access_token' do
          status 200

          actor.update_last_used_at!
          user = actor.user

          if params[:key_id]
            unless actor.key
              break { success: false, message: 'Could not find the given key' }
            end

            if actor.key.is_a?(DeployKey)
              break { success: false, message: 'Deploy keys cannot be used to create personal access tokens' }
            end

            unless user
              break { success: false, message: 'Could not find a user for the given key' }
            end
          elsif params[:user_id] && user.nil?
            break { success: false, message: 'Could not find the given user' }
          end

          if params[:name].blank?
            break { success: false, message: "No token name specified" }
          end

          if params[:scopes].blank?
            break { success: false, message: "No token scopes specified" }
          end

          invalid_scope = params[:scopes].find { |scope| VALID_PAT_SCOPES.exclude?(scope.to_sym) }

          if invalid_scope
            valid_scopes = VALID_PAT_SCOPES.map(&:to_s).sort
            break { success: false, message: "Invalid scope: '#{invalid_scope}'. Valid scopes are: #{valid_scopes}" }
          end

          begin
            expires_at = params[:expires_at].presence && Date.parse(params[:expires_at])
          rescue ArgumentError
            break { success: false, message: "Invalid token expiry date: '#{params[:expires_at]}'" }
          end

244 245 246
          result = ::PersonalAccessTokens::CreateService.new(
            user, name: params[:name], scopes: params[:scopes], expires_at: expires_at
          ).execute
247

248 249
          unless result.status == :success
            break { success: false, message: "Failed to create token: #{result.message}" }
250 251
          end

252 253
          access_token = result.payload[:personal_access_token]

254 255 256
          { success: true, token: access_token.token, scopes: access_token.scopes, expires_at: access_token.expires_at }
        end

257 258 259 260 261 262 263 264 265 266 267
        post '/pre_receive' do
          status 200

          reference_counter_increased = Gitlab::ReferenceCounter.new(params[:gl_repository]).increase

          { reference_counter_increased: reference_counter_increased }
        end

        post '/post_receive' do
          status 200

268
          response = PostReceiveService.new(actor.user, repository, project, params).execute
269 270 271 272 273 274 275

          present response, with: Entities::InternalPostReceive::Response
        end
      end
    end
  end
end
276 277

API::Internal::Base.prepend_if_ee('EE::API::Internal::Base')