# 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' 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.css(METRICS_CSS_CLASS) 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] 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 ) ] 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] 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] 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] 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] def unique_project_paths embeds_by_node.values.map(&:project_path).uniq end end end end Banzai::Filter::InlineMetricsRedactorFilter.prepend_if_ee('EE::Banzai::Filter::InlineMetricsRedactorFilter')