Commit 15c040a6 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/security/gitlab@13-12-stable-ee

parent 6e4e4023
...@@ -80,6 +80,10 @@ module PolicyActor ...@@ -80,6 +80,10 @@ module PolicyActor
def can_read_all_resources? def can_read_all_resources?
false false
end end
def password_expired?
false
end
end end
PolicyActor.prepend_mod_with('PolicyActor') PolicyActor.prepend_mod_with('PolicyActor')
...@@ -15,6 +15,10 @@ class GlobalPolicy < BasePolicy ...@@ -15,6 +15,10 @@ class GlobalPolicy < BasePolicy
@user&.required_terms_not_accepted? @user&.required_terms_not_accepted?
end end
condition(:password_expired, scope: :user) do
@user&.password_expired?
end
condition(:project_bot, scope: :user) { @user&.project_bot? } condition(:project_bot, scope: :user) { @user&.project_bot? }
condition(:migration_bot, scope: :user) { @user&.migration_bot? } condition(:migration_bot, scope: :user) { @user&.migration_bot? }
...@@ -73,6 +77,12 @@ class GlobalPolicy < BasePolicy ...@@ -73,6 +77,12 @@ class GlobalPolicy < BasePolicy
prevent :access_git prevent :access_git
end end
rule { password_expired }.policy do
prevent :access_api
prevent :access_git
prevent :use_slash_commands
end
rule { can_create_group }.policy do rule { can_create_group }.policy do
enable :create_group enable :create_group
end end
......
...@@ -40,7 +40,9 @@ class MemberEntity < Grape::Entity ...@@ -40,7 +40,9 @@ class MemberEntity < Grape::Entity
expose :valid_level_roles, as: :valid_roles expose :valid_level_roles, as: :valid_roles
expose :user, if: -> (member) { member.user.present? }, using: MemberUserEntity expose :user, if: -> (member) { member.user.present? } do |member, options|
MemberUserEntity.represent(member.user, source: options[:source])
end
expose :invite, if: -> (member) { member.invite? } do expose :invite, if: -> (member) { member.invite? } do
expose :email do |member| expose :email do |member|
......
...@@ -18,6 +18,7 @@ unless Gitlab::Runtime.sidekiq? ...@@ -18,6 +18,7 @@ unless Gitlab::Runtime.sidekiq?
data[:db_duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:db)) if data[:db] data[:db_duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:db)) if data[:db]
data[:view_duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:view)) if data[:view] data[:view_duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:view)) if data[:view]
data[:duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:duration)) if data[:duration] data[:duration_s] = Gitlab::Utils.ms_to_round_sec(data.delete(:duration)) if data[:duration]
data[:location] = Gitlab::Utils.removes_sensitive_data_from_url(data[:location]) if data[:location]
# Remove empty hashes to prevent type mismatches # Remove empty hashes to prevent type mismatches
# These are set to empty hashes in Lograge's ActionCable subscriber # These are set to empty hashes in Lograge's ActionCable subscriber
......
...@@ -6,10 +6,13 @@ module Banzai ...@@ -6,10 +6,13 @@ module Banzai
module Filter module Filter
# HTML filter that converts relative urls into absolute ones. # HTML filter that converts relative urls into absolute ones.
class AbsoluteLinkFilter < HTML::Pipeline::Filter class AbsoluteLinkFilter < HTML::Pipeline::Filter
CSS = 'a.gfm'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call def call
return doc unless context[:only_path] == false return doc unless context[:only_path] == false
doc.search('a.gfm').each do |el| doc.xpath(XPATH).each do |el|
process_link_attr el.attribute('href') process_link_attr el.attribute('href')
end end
......
...@@ -3,14 +3,20 @@ ...@@ -3,14 +3,20 @@
module Banzai module Banzai
module Filter module Filter
class AsciiDocPostProcessingFilter < HTML::Pipeline::Filter class AsciiDocPostProcessingFilter < HTML::Pipeline::Filter
CSS_MATH = '[data-math-style]'
XPATH_MATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_MATH).freeze
CSS_MERM = '[data-mermaid-style]'
XPATH_MERM = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_MERM).freeze
def call def call
doc.search('[data-math-style]').each do |node| doc.xpath(XPATH_MATH).each do |node|
node.set_attribute('class', 'code math js-render-math') node.set_attribute('class', 'code math js-render-math')
end end
doc.search('[data-mermaid-style]').each do |node| doc.xpath(XPATH_MERM).each do |node|
node.set_attribute('class', 'js-render-mermaid') node.set_attribute('class', 'js-render-mermaid')
end end
doc doc
end end
end end
......
...@@ -7,6 +7,9 @@ module Banzai ...@@ -7,6 +7,9 @@ module Banzai
class BaseRelativeLinkFilter < HTML::Pipeline::Filter class BaseRelativeLinkFilter < HTML::Pipeline::Filter
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
CSS = 'a:not(.gfm), img:not(.gfm), video:not(.gfm), audio:not(.gfm)'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
protected protected
def linkable_attributes def linkable_attributes
...@@ -41,7 +44,7 @@ module Banzai ...@@ -41,7 +44,7 @@ module Banzai
def fetch_linkable_attributes def fetch_linkable_attributes
attrs = [] attrs = []
attrs += doc.search('a:not(.gfm), img:not(.gfm), video:not(.gfm), audio:not(.gfm)').flat_map do |el| attrs += doc.xpath(XPATH).flat_map do |el|
[el.attribute('href'), el.attribute('src'), el.attribute('data-src')] [el.attribute('href'), el.attribute('src'), el.attribute('data-src')]
end end
......
...@@ -7,8 +7,11 @@ module Banzai ...@@ -7,8 +7,11 @@ module Banzai
class ColorFilter < HTML::Pipeline::Filter class ColorFilter < HTML::Pipeline::Filter
COLOR_CHIP_CLASS = 'gfm-color_chip' COLOR_CHIP_CLASS = 'gfm-color_chip'
CSS = 'code'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call def call
doc.css('code').each do |node| doc.xpath(XPATH).each do |node|
color = ColorParser.parse(node.content) color = ColorParser.parse(node.content)
node << color_chip(color) if color node << color_chip(color) if color
end end
......
...@@ -11,7 +11,7 @@ module Banzai ...@@ -11,7 +11,7 @@ module Banzai
return doc unless context[:project] return doc unless context[:project]
return doc unless Feature.enabled?(:custom_emoji, context[:project]) return doc unless Feature.enabled?(:custom_emoji, context[:project])
doc.search(".//text()").each do |node| doc.xpath('descendant-or-self::text()').each do |node|
content = node.to_html content = node.to_html
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
......
...@@ -11,7 +11,7 @@ module Banzai ...@@ -11,7 +11,7 @@ module Banzai
IGNORE_UNICODE_EMOJIS = %w(™ © ®).freeze IGNORE_UNICODE_EMOJIS = %w(™ © ®).freeze
def call def call
doc.search(".//text()").each do |node| doc.xpath('descendant-or-self::text()').each do |node|
content = node.to_html content = node.to_html
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
......
...@@ -23,17 +23,23 @@ module Banzai ...@@ -23,17 +23,23 @@ module Banzai
FOOTNOTE_LINK_REFERENCE_PATTERN = /\A#{FOOTNOTE_LINK_ID_PREFIX}\d+\z/.freeze FOOTNOTE_LINK_REFERENCE_PATTERN = /\A#{FOOTNOTE_LINK_ID_PREFIX}\d+\z/.freeze
FOOTNOTE_START_NUMBER = 1 FOOTNOTE_START_NUMBER = 1
CSS_SECTION = "ol > li[id=#{FOOTNOTE_ID_PREFIX}#{FOOTNOTE_START_NUMBER}]"
XPATH_SECTION = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_SECTION).freeze
CSS_FOOTNOTE = 'sup > a[id]'
XPATH_FOOTNOTE = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_FOOTNOTE).freeze
def call def call
return doc unless first_footnote = doc.at_css("ol > li[id=#{fn_id(FOOTNOTE_START_NUMBER)}]") return doc unless first_footnote = doc.at_xpath(XPATH_SECTION)
# Sanitization stripped off the section wrapper - add it back in # Sanitization stripped off the section wrapper - add it back in
first_footnote.parent.wrap('<section class="footnotes">') first_footnote.parent.wrap('<section class="footnotes">')
rand_suffix = "-#{random_number}" rand_suffix = "-#{random_number}"
modified_footnotes = {} modified_footnotes = {}
doc.css('sup > a[id]').each do |link_node| doc.xpath(XPATH_FOOTNOTE).each do |link_node|
ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX) ref_num = link_node[:id].delete_prefix(FOOTNOTE_LINK_ID_PREFIX)
footnote_node = doc.at_css("li[id=#{fn_id(ref_num)}]") node_xpath = Gitlab::Utils::Nokogiri.css_to_xpath("li[id=#{fn_id(ref_num)}]")
footnote_node = doc.at_xpath(node_xpath)
if INTEGER_PATTERN.match?(ref_num) && (footnote_node || modified_footnotes[ref_num]) if INTEGER_PATTERN.match?(ref_num) && (footnote_node || modified_footnotes[ref_num])
link_node[:href] += rand_suffix link_node[:href] += rand_suffix
......
...@@ -60,7 +60,7 @@ module Banzai ...@@ -60,7 +60,7 @@ module Banzai
IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
def call def call
doc.search(".//text()").each do |node| doc.xpath('descendant-or-self::text()').each do |node|
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
next unless node.content =~ TAGS_PATTERN next unless node.content =~ TAGS_PATTERN
......
...@@ -6,8 +6,11 @@ module Banzai ...@@ -6,8 +6,11 @@ module Banzai
# HTML filter that moves the value of image `src` attributes to `data-src` # HTML filter that moves the value of image `src` attributes to `data-src`
# so they can be lazy loaded. # so they can be lazy loaded.
class ImageLazyLoadFilter < HTML::Pipeline::Filter class ImageLazyLoadFilter < HTML::Pipeline::Filter
CSS = 'img'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call def call
doc.xpath('descendant-or-self::img').each do |img| doc.xpath(XPATH).each do |img|
img.add_class('lazy') img.add_class('lazy')
img['data-src'] = img['src'] img['data-src'] = img['src']
img['src'] = LazyImageTagHelper.placeholder_image img['src'] = LazyImageTagHelper.placeholder_image
......
...@@ -7,7 +7,7 @@ module Banzai ...@@ -7,7 +7,7 @@ module Banzai
IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
def call def call
doc.search(".//text()").each do |node| doc.xpath('descendant-or-self::text()').each do |node|
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
content = node.to_html content = node.to_html
......
...@@ -8,6 +8,7 @@ module Banzai ...@@ -8,6 +8,7 @@ module Banzai
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
METRICS_CSS_CLASS = '.js-render-metrics' METRICS_CSS_CLASS = '.js-render-metrics'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(METRICS_CSS_CLASS).freeze
EMBED_LIMIT = 100 EMBED_LIMIT = 100
Route = Struct.new(:regex, :permission) Route = Struct.new(:regex, :permission)
...@@ -41,7 +42,7 @@ module Banzai ...@@ -41,7 +42,7 @@ module Banzai
# @return [Nokogiri::XML::NodeSet] # @return [Nokogiri::XML::NodeSet]
def nodes def nodes
strong_memoize(:nodes) do strong_memoize(:nodes) do
nodes = doc.css(METRICS_CSS_CLASS) nodes = doc.xpath(XPATH)
nodes.drop(EMBED_LIMIT).each(&:remove) nodes.drop(EMBED_LIMIT).each(&:remove)
nodes nodes
......
...@@ -15,10 +15,11 @@ module Banzai ...@@ -15,10 +15,11 @@ module Banzai
.map { |diagram_type| %(pre[lang="#{diagram_type}"] > code) } .map { |diagram_type| %(pre[lang="#{diagram_type}"] > code) }
.join(', ') .join(', ')
return doc unless doc.at(diagram_selectors) xpath = Gitlab::Utils::Nokogiri.css_to_xpath(diagram_selectors)
return doc unless doc.at_xpath(xpath)
diagram_format = "svg" diagram_format = "svg"
doc.css(diagram_selectors).each do |node| doc.xpath(xpath).each do |node|
diagram_type = node.parent['lang'] diagram_type = node.parent['lang']
img_tag = Nokogiri::HTML::DocumentFragment.parse(%(<img src="#{create_image_src(diagram_type, diagram_format, node.content)}"/>)) img_tag = Nokogiri::HTML::DocumentFragment.parse(%(<img src="#{create_image_src(diagram_type, diagram_format, node.content)}"/>))
node.parent.replace(img_tag) node.parent.replace(img_tag)
......
...@@ -8,6 +8,11 @@ module Banzai ...@@ -8,6 +8,11 @@ module Banzai
NOT_LITERAL_REGEX = %r{#{LITERAL_KEYWORD}-((%5C|\\).+?)-#{LITERAL_KEYWORD}}.freeze NOT_LITERAL_REGEX = %r{#{LITERAL_KEYWORD}-((%5C|\\).+?)-#{LITERAL_KEYWORD}}.freeze
SPAN_REGEX = %r{<span>(.*?)</span>}.freeze SPAN_REGEX = %r{<span>(.*?)</span>}.freeze
CSS_A = 'a'
XPATH_A = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_A).freeze
CSS_CODE = 'code'
XPATH_CODE = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_CODE).freeze
def call def call
return doc unless result[:escaped_literals] return doc unless result[:escaped_literals]
...@@ -24,12 +29,12 @@ module Banzai ...@@ -24,12 +29,12 @@ module Banzai
# Banzai::Renderer::CommonMark::HTML. However, we eventually want to use # Banzai::Renderer::CommonMark::HTML. However, we eventually want to use
# the built-in compiled renderer, rather than the ruby version, for speed. # the built-in compiled renderer, rather than the ruby version, for speed.
# So let's do this work here. # So let's do this work here.
doc.css('a').each do |node| doc.xpath(XPATH_A).each do |node|
node.attributes['href'].value = node.attributes['href'].value.gsub(SPAN_REGEX, '\1') if node.attributes['href'] node.attributes['href'].value = node.attributes['href'].value.gsub(SPAN_REGEX, '\1') if node.attributes['href']
node.attributes['title'].value = node.attributes['title'].value.gsub(SPAN_REGEX, '\1') if node.attributes['title'] node.attributes['title'].value = node.attributes['title'].value.gsub(SPAN_REGEX, '\1') if node.attributes['title']
end end
doc.css('code').each do |node| doc.xpath(XPATH_CODE).each do |node|
node.attributes['lang'].value = node.attributes['lang'].value.gsub(SPAN_REGEX, '\1') if node.attributes['lang'] node.attributes['lang'].value = node.attributes['lang'].value.gsub(SPAN_REGEX, '\1') if node.attributes['lang']
end end
......
...@@ -10,6 +10,11 @@ module Banzai ...@@ -10,6 +10,11 @@ module Banzai
# HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$. # HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$.
# #
class MathFilter < HTML::Pipeline::Filter class MathFilter < HTML::Pipeline::Filter
CSS_MATH = 'pre.code.language-math'
XPATH_MATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_MATH).freeze
CSS_CODE = 'code'
XPATH_CODE = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_CODE).freeze
# Attribute indicating inline or display math. # Attribute indicating inline or display math.
STYLE_ATTRIBUTE = 'data-math-style' STYLE_ATTRIBUTE = 'data-math-style'
...@@ -21,7 +26,7 @@ module Banzai ...@@ -21,7 +26,7 @@ module Banzai
DOLLAR_SIGN = '$' DOLLAR_SIGN = '$'
def call def call
doc.css('code').each do |code| doc.xpath(XPATH_CODE).each do |code|
closing = code.next closing = code.next
opening = code.previous opening = code.previous
...@@ -39,7 +44,7 @@ module Banzai ...@@ -39,7 +44,7 @@ module Banzai
end end
end end
doc.css('pre.code.language-math').each do |el| doc.xpath(XPATH_MATH).each do |el|
el[STYLE_ATTRIBUTE] = 'display' el[STYLE_ATTRIBUTE] = 'display'
el[:class] += " #{TAG_CLASS}" el[:class] += " #{TAG_CLASS}"
end end
......
...@@ -4,8 +4,11 @@ ...@@ -4,8 +4,11 @@
module Banzai module Banzai
module Filter module Filter
class MermaidFilter < HTML::Pipeline::Filter class MermaidFilter < HTML::Pipeline::Filter
CSS = 'pre[lang="mermaid"] > code'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call def call
doc.css('pre[lang="mermaid"] > code').add_class('js-render-mermaid') doc.xpath(XPATH).add_class('js-render-mermaid')
doc doc
end end
......
...@@ -8,12 +8,15 @@ module Banzai ...@@ -8,12 +8,15 @@ module Banzai
# HTML that replaces all `code plantuml` tags with PlantUML img tags. # HTML that replaces all `code plantuml` tags with PlantUML img tags.
# #
class PlantumlFilter < HTML::Pipeline::Filter class PlantumlFilter < HTML::Pipeline::Filter
CSS = 'pre > code[lang="plantuml"]'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call def call
return doc unless settings.plantuml_enabled? && doc.at('pre > code[lang="plantuml"]') return doc unless settings.plantuml_enabled? && doc.at_xpath(XPATH)
plantuml_setup plantuml_setup
doc.css('pre > code[lang="plantuml"]').each do |node| doc.xpath(XPATH).each do |node|
img_tag = Nokogiri::HTML::DocumentFragment.parse( img_tag = Nokogiri::HTML::DocumentFragment.parse(
Asciidoctor::PlantUml::Processor.plantuml_content(node.content, {})) Asciidoctor::PlantUml::Processor.plantuml_content(node.content, {}))
node.parent.replace(img_tag) node.parent.replace(img_tag)
......
...@@ -7,10 +7,13 @@ module Banzai ...@@ -7,10 +7,13 @@ module Banzai
# Class used for tagging elements that should be rendered # Class used for tagging elements that should be rendered
TAG_CLASS = 'js-render-suggestion' TAG_CLASS = 'js-render-suggestion'
CSS = 'pre.language-suggestion > code'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call def call
return doc unless suggestions_filter_enabled? return doc unless suggestions_filter_enabled?
doc.search('pre.language-suggestion > code').each do |node| doc.xpath(XPATH).each do |node|
node.add_class(TAG_CLASS) node.add_class(TAG_CLASS)
end end
......
...@@ -14,8 +14,11 @@ module Banzai ...@@ -14,8 +14,11 @@ module Banzai
PARAMS_DELIMITER = ':' PARAMS_DELIMITER = ':'
LANG_PARAMS_ATTR = 'data-lang-params' LANG_PARAMS_ATTR = 'data-lang-params'
CSS = 'pre:not([data-math-style]):not([data-mermaid-style]):not([data-kroki-style]) > code'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call def call
doc.search('pre:not([data-math-style]):not([data-mermaid-style]):not([data-kroki-style]) > code').each do |node| doc.xpath(XPATH).each do |node|
highlight_node(node) highlight_node(node)
end end
......
...@@ -19,6 +19,9 @@ module Banzai ...@@ -19,6 +19,9 @@ module Banzai
class TableOfContentsFilter < HTML::Pipeline::Filter class TableOfContentsFilter < HTML::Pipeline::Filter
include Gitlab::Utils::Markdown include Gitlab::Utils::Markdown
CSS = 'h1, h2, h3, h4, h5, h6'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call def call
return doc if context[:no_header_anchors] return doc if context[:no_header_anchors]
...@@ -27,7 +30,7 @@ module Banzai ...@@ -27,7 +30,7 @@ module Banzai
headers = Hash.new(0) headers = Hash.new(0)
header_root = current_header = HeaderNode.new header_root = current_header = HeaderNode.new
doc.css('h1, h2, h3, h4, h5, h6').each do |node| doc.xpath(XPATH).each do |node|
if header_content = node.children.first if header_content = node.children.first
id = string_to_anchor(node.text) id = string_to_anchor(node.text)
......
...@@ -10,14 +10,21 @@ module Banzai ...@@ -10,14 +10,21 @@ module Banzai
class WikiLinkFilter < HTML::Pipeline::Filter class WikiLinkFilter < HTML::Pipeline::Filter
include Gitlab::Utils::SanitizeNodeLink include Gitlab::Utils::SanitizeNodeLink
CSS_A = 'a:not(.gfm)'
XPATH_A = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_A).freeze
CSS_VA = 'video, audio'
XPATH_VA = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_VA).freeze
CSS_IMG = 'img'
XPATH_IMG = Gitlab::Utils::Nokogiri.css_to_xpath(CSS_IMG).freeze
def call def call
return doc unless wiki? return doc unless wiki?
doc.search('a:not(.gfm)').each { |el| process_link(el.attribute('href'), el) } doc.xpath(XPATH_A).each { |el| process_link(el.attribute('href'), el) }
doc.search('video, audio').each { |el| process_link(el.attribute('src'), el) } doc.xpath(XPATH_VA).each { |el| process_link(el.attribute('src'), el) }
doc.search('img').each do |el| doc.xpath(XPATH_IMG).each do |el|
attr = el.attribute('data-src') || el.attribute('src') attr = el.attribute('data-src') || el.attribute('src')
process_link(attr, el) process_link(attr, el)
......
...@@ -84,7 +84,7 @@ module Gitlab ...@@ -84,7 +84,7 @@ module Gitlab
Gitlab::Auth::UniqueIpsLimiter.limit_user! do Gitlab::Auth::UniqueIpsLimiter.limit_user! do
user = User.by_login(login) user = User.by_login(login)
break if user && !user.can?(:log_in) break if user && !can_user_login_with_non_expired_password?(user)
authenticators = [] authenticators = []
...@@ -182,7 +182,7 @@ module Gitlab ...@@ -182,7 +182,7 @@ module Gitlab
if valid_oauth_token?(token) if valid_oauth_token?(token)
user = User.id_in(token.resource_owner_id).first user = User.id_in(token.resource_owner_id).first
return unless user&.can?(:log_in) return unless user && can_user_login_with_non_expired_password?(user)
Gitlab::Auth::Result.new(user, nil, :oauth, full_authentication_abilities) Gitlab::Auth::Result.new(user, nil, :oauth, full_authentication_abilities)
end end
...@@ -200,7 +200,7 @@ module Gitlab ...@@ -200,7 +200,7 @@ module Gitlab
return if project && token.user.project_bot? && !project.bots.include?(token.user) return if project && token.user.project_bot? && !project.bots.include?(token.user)
if token.user.can?(:log_in) || token.user.project_bot? if can_user_login_with_non_expired_password?(token.user) || token.user.project_bot?
Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes)) Gitlab::Auth::Result.new(token.user, nil, :personal_access_token, abilities_for_scopes(token.scopes))
end end
end end
...@@ -285,7 +285,7 @@ module Gitlab ...@@ -285,7 +285,7 @@ module Gitlab
return unless build.project.builds_enabled? return unless build.project.builds_enabled?
if build.user if build.user
return unless build.user.can?(:log_in) || (build.user.project_bot? && build.project.bots&.include?(build.user)) return unless can_user_login_with_non_expired_password?(build.user) || (build.user.project_bot? && build.project.bots&.include?(build.user))
# If user is assigned to build, use restricted credentials of user # If user is assigned to build, use restricted credentials of user
Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities) Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities)
...@@ -380,6 +380,10 @@ module Gitlab ...@@ -380,6 +380,10 @@ module Gitlab
user.increment_failed_attempts! user.increment_failed_attempts!
end end
def can_user_login_with_non_expired_password?(user)
user.can?(:log_in) && !user.password_expired?
end
end end
end end
end end
...@@ -23,6 +23,9 @@ module Gitlab ...@@ -23,6 +23,9 @@ module Gitlab
"Your primary email address is not confirmed. "\ "Your primary email address is not confirmed. "\
"Please check your inbox for the confirmation instructions. "\ "Please check your inbox for the confirmation instructions. "\
"In case the link is expired, you can request a new confirmation email at #{Rails.application.routes.url_helpers.new_user_confirmation_url}" "In case the link is expired, you can request a new confirmation email at #{Rails.application.routes.url_helpers.new_user_confirmation_url}"
when :password_expired
"Your password expired. "\
"Please access GitLab from a web browser to update your password."
else else
"Your account has been blocked." "Your account has been blocked."
end end
...@@ -41,6 +44,8 @@ module Gitlab ...@@ -41,6 +44,8 @@ module Gitlab
:deactivated :deactivated
elsif !@user.confirmed? elsif !@user.confirmed?
:unconfirmed :unconfirmed
elsif @user.password_expired?
:password_expired
else else
:blocked :blocked
end end
......
...@@ -6,6 +6,9 @@ module Gitlab ...@@ -6,6 +6,9 @@ module Gitlab
# Matches for instance "-1", "+1" or "-1+2". # Matches for instance "-1", "+1" or "-1+2".
SUGGESTION_CONTEXT = /^(\-(?<above>\d+))?(\+(?<below>\d+))?$/.freeze SUGGESTION_CONTEXT = /^(\-(?<above>\d+))?(\+(?<below>\d+))?$/.freeze
CSS = 'pre.language-suggestion'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
class << self class << self
# Returns an array of Gitlab::Diff::Suggestion which represents each # Returns an array of Gitlab::Diff::Suggestion which represents each
# suggestion in the given text. # suggestion in the given text.
...@@ -17,7 +20,7 @@ module Gitlab ...@@ -17,7 +20,7 @@ module Gitlab
no_original_data: true, no_original_data: true,
suggestions_filter_enabled: supports_suggestion) suggestions_filter_enabled: supports_suggestion)
doc = Nokogiri::HTML(html) doc = Nokogiri::HTML(html)
suggestion_nodes = doc.search('pre.language-suggestion') suggestion_nodes = doc.xpath(XPATH)
return [] if suggestion_nodes.empty? return [] if suggestion_nodes.empty?
......
...@@ -197,6 +197,24 @@ module Gitlab ...@@ -197,6 +197,24 @@ module Gitlab
rescue Addressable::URI::InvalidURIError, TypeError rescue Addressable::URI::InvalidURIError, TypeError
end end
def removes_sensitive_data_from_url(uri_string)
uri = parse_url(uri_string)
return unless uri
return uri_string unless uri.fragment
stripped_params = CGI.parse(uri.fragment)
if stripped_params['access_token']
stripped_params['access_token'] = 'filtered'
filtered_query = Addressable::URI.new
filtered_query.query_values = stripped_params
uri.fragment = filtered_query.query
end
uri.to_s
end
# Invert a hash, collecting all keys that map to a given value in an array. # Invert a hash, collecting all keys that map to a given value in an array.
# #
# Unlike `Hash#invert`, where the last encountered pair wins, and which has the # Unlike `Hash#invert`, where the last encountered pair wins, and which has the
......
# frozen_string_literal: true
module Gitlab
module Utils
class Nokogiri
class << self
# Use Nokogiri to convert a css selector into an xpath selector.
# Nokogiri can use css selectors with `doc.search()`. However
# for large node trees, it is _much_ slower than using xpath,
# by several orders of magnitude.
# https://gitlab.com/gitlab-org/gitlab/-/issues/329186
def css_to_xpath(css)
xpath = ::Nokogiri::CSS.xpath_for(css)
# Due to https://github.com/sparklemotion/nokogiri/issues/572,
# we remove the leading `//` and add `descendant-or-self::`
# in order to ensure we're searching from this node and all
# descendants.
xpath.map { |t| "descendant-or-self::#{t[2..-1]}" }.join('|')
end
end
end
end
end
...@@ -394,6 +394,25 @@ RSpec.describe 'Login' do ...@@ -394,6 +394,25 @@ RSpec.describe 'Login' do
gitlab_sign_in(user) gitlab_sign_in(user)
end end
context 'when the users password is expired' do
before do
user.update!(password_expires_at: Time.parse('2018-05-08 11:29:46 UTC'))
end
it 'asks for a new password' do
expect(authentication_metrics)
.to increment(:user_authenticated_counter)
visit new_user_session_path
fill_in 'user_login', with: user.email
fill_in 'user_password', with: '12345678'
click_button 'Sign in'
expect(current_path).to eq(new_profile_password_path)
end
end
end end
context 'with invalid username and password' do context 'with invalid username and password' do
......
...@@ -173,6 +173,27 @@ RSpec.describe 'lograge', type: :request do ...@@ -173,6 +173,27 @@ RSpec.describe 'lograge', type: :request do
end end
end end
describe 'with access token in url' do
before do
event.payload[:location] = 'http://example.com/auth.html#access_token=secret_token&token_type=Bearer'
end
it 'strips location from sensitive information' do
subscriber.redirect_to(event)
subscriber.process_action(event)
expect(log_data['location']).not_to include('secret_token')
expect(log_data['location']).to include('filtered')
end
it 'leaves non-sensitive information from location' do
subscriber.redirect_to(event)
subscriber.process_action(event)
expect(log_data['location']).to include('&token_type=Bearer')
end
end
context 'with db payload' do context 'with db payload' do
context 'when RequestStore is enabled', :request_store do context 'when RequestStore is enabled', :request_store do
it 'includes db counters' do it 'includes db counters' do
......
...@@ -36,9 +36,11 @@ RSpec.describe Banzai::Pipeline::PostProcessPipeline do ...@@ -36,9 +36,11 @@ RSpec.describe Banzai::Pipeline::PostProcessPipeline do
end end
let(:doc) { HTML::Pipeline.parse(html) } let(:doc) { HTML::Pipeline.parse(html) }
let(:non_related_xpath_calls) { 2 }
it 'searches for attributes only once' do it 'searches for attributes only once' do
expect(doc).to receive(:search).once.and_call_original expect(doc).to receive(:xpath).exactly(non_related_xpath_calls + 1).times
.and_call_original
subject subject
end end
...@@ -49,7 +51,8 @@ RSpec.describe Banzai::Pipeline::PostProcessPipeline do ...@@ -49,7 +51,8 @@ RSpec.describe Banzai::Pipeline::PostProcessPipeline do
end end
it 'searches for attributes twice' do it 'searches for attributes twice' do
expect(doc).to receive(:search).twice.and_call_original expect(doc).to receive(:xpath).exactly(non_related_xpath_calls + 2).times
.and_call_original
subject subject
end end
......
...@@ -57,5 +57,13 @@ RSpec.describe Gitlab::Auth::UserAccessDeniedReason do ...@@ -57,5 +57,13 @@ RSpec.describe Gitlab::Auth::UserAccessDeniedReason do
it { is_expected.to eq('Your account is pending approval from your administrator and hence blocked.') } it { is_expected.to eq('Your account is pending approval from your administrator and hence blocked.') }
end end
context 'when the user has expired password' do
before do
user.update!(password_expires_at: 2.days.ago)
end
it { is_expected.to eq('Your password expired. Please access GitLab from a web browser to update your password.') }
end
end end
end end
...@@ -433,6 +433,13 @@ RSpec.describe Gitlab::GitAccess do ...@@ -433,6 +433,13 @@ RSpec.describe Gitlab::GitAccess do
expect { pull_access_check }.to raise_forbidden("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}") expect { pull_access_check }.to raise_forbidden("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}")
end end
it 'disallows users with expired password to pull' do
project.add_maintainer(user)
user.update!(password_expires_at: 2.minutes.ago)
expect { pull_access_check }.to raise_forbidden("Your password expired. Please access GitLab from a web browser to update your password.")
end
context 'when the project repository does not exist' do context 'when the project repository does not exist' do
before do before do
project.add_guest(user) project.add_guest(user)
...@@ -969,6 +976,13 @@ RSpec.describe Gitlab::GitAccess do ...@@ -969,6 +976,13 @@ RSpec.describe Gitlab::GitAccess do
expect { push_access_check }.to raise_forbidden("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}") expect { push_access_check }.to raise_forbidden("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}")
end end
it 'disallows users with expired password to push' do
project.add_maintainer(user)
user.update!(password_expires_at: 2.minutes.ago)
expect { push_access_check }.to raise_forbidden("Your password expired. Please access GitLab from a web browser to update your password.")
end
it 'cleans up the files' do it 'cleans up the files' do
expect(project.repository).to receive(:clean_stale_repository_files).and_call_original expect(project.repository).to receive(:clean_stale_repository_files).and_call_original
expect { push_access_check }.not_to raise_error expect { push_access_check }.not_to raise_error
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Utils::Nokogiri do
describe '#css_to_xpath' do
using RSpec::Parameterized::TableSyntax
where(:css, :xpath) do
'img' | "descendant-or-self::img"
'a.gfm' | "descendant-or-self::a[contains(concat(' ',normalize-space(@class),' '),' gfm ')]"
'a:not(.gfm)' | "descendant-or-self::a[not(contains(concat(' ',normalize-space(@class),' '),' gfm '))]"
'video, audio' | "descendant-or-self::video|descendant-or-self::audio"
'[data-math-style]' | "descendant-or-self::*[@data-math-style]"
'[data-mermaid-style]' | "descendant-or-self::*[@data-mermaid-style]"
'.js-render-metrics' | "descendant-or-self::*[contains(concat(' ',normalize-space(@class),' '),' js-render-metrics ')]"
'h1, h2, h3, h4, h5, h6' | "descendant-or-self::h1|descendant-or-self::h2|descendant-or-self::h3|descendant-or-self::h4|descendant-or-self::h5|descendant-or-self::h6"
'pre.code.language-math' | "descendant-or-self::pre[contains(concat(' ',normalize-space(@class),' '),' code ') and contains(concat(' ',normalize-space(@class),' '),' language-math ')]"
'pre > code[lang="plantuml"]' | "descendant-or-self::pre/code[@lang=\"plantuml\"]"
'pre[lang="mermaid"] > code' | "descendant-or-self::pre[@lang=\"mermaid\"]/code"
'pre.language-suggestion' | "descendant-or-self::pre[contains(concat(' ',normalize-space(@class),' '),' language-suggestion ')]"
'pre.language-suggestion > code' | "descendant-or-self::pre[contains(concat(' ',normalize-space(@class),' '),' language-suggestion ')]/code"
'a.gfm[data-reference-type="user"]' | "descendant-or-self::a[contains(concat(' ',normalize-space(@class),' '),' gfm ') and @data-reference-type=\"user\"]"
'a:not(.gfm), img:not(.gfm), video:not(.gfm), audio:not(.gfm)' | "descendant-or-self::a[not(contains(concat(' ',normalize-space(@class),' '),' gfm '))]|descendant-or-self::img[not(contains(concat(' ',normalize-space(@class),' '),' gfm '))]|descendant-or-self::video[not(contains(concat(' ',normalize-space(@class),' '),' gfm '))]|descendant-or-self::audio[not(contains(concat(' ',normalize-space(@class),' '),' gfm '))]"
'pre:not([data-math-style]):not([data-mermaid-style]):not([data-kroki-style]) > code' | "descendant-or-self::pre[not(@data-math-style) and not(@data-mermaid-style) and not(@data-kroki-style)]/code"
end
with_them do
it 'generates the xpath' do
expect(described_class.css_to_xpath(css)).to eq xpath
end
end
end
end
...@@ -417,6 +417,29 @@ RSpec.describe Gitlab::Utils do ...@@ -417,6 +417,29 @@ RSpec.describe Gitlab::Utils do
end end
end end
describe '.removes_sensitive_data_from_url' do
it 'returns string object' do
expect(described_class.removes_sensitive_data_from_url('http://gitlab.com')).to be_instance_of(String)
end
it 'returns nil when URI cannot be parsed' do
expect(described_class.removes_sensitive_data_from_url('://gitlab.com')).to be nil
end
it 'returns nil with invalid parameter' do
expect(described_class.removes_sensitive_data_from_url(1)).to be nil
end
it 'returns string with filtered access_token param' do
expect(described_class.removes_sensitive_data_from_url('http://gitlab.com/auth.html#access_token=secret_token')).to eq('http://gitlab.com/auth.html#access_token=filtered')
end
it 'returns string with filtered access_token param but other params preserved' do
expect(described_class.removes_sensitive_data_from_url('http://gitlab.com/auth.html#access_token=secret_token&token_type=Bearer&state=test'))
.to include('&token_type=Bearer', '&state=test')
end
end
describe 'multiple_key_invert' do describe 'multiple_key_invert' do
it 'invert keys with array values' do it 'invert keys with array values' do
hash = { hash = {
......
...@@ -239,6 +239,14 @@ RSpec.describe GlobalPolicy do ...@@ -239,6 +239,14 @@ RSpec.describe GlobalPolicy do
it { is_expected.not_to be_allowed(:access_api) } it { is_expected.not_to be_allowed(:access_api) }
end end
context 'user with expired password' do
before do
current_user.update!(password_expires_at: 2.minutes.ago)
end
it { is_expected.not_to be_allowed(:access_api) }
end
context 'when terms are enforced' do context 'when terms are enforced' do
before do before do
enforce_terms enforce_terms
...@@ -418,6 +426,14 @@ RSpec.describe GlobalPolicy do ...@@ -418,6 +426,14 @@ RSpec.describe GlobalPolicy do
it { is_expected.not_to be_allowed(:access_git) } it { is_expected.not_to be_allowed(:access_git) }
end end
context 'user with expired password' do
before do
current_user.update!(password_expires_at: 2.minutes.ago)
end
it { is_expected.not_to be_allowed(:access_git) }
end
end end
describe 'read instance metadata' do describe 'read instance metadata' do
...@@ -494,6 +510,14 @@ RSpec.describe GlobalPolicy do ...@@ -494,6 +510,14 @@ RSpec.describe GlobalPolicy do
it { is_expected.not_to be_allowed(:use_slash_commands) } it { is_expected.not_to be_allowed(:use_slash_commands) }
end end
context 'user with expired password' do
before do
current_user.update!(password_expires_at: 2.minutes.ago)
end
it { is_expected.not_to be_allowed(:use_slash_commands) }
end
end end
describe 'create_snippet' do describe 'create_snippet' do
......
...@@ -35,6 +35,26 @@ RSpec.describe 'Git HTTP requests' do ...@@ -35,6 +35,26 @@ RSpec.describe 'Git HTTP requests' do
expect(response.header['WWW-Authenticate']).to start_with('Basic ') expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end end
end end
context "when password is expired" do
it "responds to downloads with status 401 Unauthorized" do
user.update!(password_expires_at: 2.days.ago)
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
context "when user is blocked" do
let(:user) { create(:user, :blocked) }
it "responds to downloads with status 401 Unauthorized" do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
end end
context "when authentication succeeds" do context "when authentication succeeds" do
...@@ -75,6 +95,15 @@ RSpec.describe 'Git HTTP requests' do ...@@ -75,6 +95,15 @@ RSpec.describe 'Git HTTP requests' do
expect(response.header['WWW-Authenticate']).to start_with('Basic ') expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end end
end end
context "when password is expired" do
it "responds to uploads with status 401 Unauthorized" do
user.update!(password_expires_at: 2.days.ago)
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
end end
context "when authentication succeeds" do context "when authentication succeeds" do
...@@ -576,6 +605,16 @@ RSpec.describe 'Git HTTP requests' do ...@@ -576,6 +605,16 @@ RSpec.describe 'Git HTTP requests' do
it_behaves_like 'pulls are allowed' it_behaves_like 'pulls are allowed'
it_behaves_like 'pushes are allowed' it_behaves_like 'pushes are allowed'
context "when password is expired" do
it "responds to downloads with status 401 unauthorized" do
user.update!(password_expires_at: 2.days.ago)
download(path, **env) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
end end
context 'when user has 2FA enabled' do context 'when user has 2FA enabled' do
...@@ -649,6 +688,18 @@ RSpec.describe 'Git HTTP requests' do ...@@ -649,6 +688,18 @@ RSpec.describe 'Git HTTP requests' do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
end end
context "when password is expired" do
it "responds to uploads with status 401 unauthorized" do
user.update!(password_expires_at: 2.days.ago)
write_access_token = create(:personal_access_token, user: user, scopes: [:write_repository])
upload(path, user: user.username, password: write_access_token.token) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
end end
end end
...@@ -860,6 +911,16 @@ RSpec.describe 'Git HTTP requests' do ...@@ -860,6 +911,16 @@ RSpec.describe 'Git HTTP requests' do
expect(response).to have_gitlab_http_status(:not_found) expect(response).to have_gitlab_http_status(:not_found)
end end
context 'when users password is expired' do
it 'rejects pulls with 401 unauthorized' do
user.update!(password_expires_at: 2.days.ago)
download(path, user: 'gitlab-ci-token', password: build.token) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
end end
end end
end end
......
...@@ -346,9 +346,7 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -346,9 +346,7 @@ RSpec.describe 'Git LFS API and storage' do
let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago)} let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago)}
let(:role) { :reporter} let(:role) { :reporter}
# TODO: This should return a 404 response it_behaves_like 'LFS http 401 response'
# https://gitlab.com/gitlab-org/gitlab/-/issues/292006
it_behaves_like 'LFS http 200 response'
end end
context 'when user is blocked' do context 'when user is blocked' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment