Welcome to mirror list, hosted at ThFree Co, Russian Federation.

auditor.rb « audit « gitlab « lib - gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: e3d2b394404ff06c968043217082bc95fafc8903 (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
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
# frozen_string_literal: true

module Gitlab
  module Audit
    class Auditor
      attr_reader :scope, :name

      PERMITTED_TARGET_CLASSES = [
        ::Operations::FeatureFlag
      ].freeze

      # Record audit events
      #
      # @param [Hash] context
      # @option context [String] :name the operation name to be audited, used for error tracking
      # @option context [User] :author the user who authors the change
      # @option context [User, Project, Group] :scope the scope which audit event belongs to
      # @option context [Object] :target the target object being audited
      # @option context [String] :message the message describing the action
      # @option context [Hash] :additional_details the additional details we want to merge into audit event details.
      # @option context [Time] :created_at the time that the event occurred (defaults to the current time)
      #
      # @example Using block (useful when events are emitted deep in the call stack)
      #   i.e. multiple audit events
      #
      #   audit_context = {
      #     name: 'merge_approval_rule_updated',
      #     author: current_user,
      #     scope: project_alpha,
      #     target: merge_approval_rule,
      #     message: 'a user has attempted to update an approval rule'
      #   }
      #
      #   # in the initiating service
      #   Gitlab::Audit::Auditor.audit(audit_context) do
      #     service.execute
      #   end
      #
      #   # in the model
      #   Auditable.push_audit_event('an approver has been added')
      #   Auditable.push_audit_event('an approval group has been removed')
      #
      # @example Using standard method call
      #   i.e. single audit event
      #
      #   merge_approval_rule.save
      #   Gitlab::Audit::Auditor.audit(audit_context)
      #
      # @return result of block execution
      def self.audit(context, &block)
        auditor = new(context)

        return unless auditor.audit_enabled?

        if block
          auditor.multiple_audit(&block)
        else
          auditor.single_audit
        end
      end

      def initialize(context = {})
        @context = context

        @name = @context.fetch(:name, 'audit_operation')
        @is_audit_event_yaml_defined = Gitlab::Audit::Type::Definition.defined?(@name)
        @stream_only = stream_only?
        @author = @context.fetch(:author)
        @scope = @context.fetch(:scope)
        @target = @context.fetch(:target)
        @created_at = @context.fetch(:created_at, DateTime.current)
        @message = @context.fetch(:message, '')
        @additional_details = @context.fetch(:additional_details, {})
        @ip_address = @context[:ip_address]
        @target_details = @context[:target_details]
        @authentication_event = @context.fetch(:authentication_event, false)
        @authentication_provider = @context[:authentication_provider]

        # TODO: Remove this code once we close https://gitlab.com/gitlab-org/gitlab/-/issues/367870
        return unless @is_audit_event_yaml_defined

        # rubocop:disable Gitlab/RailsLogger
        Rails.logger.warn('WARNING: Logging audit events without an event type definition will be deprecated soon.')
        Rails.logger.warn('See https://docs.gitlab.com/ee/development/audit_event_guide/#event-type-definitions')
        # rubocop:enable Gitlab/RailsLogger
      end

      def single_audit
        events = [build_event(@message)]

        record(events)
      end

      def multiple_audit
        # For now we dont have any need to implement multiple audit event functionality in CE
        # Defined in EE
      end

      def record(events)
        @stream_only ? send_to_stream(events) : log_events_and_stream(events)
      end

      def log_events_and_stream(events)
        log_authentication_event
        saved_events = log_to_database(events)

        # we only want to override events with saved_events when it successfully saves into database.
        # we are doing so to ensure events in memory reflects events saved in database and have id column.
        events = saved_events if saved_events.present?

        log_to_file_and_stream(events)
      end

      def log_to_file_and_stream(events)
        log_to_file(events)
        send_to_stream(events)
      end

      def audit_enabled?
        authentication_event? || permitted_target?
      end

      def permitted_target?
        @target.class.in? PERMITTED_TARGET_CLASSES
      end

      def authentication_event?
        @authentication_event
      end

      def stream_only?
        if @is_audit_event_yaml_defined
          Gitlab::Audit::Type::Definition.stream_only?(@name)
        else
          @context.fetch(:stream_only, false)
        end
      end

      def log_authentication_event
        return unless Gitlab::Database.read_write? && authentication_event?

        event = AuthenticationEvent.new(authentication_event_payload)
        event.save!
      rescue ActiveRecord::RecordInvalid => e
        ::Gitlab::ErrorTracking.track_exception(e, audit_operation: @name)
      end

      def authentication_event_payload
        {
          # @author can be a User or various Gitlab::Audit authors.
          # Only capture real users for successful authentication events.
          user: author_if_user,
          user_name: @author.name,
          ip_address: Gitlab::RequestContext.instance.client_ip || @author.current_sign_in_ip,
          result: AuthenticationEvent.results[:success],
          provider: @authentication_provider
        }
      end

      def author_if_user
        @author if @author.is_a?(User)
      end

      def send_to_stream(events)
        # Defined in EE
      end

      def build_event(message)
        AuditEvents::BuildService.new(
          author: @author,
          scope: @scope,
          target: @target,
          created_at: @created_at,
          message: message,
          additional_details: @additional_details,
          ip_address: @ip_address,
          target_details: @target_details
        ).execute
      end

      def log_to_database(events)
        if events.one?
          events.first.save!
          events
        else
          event_ids = AuditEvent.bulk_insert!(events, returns: :ids)
          AuditEvent.id_in(event_ids)
        end
      rescue ActiveRecord::RecordInvalid => e
        ::Gitlab::ErrorTracking.track_exception(e, audit_operation: @name)
      end

      def log_to_file(events)
        file_logger = ::Gitlab::AuditJsonLogger.build

        events.each { |event| file_logger.info(log_payload(event)) }
      end

      private

      def log_payload(event)
        payload = event.as_json
        details = formatted_details(event.details)
        payload["details"] = details
        payload.merge!(details).as_json
      end

      def formatted_details(details)
        details.merge(details.slice(:from, :to).transform_values(&:to_s))
      end
    end
  end
end

Gitlab::Audit::Auditor.prepend_mod_with("Gitlab::Audit::Auditor")