Commit 94510ce2 authored by John T Skarbek's avatar John T Skarbek

Merge remote-tracking branch 'security/master'

parents 9806cb70 5c6f2e13
...@@ -146,7 +146,7 @@ GEM ...@@ -146,7 +146,7 @@ GEM
coderay (>= 1.0.0) coderay (>= 1.0.0)
erubi (>= 1.0.0) erubi (>= 1.0.0)
rack (>= 0.9.0) rack (>= 0.9.0)
bindata (2.4.8) bindata (2.4.10)
binding_ninja (0.2.3) binding_ninja (0.2.3)
bootsnap (1.4.6) bootsnap (1.4.6)
msgpack (~> 1.0) msgpack (~> 1.0)
......
...@@ -195,6 +195,7 @@ export default { ...@@ -195,6 +195,7 @@ export default {
'var', 'var',
], ],
ALLOWED_ATTR: ['class', 'style', 'href', 'src'], ALLOWED_ATTR: ['class', 'style', 'href', 'src'],
ALLOW_DATA_ATTR: false,
}); });
}, },
}, },
......
...@@ -31,6 +31,7 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController ...@@ -31,6 +31,7 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
scopes: %w(READ WRITE DELETE), scopes: %w(READ WRITE DELETE),
apiVersion: 1, apiVersion: 1,
apiMigrations: { apiMigrations: {
'context-qsh': true,
gdpr: true gdpr: true
} }
} }
......
...@@ -14,8 +14,9 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController ...@@ -14,8 +14,9 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
if pre_auth.authorizable? if pre_auth.authorizable?
if skip_authorization? || matching_token? if skip_authorization? || matching_token?
auth = authorization.authorize auth = authorization.authorize
parsed_redirect_uri = URI.parse(auth.redirect_uri)
session.delete(:user_return_to) session.delete(:user_return_to)
redirect_to auth.redirect_uri render "doorkeeper/authorizations/redirect", locals: { redirect_uri: parsed_redirect_uri }, layout: false
else else
render "doorkeeper/authorizations/new" render "doorkeeper/authorizations/new"
end end
......
...@@ -118,6 +118,7 @@ module MarkupHelper ...@@ -118,6 +118,7 @@ module MarkupHelper
def markup(file_name, text, context = {}) def markup(file_name, text, context = {})
context[:project] ||= @project context[:project] ||= @project
context[:text_source] ||= :blob
html = context.delete(:rendered) || markup_unsafe(file_name, text, context) html = context.delete(:rendered) || markup_unsafe(file_name, text, context)
prepare_for_rendering(html, context) prepare_for_rendering(html, context)
end end
......
...@@ -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|
......
%h3.page-title= _("Redirecting")
%div
%a{ :href => redirect_uri } Click here to redirect to #{redirect_uri}
= javascript_tag do
:plain
window.location= "#{redirect_uri}";
...@@ -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
......
# frozen_string_literal: true
class ScheduleUpdateUsersWhereTwoFactorAuthRequiredFromGroup < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
MIGRATION = 'UpdateUsersWhereTwoFactorAuthRequiredFromGroup'
DELAY_INTERVAL = 2.minutes
BATCH_SIZE = 10_000
INDEX_NAME = 'index_users_require_two_factor_authentication_from_group_false'
disable_ddl_transaction!
class User < ActiveRecord::Base
include EachBatch
self.table_name = 'users'
end
def up
add_concurrent_index :users,
:require_two_factor_authentication_from_group,
where: 'require_two_factor_authentication_from_group = FALSE',
name: INDEX_NAME
relation = User.where(require_two_factor_authentication_from_group: false)
queue_background_migration_jobs_by_range_at_intervals(
relation, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
end
def down
remove_concurrent_index_by_name :users, INDEX_NAME
end
end
bdd82fc5cb2bbb322125c153c741002725853e23cd0ae0edbfd80563a4a87f2f
\ No newline at end of file
...@@ -24767,6 +24767,8 @@ CREATE INDEX index_users_ops_dashboard_projects_on_project_id ON users_ops_dashb ...@@ -24767,6 +24767,8 @@ CREATE INDEX index_users_ops_dashboard_projects_on_project_id ON users_ops_dashb
CREATE UNIQUE INDEX index_users_ops_dashboard_projects_on_user_id_and_project_id ON users_ops_dashboard_projects USING btree (user_id, project_id); CREATE UNIQUE INDEX index_users_ops_dashboard_projects_on_user_id_and_project_id ON users_ops_dashboard_projects USING btree (user_id, project_id);
CREATE INDEX index_users_require_two_factor_authentication_from_group_false ON users USING btree (require_two_factor_authentication_from_group) WHERE (require_two_factor_authentication_from_group = false);
CREATE INDEX index_users_security_dashboard_projects_on_user_id ON users_security_dashboard_projects USING btree (user_id); CREATE INDEX index_users_security_dashboard_projects_on_user_id ON users_security_dashboard_projects USING btree (user_id);
CREATE INDEX index_users_star_projects_on_project_id ON users_star_projects USING btree (project_id); CREATE INDEX index_users_star_projects_on_project_id ON users_star_projects USING btree (project_id);
...@@ -9,8 +9,14 @@ module EE ...@@ -9,8 +9,14 @@ module EE
unexpose :email unexpose :email
expose :oncall_schedules, with: ::IncidentManagement::OncallScheduleEntity expose :oncall_schedules, with: ::IncidentManagement::OncallScheduleEntity
# options[:source] is required to scope the schedules
# It should be either a Group or Project
def oncall_schedules def oncall_schedules
object.oncall_schedules.uniq return [] unless options[:source].present?
project_ids = options[:source].is_a?(Group) ? options[:source].project_ids : [options[:source].id]
object.oncall_schedules.select { |schedule| project_ids.include?(schedule.project_id) }
end end
end end
end end
......
...@@ -7,8 +7,11 @@ module Banzai ...@@ -7,8 +7,11 @@ module Banzai
PRIVATE_IMAGE_PATH = '/secure/attachment/' PRIVATE_IMAGE_PATH = '/secure/attachment/'
CSS_WITH_ATTACHMENT_ICON = 'with-attachment-icon' CSS_WITH_ATTACHMENT_ICON = 'with-attachment-icon'
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|
next unless img['src'].start_with?(PRIVATE_IMAGE_PATH) next unless img['src'].start_with?(PRIVATE_IMAGE_PATH)
img_link = "#{project.jira_service.url}#{img['src']}" img_link = "#{project.jira_service.url}#{img['src']}"
......
...@@ -9,8 +9,11 @@ module Gitlab ...@@ -9,8 +9,11 @@ module Gitlab
# Part of FileUploader::MARKDOWN_PATTERN but with a non-greedy file name matcher (?<file>.*) vs (?<file>.*?) # Part of FileUploader::MARKDOWN_PATTERN but with a non-greedy file name matcher (?<file>.*) vs (?<file>.*?)
NON_GREEDY_UPLOAD_FILE_PATH_PATTERN = %r{/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*)}.freeze NON_GREEDY_UPLOAD_FILE_PATH_PATTERN = %r{/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*)}.freeze
CSS = 'img'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
def call def call
doc.css('img').each do |image_node| doc.xpath(XPATH).each do |image_node|
image_node['class'] = 'gl-image' image_node['class'] = 'gl-image'
original_src = image_node.delete('data-src').value original_src = image_node.delete('data-src').value
......
...@@ -9,14 +9,15 @@ module Gitlab ...@@ -9,14 +9,15 @@ module Gitlab
# +Banzai::Filter::ReferenceRedactorFilter+, so it's easier to find and # +Banzai::Filter::ReferenceRedactorFilter+, so it's easier to find and
# anonymize `user` references. # anonymize `user` references.
class MentionAnonymizationFilter < HTML::Pipeline::Filter class MentionAnonymizationFilter < HTML::Pipeline::Filter
LINK_CSS_SELECTOR = "a.gfm[data-reference-type='user']" CSS = 'a.gfm[data-reference-type="user"]'
XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze
# Static for now. In https://gitlab.com/gitlab-org/gitlab/-/issues/209114 # Static for now. In https://gitlab.com/gitlab-org/gitlab/-/issues/209114
# we'll map names with a more sophisticated approach. # we'll map names with a more sophisticated approach.
ANONYMIZED_NAME = 'Incident Responder' ANONYMIZED_NAME = 'Incident Responder'
def call def call
doc.css(LINK_CSS_SELECTOR).each do |link_node| doc.xpath(XPATH).each do |link_node|
link_node.replace(ANONYMIZED_NAME) link_node.replace(ANONYMIZED_NAME)
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::ProjectMembersHelper do
include OncallHelpers
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:current_user) { user }
before do
allow(helper).to receive(:current_user).and_return(current_user)
allow(helper).to receive(:can?).and_return(true)
end
describe '#project_members_app_data_json' do
before do
project.add_developer(user)
create_schedule_with_user(project, user)
end
it 'does not execute N+1' do
control_count = ActiveRecord::QueryRecorder.new do
call_project_members_app_data_json
end.count
expect(project.members.count).to eq(2)
user_2 = create(:user)
project.add_developer(user_2)
create_schedule_with_user(project, user_2)
expect(project.members.count).to eq(3)
expect { call_project_members_app_data_json }.not_to exceed_query_limit(control_count).with_threshold(6) # existing n+1
end
end
def call_project_members_app_data_json
helper.project_members_app_data_json(project, members: preloaded_members, group_links: [], invited: [], access_requests: [])
end
# Simulates the behaviour in ProjectMembersController
def preloaded_members
klass = Class.new do
include MembersPresentation
def initialize(user)
@current_user = user
end
attr_reader :current_user
end
klass.new(current_user).present_members(project.members.reload)
end
end
...@@ -3,35 +3,54 @@ ...@@ -3,35 +3,54 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe MemberUserEntity do RSpec.describe MemberUserEntity do
include OncallHelpers
let_it_be_with_reload(:user) { create(:user) } let_it_be_with_reload(:user) { create(:user) }
let(:entity) { described_class.new(user) } let(:entity) { described_class.new(user, source: source) }
let(:entity_hash) { entity.as_json } let(:entity_hash) { entity.as_json }
let(:source) { nil }
it 'matches json schema' do it 'matches json schema' do
expect(entity.to_json).to match_schema('entities/member_user') expect(entity.to_json).to match_schema('entities/member_user')
end end
context 'with oncall schedules' do context 'with oncall schedules' do
let_it_be(:oncall_schedule) { create(:incident_management_oncall_participant, user: user).rotation.schedule } let_it_be(:group) { create(:group) }
let_it_be(:project_1) { create(:project, group: group )}
let_it_be(:project_2) { create(:project, group: group )}
let_it_be(:oncall_schedule_1) { create_schedule_with_user(project_1, user) }
let_it_be(:oncall_schedule_2) { create_schedule_with_user(project_2, user) }
it 'returns an empty array if no source option is given' do
expect(entity_hash[:oncall_schedules]).to eq []
end
context 'source is project' do
let(:source) { project_1 }
it 'correctly exposes `oncall_schedules`' do it 'correctly exposes `oncall_schedules`' do
expect(entity_hash[:oncall_schedules]).to include(schedule_hash(oncall_schedule)) expect(entity_hash[:oncall_schedules]).to contain_exactly(schedule_hash(oncall_schedule_1))
end
end end
it 'exposed and de-dupes the schedules' do context 'source is group' do
allow(user).to receive(:oncall_schedules).and_return([oncall_schedule, oncall_schedule]) let(:source) { group }
expect(entity_hash[:oncall_schedules].size).to eq(1) it 'correctly exposes `oncall_schedules`' do
expect(entity_hash[:oncall_schedules]).to include(schedule_hash(oncall_schedule)) expect(entity_hash[:oncall_schedules]).to contain_exactly(schedule_hash(oncall_schedule_1), schedule_hash(oncall_schedule_2))
end end
end
private
def schedule_hash(schedule) def schedule_hash(schedule)
schedule_url = Gitlab::Routing.url_helpers.project_incident_management_oncall_schedules_url(schedule.project) schedule_url = Gitlab::Routing.url_helpers.project_incident_management_oncall_schedules_url(schedule.project)
project_url = Gitlab::Routing.url_helpers.project_url(schedule.project) project_url = Gitlab::Routing.url_helpers.project_url(schedule.project)
{ {
name: oncall_schedule.name, name: schedule.name,
project_name: oncall_schedule.project.name, project_name: schedule.project.name,
schedule_url: schedule_url, schedule_url: schedule_url,
project_url: project_url project_url: project_url
} }
......
...@@ -6,4 +6,12 @@ module OncallHelpers ...@@ -6,4 +6,12 @@ module OncallHelpers
rotation.active_period.for_date(date) rotation.active_period.for_date(date)
end end
def create_schedule_with_user(project, user)
create(:incident_management_oncall_schedule, project: project) do |schedule|
create(:incident_management_oncall_rotation, schedule: schedule) do |rotation|
create(:incident_management_oncall_participant, rotation: rotation, user: user)
end
end
end
end end
...@@ -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
...@@ -35,7 +38,7 @@ module Banzai ...@@ -35,7 +38,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)
......
...@@ -3,12 +3,29 @@ ...@@ -3,12 +3,29 @@
module Banzai module Banzai
module Filter module Filter
class TruncateSourceFilter < HTML::Pipeline::TextFilter class TruncateSourceFilter < HTML::Pipeline::TextFilter
CHARACTER_COUNT_LIMIT = 1.megabyte
USER_MSG_LIMIT = 10_000
def call def call
return text unless context.key?(:limit) # don't truncate if it's a :blob and no limit is set
return text if context[:text_source] == :blob && !context.key?(:limit)
limit = context[:limit] || CHARACTER_COUNT_LIMIT
# no sense in allowing `truncate_bytes` to duplicate a large
# string unless it's too big
return text if text.bytesize <= limit
# Use three dots instead of the ellipsis Unicode character because # Use three dots instead of the ellipsis Unicode character because
# some clients show the raw Unicode value in the merge commit. # some clients show the raw Unicode value in the merge commit.
text.truncate_bytes(context[:limit], omission: '...') trunc = text.truncate_bytes(limit, omission: '...')
# allows us to indicate to the user that what they see is a truncated copy
if limit > USER_MSG_LIMIT
trunc.prepend("_The text is longer than #{limit} characters and has been visually truncated._\n\n")
end
trunc
end end
end end
end end
......
...@@ -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
......
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class UpdateUsersWhereTwoFactorAuthRequiredFromGroup # rubocop:disable Metrics/ClassLength
def perform(start_id, stop_id)
ActiveRecord::Base.connection.execute <<~SQL
UPDATE
users
SET
require_two_factor_authentication_from_group = TRUE
WHERE
users.id BETWEEN #{start_id}
AND #{stop_id}
AND users.require_two_factor_authentication_from_group = FALSE
AND users.id IN (
SELECT
DISTINCT users_groups_query.user_id
FROM
(
SELECT
users.id AS user_id,
members.source_id AS group_ids
FROM
users
LEFT JOIN members ON members.source_type = 'Namespace'
AND members.requested_at IS NULL
AND members.user_id = users.id
AND members.type = 'GroupMember'
WHERE
users.require_two_factor_authentication_from_group = FALSE
AND users.id BETWEEN #{start_id}
AND #{stop_id}) AS users_groups_query
INNER JOIN LATERAL (
WITH RECURSIVE "base_and_ancestors" AS (
(
SELECT
"namespaces"."type",
"namespaces"."id",
"namespaces"."parent_id",
"namespaces"."require_two_factor_authentication"
FROM
"namespaces"
WHERE
"namespaces"."type" = 'Group'
AND "namespaces"."id" = users_groups_query.group_ids
)
UNION
(
SELECT
"namespaces"."type",
"namespaces"."id",
"namespaces"."parent_id",
"namespaces"."require_two_factor_authentication"
FROM
"namespaces",
"base_and_ancestors"
WHERE
"namespaces"."type" = 'Group'
AND "namespaces"."id" = "base_and_ancestors"."parent_id"
)
),
"base_and_descendants" AS (
(
SELECT
"namespaces"."type",
"namespaces"."id",
"namespaces"."parent_id",
"namespaces"."require_two_factor_authentication"
FROM
"namespaces"
WHERE
"namespaces"."type" = 'Group'
AND "namespaces"."id" = users_groups_query.group_ids
)
UNION
(
SELECT
"namespaces"."type",
"namespaces"."id",
"namespaces"."parent_id",
"namespaces"."require_two_factor_authentication"
FROM
"namespaces",
"base_and_descendants"
WHERE
"namespaces"."type" = 'Group'
AND "namespaces"."parent_id" = "base_and_descendants"."id"
)
)
SELECT
"namespaces".*
FROM
(
(
SELECT
"namespaces"."type",
"namespaces"."id",
"namespaces"."parent_id",
"namespaces"."require_two_factor_authentication"
FROM
"base_and_ancestors" AS "namespaces"
WHERE
"namespaces"."type" = 'Group'
)
UNION
(
SELECT
"namespaces"."type",
"namespaces"."id",
"namespaces"."parent_id",
"namespaces"."require_two_factor_authentication"
FROM
"base_and_descendants" AS "namespaces"
WHERE
"namespaces"."type" = 'Group'
)
) namespaces
WHERE
"namespaces"."type" = 'Group'
AND "namespaces"."require_two_factor_authentication" = TRUE
) AS hierarchy_tree ON TRUE
);
SQL
end
end
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
...@@ -23,7 +23,7 @@ module Gitlab ...@@ -23,7 +23,7 @@ module Gitlab
end end
def user def user
User.find_by_any_email(@email) strong_memoize(:user) { User.find_by_any_email(@email) }
end end
def verified_signature def verified_signature
...@@ -31,9 +31,13 @@ module Gitlab ...@@ -31,9 +31,13 @@ module Gitlab
end end
def verification_status def verification_status
return :unverified if x509_certificate.nil? || x509_certificate.revoked? return :unverified if
x509_certificate.nil? ||
x509_certificate.revoked? ||
!verified_signature ||
user.nil?
if verified_signature && certificate_email == @email if user.verified_emails.include?(@email) && certificate_email == @email
:verified :verified
else else
:unverified :unverified
......
...@@ -27077,6 +27077,9 @@ msgstr "" ...@@ -27077,6 +27077,9 @@ msgstr ""
msgid "Redirect to SAML provider to test configuration" msgid "Redirect to SAML provider to test configuration"
msgstr "" msgstr ""
msgid "Redirecting"
msgstr ""
msgid "Redis" msgid "Redis"
msgstr "" msgstr ""
......
...@@ -70,56 +70,26 @@ RSpec.describe Oauth::AuthorizationsController do ...@@ -70,56 +70,26 @@ RSpec.describe Oauth::AuthorizationsController do
describe 'GET #new' do describe 'GET #new' do
subject { get :new, params: params } subject { get :new, params: params }
include_examples 'OAuth Authorizations require confirmed user'
include_examples "Implicit grant can't be used in confidential application" include_examples "Implicit grant can't be used in confidential application"
context 'rendering of views based on the ownership of the application' do context 'when the user is confirmed' do
shared_examples 'render views' do let(:confirmed_at) { 1.hour.ago }
render_views
it 'returns 200 and renders view with correct info', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to include(application.owner.name)
expect(response).to render_template('doorkeeper/authorizations/new')
end
end
subject { get :new, params: params }
context 'when auth app owner is a user' do
context 'with valid params' do
it_behaves_like 'render views'
end
end
context 'when auth app owner is a group' do
let(:group) { create(:group) }
context 'when auth app owner is a root group' do
let(:application) { create(:oauth_application, owner_id: group.id, owner_type: 'Namespace') }
it_behaves_like 'render views' context 'when there is already an access token for the application with a matching scope' do
end before do
scopes = Doorkeeper::OAuth::Scopes.from_string('api')
context 'when auth app owner is a subgroup' do allow(Doorkeeper.configuration).to receive(:scopes).and_return(scopes)
let(:subgroup) { create(:group, parent: group) }
let(:application) { create(:oauth_application, owner_id: subgroup.id, owner_type: 'Namespace') }
it_behaves_like 'render views' create(:oauth_access_token, application: application, resource_owner_id: user.id, scopes: scopes)
end
end end
context 'when there is no owner associated' do it 'authorizes the request and shows the user a page that redirects' do
let(:application) { create(:oauth_application, owner_id: nil, owner_type: nil) }
it 'renders view' do
subject subject
expect(request.session['user_return_to']).to be_nil
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('doorkeeper/authorizations/new') expect(response).to render_template('doorkeeper/authorizations/redirect')
end
end end
end end
...@@ -132,6 +102,16 @@ RSpec.describe Oauth::AuthorizationsController do ...@@ -132,6 +102,16 @@ RSpec.describe Oauth::AuthorizationsController do
end end
end end
context 'with valid params' do
render_views
it 'returns 200 code and renders view' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('doorkeeper/authorizations/new')
end
it 'deletes session.user_return_to and redirects when skip authorization' do it 'deletes session.user_return_to and redirects when skip authorization' do
application.update!(trusted: true) application.update!(trusted: true)
request.session['user_return_to'] = 'http://example.com' request.session['user_return_to'] = 'http://example.com'
...@@ -139,7 +119,10 @@ RSpec.describe Oauth::AuthorizationsController do ...@@ -139,7 +119,10 @@ RSpec.describe Oauth::AuthorizationsController do
subject subject
expect(request.session['user_return_to']).to be_nil expect(request.session['user_return_to']).to be_nil
expect(response).to have_gitlab_http_status(:found) expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template('doorkeeper/authorizations/redirect')
end
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
......
...@@ -39,7 +39,7 @@ describe('Markdown component', () => { ...@@ -39,7 +39,7 @@ describe('Markdown component', () => {
expect(vm.$el.querySelector('.markdown h1')).not.toBeNull(); expect(vm.$el.querySelector('.markdown h1')).not.toBeNull();
}); });
it('sanitizes output', async () => { it('sanitizes Markdown output', async () => {
Object.assign(cell, { Object.assign(cell, {
source: [ source: [
'[XSS](data:text/html;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+Cg==)\n', '[XSS](data:text/html;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+Cg==)\n',
...@@ -50,6 +50,17 @@ describe('Markdown component', () => { ...@@ -50,6 +50,17 @@ describe('Markdown component', () => {
expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull(); expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull();
}); });
it('sanitizes HTML', async () => {
const findLink = () => vm.$el.querySelector('.xss-link');
Object.assign(cell, {
source: ['<a href="test.js" data-remote=true data-type="script" class="xss-link">XSS</a>\n'],
});
await vm.$nextTick();
expect(findLink().getAttribute('data-remote')).toBe(null);
expect(findLink().getAttribute('data-type')).toBe(null);
});
describe('tables', () => { describe('tables', () => {
beforeEach(() => { beforeEach(() => {
json = getJSONFixture('blob/notebook/markdown-table.json'); json = getJSONFixture('blob/notebook/markdown-table.json');
......
...@@ -418,6 +418,13 @@ FooBar ...@@ -418,6 +418,13 @@ FooBar
describe '#markup' do describe '#markup' do
let(:content) { 'Noël' } let(:content) { 'Noël' }
it 'sets the :text_source to :blob in the context' do
context = {}
helper.markup('foo.md', content, context)
expect(context).to include(text_source: :blob)
end
it 'preserves encoding' do it 'preserves encoding' do
expect(content.encoding.name).to eq('UTF-8') expect(content.encoding.name).to eq('UTF-8')
expect(helper.markup('foo.rst', content).encoding.name).to eq('UTF-8') expect(helper.markup('foo.rst', content).encoding.name).to eq('UTF-8')
......
...@@ -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
......
...@@ -8,12 +8,55 @@ RSpec.describe Banzai::Filter::TruncateSourceFilter do ...@@ -8,12 +8,55 @@ RSpec.describe Banzai::Filter::TruncateSourceFilter do
let(:short_text) { 'foo' * 10 } let(:short_text) { 'foo' * 10 }
let(:long_text) { ([short_text] * 10).join(' ') } let(:long_text) { ([short_text] * 10).join(' ') }
before do
stub_const("#{described_class}::CHARACTER_COUNT_LIMIT", 50)
stub_const("#{described_class}::USER_MSG_LIMIT", 20)
end
context 'when markdown belongs to a blob' do
it 'does nothing when limit is unspecified' do it 'does nothing when limit is unspecified' do
output = filter(long_text) output = filter(long_text, text_source: :blob)
expect(output).to eq(long_text)
end
it 'truncates normally when limit specified' do
truncated = 'foofoof...'
output = filter(long_text, text_source: :blob, limit: 10)
expect(output).to eq(truncated)
end
end
context 'when markdown belongs to a field (non-blob)' do
it 'does nothing when limit is greater' do
output = filter(long_text, limit: 1.megabyte)
expect(output).to eq(long_text) expect(output).to eq(long_text)
end end
it 'truncates to the default when limit is unspecified' do
stub_const("#{described_class}::USER_MSG_LIMIT", 200)
truncated = 'foofoofoofoofoofoofoofoofoofoo foofoofoofoofoof...'
output = filter(long_text)
expect(output).to eq(truncated)
end
it 'prepends the user message' do
truncated = <<~TEXT
_The text is longer than 50 characters and has been visually truncated._
foofoofoofoofoofoofoofoofoofoo foofoofoofoofoof...
TEXT
output = filter(long_text)
expect(output).to eq(truncated.strip)
end
it 'does nothing to a short-enough text' do it 'does nothing to a short-enough text' do
output = filter(short_text, limit: short_text.bytesize) output = filter(short_text, limit: short_text.bytesize)
...@@ -28,4 +71,5 @@ RSpec.describe Banzai::Filter::TruncateSourceFilter do ...@@ -28,4 +71,5 @@ RSpec.describe Banzai::Filter::TruncateSourceFilter do
expect(filter(utf8_text, limit: utf8_text.bytesize)).to eq(utf8_text) expect(filter(utf8_text, limit: utf8_text.bytesize)).to eq(utf8_text)
expect(filter(utf8_text, limit: utf8_text.mb_chars.size)).not_to eq(utf8_text) expect(filter(utf8_text, limit: utf8_text.mb_chars.size)).not_to eq(utf8_text)
end end
end
end end
...@@ -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
......
...@@ -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
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::UpdateUsersWhereTwoFactorAuthRequiredFromGroup, :migration, schema: 20210519154058 do
include MigrationHelpers::NamespacesHelpers
let(:group_with_2fa_parent) { create_namespace('parent', Gitlab::VisibilityLevel::PRIVATE, require_two_factor_authentication: true) }
let(:group_with_2fa_child) { create_namespace('child', Gitlab::VisibilityLevel::PRIVATE, parent_id: group_with_2fa_parent.id) }
let(:members_table) { table(:members) }
let(:users_table) { table(:users) }
subject { described_class.new }
describe '#perform' do
context 'with group members' do
let(:user_1) { create_user('user@example.com') }
let!(:member) { create_group_member(user_1, group_with_2fa_parent) }
let!(:user_without_group) { create_user('user_without@example.com') }
let(:user_other) { create_user('user_other@example.com') }
let!(:member_other) { create_group_member(user_other, group_with_2fa_parent) }
it 'updates user when user should be required to establish two factor authentication' do
subject.perform(user_1.id, user_without_group.id)
expect(user_1.reload.require_two_factor_authentication_from_group).to eq(true)
end
it 'does not update user who is not in current batch' do
subject.perform(user_1.id, user_without_group.id)
expect(user_other.reload.require_two_factor_authentication_from_group).to eq(false)
end
it 'updates all users in current batch' do
subject.perform(user_1.id, user_other.id)
expect(user_other.reload.require_two_factor_authentication_from_group).to eq(true)
end
it 'updates user when user is member of group in which parent group requires two factor authentication' do
member.destroy!
subgroup = create_namespace('subgroup', Gitlab::VisibilityLevel::PRIVATE, require_two_factor_authentication: false, parent_id: group_with_2fa_child.id)
create_group_member(user_1, subgroup)
subject.perform(user_1.id, user_other.id)
expect(user_1.reload.require_two_factor_authentication_from_group).to eq(true)
end
it 'updates user when user is member of a group and the subgroup requires two factor authentication' do
member.destroy!
parent = create_namespace('other_parent', Gitlab::VisibilityLevel::PRIVATE, require_two_factor_authentication: false)
create_namespace('other_subgroup', Gitlab::VisibilityLevel::PRIVATE, require_two_factor_authentication: true, parent_id: parent.id)
create_group_member(user_1, parent)
subject.perform(user_1.id, user_other.id)
expect(user_1.reload.require_two_factor_authentication_from_group).to eq(true)
end
it 'does not update user when not a member of a group that requires two factor authentication' do
member_other.destroy!
other_group = create_namespace('other_group', Gitlab::VisibilityLevel::PRIVATE, require_two_factor_authentication: false)
create_group_member(user_other, other_group)
subject.perform(user_1.id, user_other.id)
expect(user_other.reload.require_two_factor_authentication_from_group).to eq(false)
end
end
end
def create_user(email, require_2fa: false)
users_table.create!(email: email, projects_limit: 10, require_two_factor_authentication_from_group: require_2fa)
end
def create_group_member(user, group)
members_table.create!(user_id: user.id, source_id: group.id, access_level: GroupMember::MAINTAINER, source_type: "Namespace", type: "GroupMember", notification_level: 3)
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 = {
......
...@@ -12,20 +12,30 @@ RSpec.describe Gitlab::X509::Signature do ...@@ -12,20 +12,30 @@ RSpec.describe Gitlab::X509::Signature do
end end
shared_examples "a verified signature" do shared_examples "a verified signature" do
it 'returns a verified signature if email does match' do let_it_be(:user) { create(:user, email: X509Helpers::User1.certificate_email) }
signature = described_class.new(
subject(:signature) do
described_class.new(
X509Helpers::User1.signed_commit_signature, X509Helpers::User1.signed_commit_signature,
X509Helpers::User1.signed_commit_base_data, X509Helpers::User1.signed_commit_base_data,
X509Helpers::User1.certificate_email, X509Helpers::User1.certificate_email,
X509Helpers::User1.signed_commit_time X509Helpers::User1.signed_commit_time
) )
end
it 'returns a verified signature if email does match' do
expect(signature.x509_certificate).to have_attributes(certificate_attributes) expect(signature.x509_certificate).to have_attributes(certificate_attributes)
expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes) expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
expect(signature.verified_signature).to be_truthy expect(signature.verified_signature).to be_truthy
expect(signature.verification_status).to eq(:verified) expect(signature.verification_status).to eq(:verified)
end end
it "returns an unverified signature if the email matches but isn't confirmed" do
user.update!(confirmed_at: nil)
expect(signature.verification_status).to eq(:unverified)
end
it 'returns an unverified signature if email does not match' do it 'returns an unverified signature if email does not match' do
signature = described_class.new( signature = described_class.new(
X509Helpers::User1.signed_commit_signature, X509Helpers::User1.signed_commit_signature,
...@@ -55,13 +65,6 @@ RSpec.describe Gitlab::X509::Signature do ...@@ -55,13 +65,6 @@ RSpec.describe Gitlab::X509::Signature do
end end
it 'returns an unverified signature if certificate is revoked' do it 'returns an unverified signature if certificate is revoked' do
signature = described_class.new(
X509Helpers::User1.signed_commit_signature,
X509Helpers::User1.signed_commit_base_data,
X509Helpers::User1.certificate_email,
X509Helpers::User1.signed_commit_time
)
expect(signature.verification_status).to eq(:verified) expect(signature.verification_status).to eq(:verified)
signature.x509_certificate.revoked! signature.x509_certificate.revoked!
...@@ -253,23 +256,25 @@ RSpec.describe Gitlab::X509::Signature do ...@@ -253,23 +256,25 @@ RSpec.describe Gitlab::X509::Signature do
end end
describe '#user' do describe '#user' do
signature = described_class.new( subject do
described_class.new(
X509Helpers::User1.signed_tag_signature, X509Helpers::User1.signed_tag_signature,
X509Helpers::User1.signed_tag_base_data, X509Helpers::User1.signed_tag_base_data,
X509Helpers::User1.certificate_email, X509Helpers::User1.certificate_email,
X509Helpers::User1.signed_commit_time X509Helpers::User1.signed_commit_time
) ).user
end
context 'if email is assigned to a user' do context 'if email is assigned to a user' do
let!(:user) { create(:user, email: X509Helpers::User1.certificate_email) } let!(:user) { create(:user, email: X509Helpers::User1.certificate_email) }
it 'returns user' do it 'returns user' do
expect(signature.user).to eq(user) is_expected.to eq(user)
end end
end end
it 'if email is not assigned to a user, return nil' do it 'if email is not assigned to a user, return nil' do
expect(signature.user).to be_nil is_expected.to be_nil
end end
end end
...@@ -292,6 +297,17 @@ RSpec.describe Gitlab::X509::Signature do ...@@ -292,6 +297,17 @@ RSpec.describe Gitlab::X509::Signature do
end end
context 'verified signature' do context 'verified signature' do
let_it_be(:user) { create(:user, email: X509Helpers::User1.certificate_email) }
subject(:signature) do
described_class.new(
X509Helpers::User1.signed_tag_signature,
X509Helpers::User1.signed_tag_base_data,
X509Helpers::User1.certificate_email,
X509Helpers::User1.signed_commit_time
)
end
context 'with trusted certificate store' do context 'with trusted certificate store' do
before do before do
store = OpenSSL::X509::Store.new store = OpenSSL::X509::Store.new
...@@ -301,19 +317,18 @@ RSpec.describe Gitlab::X509::Signature do ...@@ -301,19 +317,18 @@ RSpec.describe Gitlab::X509::Signature do
end end
it 'returns a verified signature if email does match' do it 'returns a verified signature if email does match' do
signature = described_class.new(
X509Helpers::User1.signed_tag_signature,
X509Helpers::User1.signed_tag_base_data,
X509Helpers::User1.certificate_email,
X509Helpers::User1.signed_commit_time
)
expect(signature.x509_certificate).to have_attributes(certificate_attributes) expect(signature.x509_certificate).to have_attributes(certificate_attributes)
expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes) expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
expect(signature.verified_signature).to be_truthy expect(signature.verified_signature).to be_truthy
expect(signature.verification_status).to eq(:verified) expect(signature.verification_status).to eq(:verified)
end end
it "returns an unverified signature if the email matches but isn't confirmed" do
user.update!(confirmed_at: nil)
expect(signature.verification_status).to eq(:unverified)
end
it 'returns an unverified signature if email does not match' do it 'returns an unverified signature if email does not match' do
signature = described_class.new( signature = described_class.new(
X509Helpers::User1.signed_tag_signature, X509Helpers::User1.signed_tag_signature,
...@@ -343,13 +358,6 @@ RSpec.describe Gitlab::X509::Signature do ...@@ -343,13 +358,6 @@ RSpec.describe Gitlab::X509::Signature do
end end
it 'returns an unverified signature if certificate is revoked' do it 'returns an unverified signature if certificate is revoked' do
signature = described_class.new(
X509Helpers::User1.signed_tag_signature,
X509Helpers::User1.signed_tag_base_data,
X509Helpers::User1.certificate_email,
X509Helpers::User1.signed_commit_time
)
expect(signature.verification_status).to eq(:verified) expect(signature.verification_status).to eq(:verified)
signature.x509_certificate.revoked! signature.x509_certificate.revoked!
...@@ -368,13 +376,6 @@ RSpec.describe Gitlab::X509::Signature do ...@@ -368,13 +376,6 @@ RSpec.describe Gitlab::X509::Signature do
end end
it 'returns an unverified signature' do it 'returns an unverified signature' do
signature = described_class.new(
X509Helpers::User1.signed_tag_signature,
X509Helpers::User1.signed_tag_base_data,
X509Helpers::User1.certificate_email,
X509Helpers::User1.signed_commit_time
)
expect(signature.x509_certificate).to have_attributes(certificate_attributes) expect(signature.x509_certificate).to have_attributes(certificate_attributes)
expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes) expect(signature.x509_certificate.x509_issuer).to have_attributes(issuer_attributes)
expect(signature.verified_signature).to be_falsey expect(signature.verified_signature).to be_falsey
......
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20210519154058_schedule_update_users_where_two_factor_auth_required_from_group.rb')
RSpec.describe ScheduleUpdateUsersWhereTwoFactorAuthRequiredFromGroup do
let(:users) { table(:users) }
let!(:user_1) { users.create!(require_two_factor_authentication_from_group: false, name: "user1", email: "user1@example.com", projects_limit: 1) }
let!(:user_2) { users.create!(require_two_factor_authentication_from_group: true, name: "user2", email: "user2@example.com", projects_limit: 1) }
let!(:user_3) { users.create!(require_two_factor_authentication_from_group: false, name: "user3", email: "user3@example.com", projects_limit: 1) }
before do
stub_const("#{described_class.name}::BATCH_SIZE", 1)
end
it 'schedules jobs for users that do not require two factor authentication' do
Sidekiq::Testing.fake! do
freeze_time do
migrate!
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
2.minutes, user_1.id, user_1.id)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
4.minutes, user_3.id, user_3.id)
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
end
end
end
end
...@@ -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
......
...@@ -8,12 +8,13 @@ RSpec.describe 'gitlab:x509 namespace rake task' do ...@@ -8,12 +8,13 @@ RSpec.describe 'gitlab:x509 namespace rake task' do
end end
describe 'update_signatures' do describe 'update_signatures' do
subject { run_rake_task('gitlab:x509:update_signatures') } let(:user) { create(:user, email: X509Helpers::User1.certificate_email) }
let(:project) { create(:project, :repository, path: X509Helpers::User1.path, creator: user) }
let(:project) { create :project, :repository, path: X509Helpers::User1.path }
let(:x509_signed_commit) { project.commit_by(oid: '189a6c924013fc3fe40d6f1ec1dc20214183bc97') } let(:x509_signed_commit) { project.commit_by(oid: '189a6c924013fc3fe40d6f1ec1dc20214183bc97') }
let(:x509_commit) { Gitlab::X509::Commit.new(x509_signed_commit).signature } let(:x509_commit) { Gitlab::X509::Commit.new(x509_signed_commit).signature }
subject { run_rake_task('gitlab:x509:update_signatures') }
it 'changes from unverified to verified if the certificate store contains the root certificate' do it 'changes from unverified to verified if the certificate store contains the root certificate' do
x509_commit x509_commit
...@@ -22,21 +23,14 @@ RSpec.describe 'gitlab:x509 namespace rake task' do ...@@ -22,21 +23,14 @@ RSpec.describe 'gitlab:x509 namespace rake task' do
store.add_cert(certificate) store.add_cert(certificate)
allow(OpenSSL::X509::Store).to receive(:new).and_return(store) allow(OpenSSL::X509::Store).to receive(:new).and_return(store)
expect(x509_commit.verification_status).to eq('unverified')
expect_any_instance_of(Gitlab::X509::Commit).to receive(:update_signature!).and_call_original expect_any_instance_of(Gitlab::X509::Commit).to receive(:update_signature!).and_call_original
expect { subject }.to change { x509_commit.reload.verification_status }.from('unverified').to('verified')
subject
x509_commit.reload
expect(x509_commit.verification_status).to eq('verified')
end end
it 'returns if no signature is available' do it 'returns if no signature is available' do
expect_any_instance_of(Gitlab::X509::Commit) do |x509_commit| expect_any_instance_of(Gitlab::X509::Commit).not_to receive(:update_signature!)
expect(x509_commit).not_to receive(:update_signature!)
subject subject
end end
end end
end
end end
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