summaryrefslogtreecommitdiff
path: root/lib/gitlab/ci/pipeline/chain/validate/external.rb
blob: 27bb7fdc05ae389783baeb6d0edb6bb66b43e991 (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
# frozen_string_literal: true

module Gitlab
  module Ci
    module Pipeline
      module Chain
        module Validate
          class External < Chain::Base
            include Chain::Helpers

            InvalidResponseCode = Class.new(StandardError)

            DEFAULT_VALIDATION_REQUEST_TIMEOUT = 5
            ACCEPTED_STATUS = 200
            REJECTED_STATUS = 406

            def perform!
              pipeline_authorized = validate_external

              log_message = pipeline_authorized ? 'authorized' : 'not authorized'
              Gitlab::AppLogger.info(message: "Pipeline #{log_message}", project_id: project.id, user_id: current_user.id)

              error('External validation failed', drop_reason: :external_validation_failure) unless pipeline_authorized
            end

            def break?
              pipeline.errors.any?
            end

            private

            def validate_external
              return true unless validation_service_url

              # 200 - accepted
              # 406 - rejected
              # everything else - accepted and logged
              response_code = validate_service_request.code
              case response_code
              when ACCEPTED_STATUS
                true
              when REJECTED_STATUS
                false
              else
                raise InvalidResponseCode, "Unsupported response code received from Validation Service: #{response_code}"
              end
            rescue StandardError => ex
              Gitlab::ErrorTracking.track_exception(ex, project_id: project.id)

              true
            end

            def validate_service_request
              headers = {
                'X-Gitlab-Correlation-id' => Labkit::Correlation::CorrelationId.current_id,
                'X-Gitlab-Token' => validation_service_token
              }.compact

              Gitlab::HTTP.post(
                validation_service_url, timeout: validation_service_timeout,
                headers: headers,
                body: validation_service_payload.to_json
              )
            end

            def validation_service_timeout
              timeout = Gitlab::CurrentSettings.external_pipeline_validation_service_timeout || ENV['EXTERNAL_VALIDATION_SERVICE_TIMEOUT'].to_i
              return timeout if timeout > 0

              DEFAULT_VALIDATION_REQUEST_TIMEOUT
            end

            def validation_service_url
              Gitlab::CurrentSettings.external_pipeline_validation_service_url || ENV['EXTERNAL_VALIDATION_SERVICE_URL']
            end

            def validation_service_token
              Gitlab::CurrentSettings.external_pipeline_validation_service_token || ENV['EXTERNAL_VALIDATION_SERVICE_TOKEN']
            end

            def validation_service_payload
              {
                project: {
                  id: project.id,
                  path: project.full_path,
                  created_at: project.created_at&.iso8601
                },
                user: {
                  id: current_user.id,
                  username: current_user.username,
                  email: current_user.email,
                  created_at: current_user.created_at&.iso8601,
                  current_sign_in_ip: current_user.current_sign_in_ip,
                  last_sign_in_ip: current_user.last_sign_in_ip
                },
                pipeline: {
                  sha: pipeline.sha,
                  ref: pipeline.ref,
                  type: pipeline.source
                },
                builds: builds_validation_payload
              }
            end

            def builds_validation_payload
              stages_attributes.flat_map { |stage| stage[:builds] }
                .map(&method(:build_validation_payload))
            end

            def build_validation_payload(build)
              {
                name: build[:name],
                stage: build[:stage],
                image: build.dig(:options, :image, :name),
                services: build.dig(:options, :services)&.map { |service| service[:name] },
                script: [
                  build.dig(:options, :before_script),
                  build.dig(:options, :script),
                  build.dig(:options, :after_script)
                ].flatten.compact
              }
            end

            def stages_attributes
              command.yaml_processor_result.stages_attributes
            end
          end
        end
      end
    end
  end
end

Gitlab::Ci::Pipeline::Chain::Validate::External.prepend_mod_with('Gitlab::Ci::Pipeline::Chain::Validate::External')