summaryrefslogtreecommitdiff
path: root/lib/banzai/filter/inline_metrics_redactor_filter.rb
blob: b256815ae847b516d497f08bc652b7e22c4f99ea (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
# 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'
      XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(METRICS_CSS_CLASS).freeze
      EMBED_LIMIT = 100

      Route = Struct.new(:regex, :permission)
      Embed = Struct.new(:project_path, :permission)

      # Finds all embeds based on the css class the FE
      # uses to identify the embedded content, removing
      # only unnecessary nodes.
      def call
        nodes.each do |node|
          embed = embeds_by_node[node]
          user_has_access = user_access_by_embed[embed]

          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 embed placeholder element
      #
      # Removes any nodes beyond the first 100
      #
      # @return [Nokogiri::XML::NodeSet]
      def nodes
        strong_memoize(:nodes) do
          nodes = doc.xpath(XPATH)
          nodes.drop(EMBED_LIMIT).each(&:remove)

          nodes
        end
      end

      # Maps a node to key properties of an embed.
      # 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, Embed>]
      def embeds_by_node
        strong_memoize(:embeds_by_node) do
          nodes.each_with_object({}) do |node, embeds|
            embed = Embed.new
            url = node.attribute('data-dashboard-url').to_s

            permissions_by_route.each do |route|
              set_path_and_permission(embed, url, route.regex, route.permission) unless embed.permission
            end

            embeds[node] = embed if embed.permission
          end
        end
      end

      def permissions_by_route
        [
          Route.new(
            ::Gitlab::Metrics::Dashboard::Url.metrics_regex,
            :read_environment
          ),
          Route.new(
            ::Gitlab::Metrics::Dashboard::Url.grafana_regex,
            :read_project
          ),
          Route.new(
            ::Gitlab::Metrics::Dashboard::Url.clusters_regex,
            :read_cluster
          ),
          Route.new(
            ::Gitlab::Metrics::Dashboard::Url.alert_regex,
            :read_prometheus_alerts
          )
        ]
      end

      # Attempts to determine the path and permission attributes
      # of a url based on expected dashboard url formats and
      # sets the attributes on an Embed object
      #
      # @param embed [Embed]
      # @param url [String]
      # @param regex [RegExp]
      # @param permission [Symbol]
      def set_path_and_permission(embed, url, regex, permission)
        return unless path = regex.match(url) do |m|
          "#{$~[:namespace]}/#{$~[:project]}"
        end

        embed.project_path = path
        embed.permission = permission
      end

      # Returns a mapping representing whether the current user
      # has permission to view the embed for the project.
      # Determined in a batch
      #
      # @return [Hash<Embed, Boolean>]
      def user_access_by_embed
        strong_memoize(:user_access_by_embed) do
          unique_embeds.each_with_object({}) do |embed, access|
            project = projects_by_path[embed.project_path]

            access[embed] = Ability.allowed?(user, embed.permission, project)
          end
        end
      end

      # Returns a unique list of embeds
      #
      # @return [Array<Embed>]
      def unique_embeds
        embeds_by_node.values.uniq
      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(unique_project_paths)
            .index_by(&:full_path)
        end
      end

      # Returns a list of the full_paths of every project which
      # has an embed in the doc
      #
      # @return [Array<String>]
      def unique_project_paths
        embeds_by_node.values.map(&:project_path).uniq
      end
    end
  end
end