summaryrefslogtreecommitdiff
path: root/lib/banzai/filter/inline_metrics_redactor_filter.rb
blob: ff91be2cbb74ec6c0e99d117885d79c928e3dc19 (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
# frozen_string_literal: true

module Banzai
  module Filter
    # HTML filter that removes embeded elements that the current user does
    # not have permission to view.
    class InlineMetricsRedactorFilter < HTML::Pipeline::Filter
      include Gitlab::Utils::StrongMemoize

      METRICS_CSS_CLASS = '.js-render-metrics'

      # Finds all embeds based on the css class the FE
      # uses to identify the embedded content, removing
      # only unnecessary nodes.
      def call
        return doc unless Feature.enabled?(:gfm_embedded_metrics, context[:project])

        nodes.each do |node|
          path = paths_by_node[node]
          user_has_access = user_access_by_path[path]

          node.remove unless user_has_access
        end

        doc
      end

      private

      def user
        context[:current_user]
      end

      # Returns all nodes which the FE will identify as
      # a metrics dashboard placeholder element
      #
      # @return [Nokogiri::XML::NodeSet]
      def nodes
        @nodes ||= doc.css(METRICS_CSS_CLASS)
      end

      # Maps a node to the full path of a project.
      # Memoized so we only need to run the regex to get
      # the project full path from the url once per node.
      #
      # @return [Hash<Nokogiri::XML::Node, String>]
      def paths_by_node
        strong_memoize(:paths_by_node) do
          nodes.each_with_object({}) do |node, paths|
            paths[node] = path_for_node(node)
          end
        end
      end

      # Gets a project's full_path from the dashboard url
      # in the placeholder node. The FE will use the attr
      # `data-dashboard-url`, so we want to check against that
      # attribute directly in case a user has manually
      # created a metrics element (rather than supporting
      # an alternate attr in InlineMetricsFilter).
      #
      # @return [String]
      def path_for_node(node)
        url = node.attribute('data-dashboard-url').to_s

        Gitlab::Metrics::Dashboard::Url.regex.match(url) do |m|
          "#{$~[:namespace]}/#{$~[:project]}"
        end
      end

      # Maps a project's full path to a Project object.
      # Contains all of the Projects referenced in the
      # metrics placeholder elements of the current document
      #
      # @return [Hash<String, Project>]
      def projects_by_path
        strong_memoize(:projects_by_path) do
          Project.eager_load(:route, namespace: [:route])
            .where_full_path_in(paths_by_node.values.uniq)
            .index_by(&:full_path)
        end
      end

      # Returns a mapping representing whether the current user
      # has permission to view the metrics for the project.
      # Determined in a batch
      #
      # @return [Hash<Project, Boolean>]
      def user_access_by_path
        strong_memoize(:user_access_by_path) do
          projects_by_path.each_with_object({}) do |(path, project), access|
            access[path] = Ability.allowed?(user, :read_environment, project)
          end
        end
      end
    end
  end
end