diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index a0ac141fefd..f483d5261c5 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -7807bc4670c11dad1d2981e6d1090fe0ddd9e088 +37934de32d680c4166ec35dcd47b59fa0cc8c397 diff --git a/GITLAB_ELASTICSEARCH_INDEXER_VERSION b/GITLAB_ELASTICSEARCH_INDEXER_VERSION index 46b81d815a2..d8b698973a4 100644 --- a/GITLAB_ELASTICSEARCH_INDEXER_VERSION +++ b/GITLAB_ELASTICSEARCH_INDEXER_VERSION @@ -1 +1 @@ -2.11.0 +2.12.0 diff --git a/Gemfile.lock b/Gemfile.lock index 56310ec6b89..f30b1cde549 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -146,7 +146,7 @@ GEM coderay (>= 1.0.0) erubi (>= 1.0.0) rack (>= 0.9.0) - bindata (2.4.8) + bindata (2.4.10) binding_ninja (0.2.3) bootsnap (1.4.6) msgpack (~> 1.0) diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue index 9bf26e5a182..a7fcce02ab3 100644 --- a/app/assets/javascripts/notebook/cells/markdown.vue +++ b/app/assets/javascripts/notebook/cells/markdown.vue @@ -195,6 +195,7 @@ export default { 'var', ], ALLOWED_ATTR: ['class', 'style', 'href', 'src'], + ALLOW_DATA_ATTR: false, }); }, }, diff --git a/app/controllers/jira_connect/app_descriptor_controller.rb b/app/controllers/jira_connect/app_descriptor_controller.rb index a6415daef68..0de42ad2452 100644 --- a/app/controllers/jira_connect/app_descriptor_controller.rb +++ b/app/controllers/jira_connect/app_descriptor_controller.rb @@ -31,6 +31,7 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController scopes: %w(READ WRITE DELETE), apiVersion: 1, apiMigrations: { + 'context-qsh': true, gdpr: true } } diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index 857f36e3833..ddf70c1892a 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -14,8 +14,9 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController if pre_auth.authorizable? if skip_authorization? || matching_token? auth = authorization.authorize + parsed_redirect_uri = URI.parse(auth.redirect_uri) session.delete(:user_return_to) - redirect_to auth.redirect_uri + render "doorkeeper/authorizations/redirect", locals: { redirect_uri: parsed_redirect_uri }, layout: false else render "doorkeeper/authorizations/new" end diff --git a/app/graphql/resolvers/timelog_resolver.rb b/app/graphql/resolvers/timelog_resolver.rb index aebd04259ee..8ac30c4cb5d 100644 --- a/app/graphql/resolvers/timelog_resolver.rb +++ b/app/graphql/resolvers/timelog_resolver.rb @@ -7,106 +7,88 @@ module Resolvers type ::Types::TimelogType.connection_type, null: false argument :start_date, Types::TimeType, - required: false, - description: 'List time logs within a date range where the logged date is equal to or after startDate.' + required: false, + description: 'List time logs within a date range where the logged date is equal to or after startDate.' argument :end_date, Types::TimeType, - required: false, - description: 'List time logs within a date range where the logged date is equal to or before endDate.' + required: false, + description: 'List time logs within a date range where the logged date is equal to or before endDate.' argument :start_time, Types::TimeType, - required: false, - description: 'List time-logs within a time range where the logged time is equal to or after startTime.' + required: false, + description: 'List time-logs within a time range where the logged time is equal to or after startTime.' argument :end_time, Types::TimeType, - required: false, - description: 'List time-logs within a time range where the logged time is equal to or before endTime.' + required: false, + description: 'List time-logs within a time range where the logged time is equal to or before endTime.' def resolve_with_lookahead(**args) - return Timelog.none unless timelogs_available_for_user? + build_timelogs - validate_params_presence!(args) - transformed_args = transform_args(args) - validate_time_difference!(transformed_args) + if args.any? + validate_args!(args) + build_parsed_args(args) + validate_time_difference! + apply_time_filter + end - find_timelogs(transformed_args) + apply_lookahead(timelogs) end private + attr_reader :parsed_args, :timelogs + def preloads { note: [:note] } end - def find_timelogs(args) - apply_lookahead(group.timelogs(args[:start_time], args[:end_time])) - end - - def timelogs_available_for_user? - group&.user_can_access_group_timelogs?(context[:current_user]) - end - - def validate_params_presence!(args) - message = case time_params_count(args) - when 0 - 'Start and End arguments must be present' - when 1 - 'Both Start and End arguments must be present' - when 2 - validate_duplicated_args(args) - when 3 || 4 - 'Only Time or Date arguments must be present' - end - - raise_argument_error(message) if message - end - - def validate_time_difference!(args) - message = if args[:end_time] < args[:start_time] - 'Start argument must be before End argument' - elsif args[:end_time] - args[:start_time] > 60.days - 'The time range period cannot contain more than 60 days' - end - - raise_argument_error(message) if message - end - - def transform_args(args) - return args if args.keys == [:start_time, :end_time] - - time_args = args.except(:start_date, :end_date) - - if time_args.empty? - time_args[:start_time] = args[:start_date].beginning_of_day - time_args[:end_time] = args[:end_date].end_of_day - elsif time_args.key?(:start_time) - time_args[:end_time] = args[:end_date].end_of_day - elsif time_args.key?(:end_time) - time_args[:start_time] = args[:start_date].beginning_of_day + def validate_args!(args) + if args[:start_time] && args[:start_date] + raise_argument_error('Provide either a start date or time, but not both') + elsif args[:end_time] && args[:end_date] + raise_argument_error('Provide either an end date or time, but not both') end - - time_args end - def time_params_count(args) - [:start_time, :end_time, :start_date, :end_date].count { |param| args.key?(param) } - end + def build_parsed_args(args) + if times_provided?(args) + @parsed_args = args + else + @parsed_args = args.except(:start_date, :end_date) - def validate_duplicated_args(args) - if args.key?(:start_time) && args.key?(:start_date) || - args.key?(:end_time) && args.key?(:end_date) - 'Both Start and End arguments must be present' + @parsed_args[:start_time] = args[:start_date].beginning_of_day if args[:start_date] + @parsed_args[:end_time] = args[:end_date].end_of_day if args[:end_date] end end + def times_provided?(args) + args[:start_time] && args[:end_time] + end + + def validate_time_difference! + return unless end_time_before_start_time? + + raise_argument_error('Start argument must be before End argument') + end + + def end_time_before_start_time? + times_provided?(parsed_args) && parsed_args[:end_time] < parsed_args[:start_time] + end + + def build_timelogs + @timelogs = Timelog.in_group(object) + end + + def apply_time_filter + @timelogs = timelogs.at_or_after(parsed_args[:start_time]) if parsed_args[:start_time] + @timelogs = timelogs.at_or_before(parsed_args[:end_time]) if parsed_args[:end_time] + end + def raise_argument_error(message) raise Gitlab::Graphql::Errors::ArgumentError, message end - - def group - @group ||= object.respond_to?(:sync) ? object.sync : object - end end end diff --git a/app/graphql/types/timelog_type.rb b/app/graphql/types/timelog_type.rb index 99a619f1b1d..925a522629e 100644 --- a/app/graphql/types/timelog_type.rb +++ b/app/graphql/types/timelog_type.rb @@ -4,7 +4,7 @@ module Types class TimelogType < BaseObject graphql_name 'Timelog' - authorize :read_group_timelogs + authorize :read_issue field :spent_at, Types::TimeType, diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index 05a55a09271..7525481047e 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -118,6 +118,7 @@ module MarkupHelper def markup(file_name, text, context = {}) context[:project] ||= @project + context[:text_source] ||= :blob html = context.delete(:rendered) || markup_unsafe(file_name, text, context) prepare_for_rendering(html, context) end diff --git a/app/models/concerns/has_timelogs_report.rb b/app/models/concerns/has_timelogs_report.rb deleted file mode 100644 index 3af063438bf..00000000000 --- a/app/models/concerns/has_timelogs_report.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module HasTimelogsReport - extend ActiveSupport::Concern - include Gitlab::Utils::StrongMemoize - - def timelogs(start_time, end_time) - strong_memoize(:timelogs) { timelogs_for(start_time, end_time) } - end - - def user_can_access_group_timelogs?(current_user) - Ability.allowed?(current_user, :read_group_timelogs, self) - end - - private - - def timelogs_for(start_time, end_time) - Timelog.between_times(start_time, end_time).in_group(self) - end -end diff --git a/app/models/group.rb b/app/models/group.rb index 5c991f74208..bbae40f4442 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -16,7 +16,6 @@ class Group < Namespace include Gitlab::Utils::StrongMemoize include GroupAPICompatibility include EachBatch - include HasTimelogsReport include BulkMemberAccessLoad has_many :all_group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb index 61e89125b5f..c7bc31cde5d 100644 --- a/app/models/members/group_member.rb +++ b/app/models/members/group_member.rb @@ -36,6 +36,10 @@ class GroupMember < Member Gitlab::Access.sym_options_with_owner end + def self.pluck_user_ids + pluck(:user_id) + end + def group source end diff --git a/app/models/project.rb b/app/models/project.rb index fb6e6437964..53054e5304f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -900,6 +900,10 @@ class Project < ApplicationRecord alias_method :ancestors, :ancestors_upto + def ancestors_upto_ids(...) + ancestors_upto(...).pluck(:id) + end + def emails_disabled? strong_memoize(:emails_disabled) do # disabling in the namespace overrides the project setting diff --git a/app/models/timelog.rb b/app/models/timelog.rb index bd543526685..96fd485b797 100644 --- a/app/models/timelog.rb +++ b/app/models/timelog.rb @@ -18,8 +18,12 @@ class Timelog < ApplicationRecord joins(:project).where(projects: { namespace: group.self_and_descendants }) end - scope :between_times, -> (start_time, end_time) do - where('spent_at BETWEEN ? AND ?', start_time, end_time) + scope :at_or_after, -> (start_time) do + where('spent_at >= ?', start_time) + end + + scope :at_or_before, -> (end_time) do + where('spent_at <= ?', end_time) end def issuable diff --git a/app/policies/concerns/policy_actor.rb b/app/policies/concerns/policy_actor.rb index cd19b46ad6c..08a26da6673 100644 --- a/app/policies/concerns/policy_actor.rb +++ b/app/policies/concerns/policy_actor.rb @@ -80,6 +80,10 @@ module PolicyActor def can_read_all_resources? false end + + def password_expired? + false + end end PolicyActor.prepend_mod_with('PolicyActor') diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 85263ec7c87..73757891cd6 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -15,6 +15,10 @@ class GlobalPolicy < BasePolicy @user&.required_terms_not_accepted? end + condition(:password_expired, scope: :user) do + @user&.password_expired? + end + condition(:project_bot, scope: :user) { @user&.project_bot? } condition(:migration_bot, scope: :user) { @user&.migration_bot? } @@ -73,6 +77,12 @@ class GlobalPolicy < BasePolicy prevent :access_git end + rule { password_expired }.policy do + prevent :access_api + prevent :access_git + prevent :use_slash_commands + end + rule { can_create_group }.policy do enable :create_group end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 821fabec266..dc8ecfa4333 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -131,7 +131,6 @@ class GroupPolicy < BasePolicy enable :read_prometheus enable :read_package enable :read_package_settings - enable :read_group_timelogs end rule { maintainer }.policy do diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 1ce19511bef..5e9633d1c8a 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -263,7 +263,6 @@ class ProjectPolicy < BasePolicy enable :read_confidential_issues enable :read_package enable :read_product_analytics - enable :read_group_timelogs end # We define `:public_user_access` separately because there are cases in gitlab-ee diff --git a/app/policies/timelog_policy.rb b/app/policies/timelog_policy.rb index 0598817d4e0..f71c4204639 100644 --- a/app/policies/timelog_policy.rb +++ b/app/policies/timelog_policy.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class TimelogPolicy < BasePolicy - delegate { @subject.issuable.resource_parent } + delegate { @subject.issuable } end diff --git a/app/serializers/member_entity.rb b/app/serializers/member_entity.rb index 7559a03bd3b..5100a41638e 100644 --- a/app/serializers/member_entity.rb +++ b/app/serializers/member_entity.rb @@ -40,7 +40,9 @@ class MemberEntity < Grape::Entity 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 :email do |member| diff --git a/app/views/doorkeeper/authorizations/redirect.html.haml b/app/views/doorkeeper/authorizations/redirect.html.haml new file mode 100644 index 00000000000..9580f33c88a --- /dev/null +++ b/app/views/doorkeeper/authorizations/redirect.html.haml @@ -0,0 +1,8 @@ +%h3.page-title= _("Redirecting") + +%div + %a{ :href => redirect_uri } Click here to redirect to #{redirect_uri} + += javascript_tag do + :plain + window.location= "#{redirect_uri}"; diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 42bb8117766..6a626d445ee 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -5,7 +5,10 @@ = render_if_exists "shared/shared_runners_minutes_limit_flash_message" -#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json, code_quality_walkthrough: params[:code_quality_walkthrough]), +- list_url = project_pipelines_path(@project, format: :json, code_quality_walkthrough: params[:code_quality_walkthrough]) +- add_page_startup_api_call list_url + +#pipelines-list-vue{ data: { endpoint: list_url, project_id: @project.id, params: params.to_json, "artifacts-endpoint" => downloadable_artifacts_project_pipeline_path(@project, artifacts_endpoint_placeholder, format: :json), diff --git a/config/feature_flags/development/ci_fix_commit_status_retried.yml b/config/feature_flags/development/ci_fix_commit_status_retried.yml index 85b1836b065..56400c861cd 100644 --- a/config/feature_flags/development/ci_fix_commit_status_retried.yml +++ b/config/feature_flags/development/ci_fix_commit_status_retried.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321631 milestone: '13.9' type: development group: group::pipeline authoring -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/group_level_protected_environments.yml b/config/feature_flags/development/group_level_protected_environments.yml new file mode 100644 index 00000000000..598513d4283 --- /dev/null +++ b/config/feature_flags/development/group_level_protected_environments.yml @@ -0,0 +1,8 @@ +--- +name: group_level_protected_environments +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61575 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331085 +milestone: '14.0' +type: development +group: group::release +default_enabled: false diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb index e8479bc6aa4..61e357808d9 100644 --- a/config/initializers/lograge.rb +++ b/config/initializers/lograge.rb @@ -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[: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[:location] = Gitlab::Utils.removes_sensitive_data_from_url(data[:location]) if data[:location] # Remove empty hashes to prevent type mismatches # These are set to empty hashes in Lograge's ActionCable subscriber diff --git a/db/migrate/20210519154058_schedule_update_users_where_two_factor_auth_required_from_group.rb b/db/migrate/20210519154058_schedule_update_users_where_two_factor_auth_required_from_group.rb new file mode 100644 index 00000000000..2da84301a72 --- /dev/null +++ b/db/migrate/20210519154058_schedule_update_users_where_two_factor_auth_required_from_group.rb @@ -0,0 +1,34 @@ +# 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 diff --git a/db/migrate/20210520102039_group_protected_environments_add_column.rb b/db/migrate/20210520102039_group_protected_environments_add_column.rb new file mode 100644 index 00000000000..642e22c5f48 --- /dev/null +++ b/db/migrate/20210520102039_group_protected_environments_add_column.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class GroupProtectedEnvironmentsAddColumn < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + def up + add_column :protected_environments, :group_id, :bigint + change_column_null :protected_environments, :project_id, true + end + + def down + change_column_null :protected_environments, :project_id, false + remove_column :protected_environments, :group_id + end +end diff --git a/db/migrate/20210601080039_group_protected_environments_add_index_and_constraint.rb b/db/migrate/20210601080039_group_protected_environments_add_index_and_constraint.rb new file mode 100644 index 00000000000..611619e496c --- /dev/null +++ b/db/migrate/20210601080039_group_protected_environments_add_index_and_constraint.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class GroupProtectedEnvironmentsAddIndexAndConstraint < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + INDEX_NAME = 'index_protected_environments_on_group_id_and_name' + + disable_ddl_transaction! + + def up + add_concurrent_index :protected_environments, [:group_id, :name], unique: true, + name: INDEX_NAME, where: 'group_id IS NOT NULL' + add_concurrent_foreign_key :protected_environments, :namespaces, column: :group_id, on_delete: :cascade + + add_check_constraint :protected_environments, + "((project_id IS NULL) != (group_id IS NULL))", + :protected_environments_project_or_group_existence + end + + def down + remove_group_protected_environments! + + remove_check_constraint :protected_environments, :protected_environments_project_or_group_existence + remove_foreign_key_if_exists :protected_environments, column: :group_id + remove_concurrent_index_by_name :protected_environments, name: INDEX_NAME + end + + private + + def remove_group_protected_environments! + execute <<-SQL + DELETE FROM protected_environments WHERE group_id IS NOT NULL + SQL + end +end diff --git a/db/post_migrate/20210519220019_backfill_escalation_policies_for_oncall_schedules.rb b/db/post_migrate/20210519220019_backfill_escalation_policies_for_oncall_schedules.rb new file mode 100644 index 00000000000..f972815cf67 --- /dev/null +++ b/db/post_migrate/20210519220019_backfill_escalation_policies_for_oncall_schedules.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +class BackfillEscalationPoliciesForOncallSchedules < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + # Creates a single new escalation policy for projects which have + # existing on-call schedules. Only one schedule is expected + # per project, but it is possible to have multiple. + # + # An escalation rule is created for each existing schedule, + # configured to immediately notify the schedule of an incoming + # alert payload unless the alert has already been acknowledged. + # For projects with multiple schedules, the name of the first saved + # schedule will be used for the policy's description. + # + # Skips projects which already have escalation policies & schedules. + # + # EX) + # For these existing records: + # Project #3 + # IncidentManagement::OncallSchedules #13 + # project_id: 3 + # name: 'Awesome Schedule' + # description: null + # IncidentManagement::OncallSchedules #14 + # project_id: 3 + # name: '2ndary sched' + # description: 'Backup on-call' + # + # These will be inserted: + # EscalationPolicy #1 + # project_id: 3 + # name: 'On-call Escalation Policy' + # description: 'Immediately notify Awesome Schedule' + # EscalationRule #1 + # policy_id: 1, + # oncall_schedule_id: 13 + # status: 1 # Acknowledged status + # elapsed_time_seconds: 0 + # EscalationRule #2 + # policy_id: 1, + # oncall_schedule_id: 14 + # status: 1 # Acknowledged status + # elapsed_time_seconds: 0 + def up + ApplicationRecord.connection.exec_query(<<~SQL.squish) + WITH new_escalation_policies AS ( + INSERT INTO incident_management_escalation_policies ( + project_id, + name, + description + ) + SELECT + DISTINCT ON (project_id) project_id, + 'On-call Escalation Policy', + CONCAT('Immediately notify ', name) + FROM incident_management_oncall_schedules + WHERE project_id NOT IN ( + SELECT DISTINCT project_id + FROM incident_management_escalation_policies + ) + ORDER BY project_id, id + RETURNING id, project_id + ) + + INSERT INTO incident_management_escalation_rules ( + policy_id, + oncall_schedule_id, + status, + elapsed_time_seconds + ) + SELECT + new_escalation_policies.id, + incident_management_oncall_schedules.id, + 1, + 0 + FROM new_escalation_policies + INNER JOIN incident_management_oncall_schedules + ON new_escalation_policies.project_id = incident_management_oncall_schedules.project_id + SQL + end + + # There is no way to distinguish between policies created + # via the backfill or as a result of a user creating a new + # on-call schedule. + def down + # no-op + end +end diff --git a/db/schema_migrations/20210519154058 b/db/schema_migrations/20210519154058 new file mode 100644 index 00000000000..9bd277e92db --- /dev/null +++ b/db/schema_migrations/20210519154058 @@ -0,0 +1 @@ +bdd82fc5cb2bbb322125c153c741002725853e23cd0ae0edbfd80563a4a87f2f \ No newline at end of file diff --git a/db/schema_migrations/20210519220019 b/db/schema_migrations/20210519220019 new file mode 100644 index 00000000000..c0578586e62 --- /dev/null +++ b/db/schema_migrations/20210519220019 @@ -0,0 +1 @@ +6c687ffd41f242dcd0ecf1ff82652aba79130d2d54016729a817dafa0bac6184 \ No newline at end of file diff --git a/db/schema_migrations/20210520102039 b/db/schema_migrations/20210520102039 new file mode 100644 index 00000000000..a850a5b07ca --- /dev/null +++ b/db/schema_migrations/20210520102039 @@ -0,0 +1 @@ +88d2c1507503de626dfdb3f2f0eaf0f51fad5fc2279fd147d901c5dcc7ae91eb \ No newline at end of file diff --git a/db/schema_migrations/20210601080039 b/db/schema_migrations/20210601080039 new file mode 100644 index 00000000000..91d517058fe --- /dev/null +++ b/db/schema_migrations/20210601080039 @@ -0,0 +1 @@ +2c5c0756757a181cf8bf7968de5184664004a82c093ae3fc14c5d6931a1ab44f \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 40eec742ac1..8f66b4a719a 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -17168,10 +17168,12 @@ ALTER SEQUENCE protected_environment_deploy_access_levels_id_seq OWNED BY protec CREATE TABLE protected_environments ( id integer NOT NULL, - project_id integer NOT NULL, + project_id integer, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, - name character varying NOT NULL + name character varying NOT NULL, + group_id bigint, + CONSTRAINT protected_environments_project_or_group_existence CHECK (((project_id IS NULL) <> (group_id IS NULL))) ); CREATE SEQUENCE protected_environments_id_seq @@ -24285,6 +24287,8 @@ CREATE INDEX index_protected_environment_deploy_access_levels_on_group_id ON pro CREATE INDEX index_protected_environment_deploy_access_levels_on_user_id ON protected_environment_deploy_access_levels USING btree (user_id); +CREATE UNIQUE INDEX index_protected_environments_on_group_id_and_name ON protected_environments USING btree (group_id, name) WHERE (group_id IS NOT NULL); + CREATE INDEX index_protected_environments_on_project_id ON protected_environments USING btree (project_id); CREATE UNIQUE INDEX index_protected_environments_on_project_id_and_name ON protected_environments USING btree (project_id, name); @@ -24767,6 +24771,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 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_star_projects_on_project_id ON users_star_projects USING btree (project_id); @@ -25718,6 +25724,9 @@ ALTER TABLE ONLY issues ALTER TABLE ONLY epics ADD CONSTRAINT fk_9d480c64b2 FOREIGN KEY (start_date_sourcing_epic_id) REFERENCES epics(id) ON DELETE SET NULL; +ALTER TABLE ONLY protected_environments + ADD CONSTRAINT fk_9e112565b7 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY alert_management_alerts ADD CONSTRAINT fk_9e49e5c2b7 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; diff --git a/doc/user/admin_area/settings/import_export_rate_limits.md b/doc/user/admin_area/settings/import_export_rate_limits.md index c6b965c18d3..b62e6eb0c3d 100644 --- a/doc/user/admin_area/settings/import_export_rate_limits.md +++ b/doc/user/admin_area/settings/import_export_rate_limits.md @@ -5,7 +5,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Project/Group Import/Export rate limits +# Project/group import/export rate limits **(FREE SELF)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35728) in GitLab 13.2. diff --git a/doc/user/group/settings/import_export.md b/doc/user/group/settings/import_export.md index 5d375e2abd9..f18186711b5 100644 --- a/doc/user/group/settings/import_export.md +++ b/doc/user/group/settings/import_export.md @@ -4,7 +4,7 @@ stage: Manage group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Group Import/Export +# Group import/export **(FREE)** > - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2888) in GitLab 13.0 as an experimental feature. May change in future releases. diff --git a/doc/user/permissions.md b/doc/user/permissions.md index d73e377e771..1b9a4629cb8 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -80,6 +80,7 @@ The following table lists project permissions available for each role: | Label issues | | ✓ | ✓ | ✓ | ✓ | | Set issue weight | | ✓ | ✓ | ✓ | ✓ | | [Set issue estimate and record time spent](project/time_tracking.md) | | ✓ | ✓ | ✓ | ✓ | +| View a time tracking report | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | Lock issue threads | | ✓ | ✓ | ✓ | ✓ | | Manage issue tracker | | ✓ | ✓ | ✓ | ✓ | | Manage linked issues | | ✓ | ✓ | ✓ | ✓ | diff --git a/doc/user/project/import/bitbucket.md b/doc/user/project/import/bitbucket.md index e77932c6427..802eb3efc51 100644 --- a/doc/user/project/import/bitbucket.md +++ b/doc/user/project/import/bitbucket.md @@ -5,7 +5,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Import your project from Bitbucket Cloud to GitLab +# Import your project from Bitbucket Cloud to GitLab **(FREE)** NOTE: The Bitbucket Cloud importer works only with Bitbucket.org, not with Bitbucket diff --git a/doc/user/project/import/bitbucket_server.md b/doc/user/project/import/bitbucket_server.md index 1e79107d76f..963b9f524ff 100644 --- a/doc/user/project/import/bitbucket_server.md +++ b/doc/user/project/import/bitbucket_server.md @@ -5,7 +5,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Import your project from Bitbucket Server to GitLab +# Import your project from Bitbucket Server to GitLab **(FREE)** > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/20164) in GitLab 11.2. diff --git a/doc/user/project/import/clearcase.md b/doc/user/project/import/clearcase.md index 7e07ca6f865..27a84476590 100644 --- a/doc/user/project/import/clearcase.md +++ b/doc/user/project/import/clearcase.md @@ -5,7 +5,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Migrating from ClearCase +# Migrating from ClearCase **(FREE)** [ClearCase](https://www.ibm.com/products/rational-clearcase) is a set of tools developed by IBM which also include a centralized version control system diff --git a/doc/user/project/import/cvs.md b/doc/user/project/import/cvs.md index 61d4d29aa4d..55c5feff1f0 100644 --- a/doc/user/project/import/cvs.md +++ b/doc/user/project/import/cvs.md @@ -5,7 +5,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Migrating from CVS +# Migrating from CVS **(FREE)** [CVS](https://savannah.nongnu.org/projects/cvs) is an old centralized version control system similar to [SVN](svn.md). diff --git a/doc/user/project/import/fogbugz.md b/doc/user/project/import/fogbugz.md index 09505d94a8c..d3d77f16200 100644 --- a/doc/user/project/import/fogbugz.md +++ b/doc/user/project/import/fogbugz.md @@ -5,7 +5,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Import your project from FogBugz to GitLab +# Import your project from FogBugz to GitLab **(FREE)** It only takes a few simple steps to import your project from FogBugz. The importer imports all your cases and comments with original case diff --git a/doc/user/project/import/gitea.md b/doc/user/project/import/gitea.md index 41141902468..9364ac4f954 100644 --- a/doc/user/project/import/gitea.md +++ b/doc/user/project/import/gitea.md @@ -5,7 +5,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Import your project from Gitea to GitLab +# Import your project from Gitea to GitLab **(FREE)** Import your projects from Gitea to GitLab with minimal effort. diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md index c8b973b673e..99b3e1acdcf 100644 --- a/doc/user/project/import/github.md +++ b/doc/user/project/import/github.md @@ -5,7 +5,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Import your project from GitHub to GitLab +# Import your project from GitHub to GitLab **(FREE)** Using the importer, you can import your GitHub repositories to GitLab.com or to your self-managed GitLab instance. diff --git a/doc/user/project/import/gitlab_com.md b/doc/user/project/import/gitlab_com.md index 9e63b1cb617..f7eb5e43a79 100644 --- a/doc/user/project/import/gitlab_com.md +++ b/doc/user/project/import/gitlab_com.md @@ -5,7 +5,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Project importing from GitLab.com to your private GitLab instance +# Project importing from GitLab.com to your private GitLab instance **(FREE)** You can import your existing GitLab.com projects to your GitLab instance, but keep in mind that it is possible only if GitLab.com integration is enabled on your GitLab instance. diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md index 3728a486070..05fd04f6e48 100644 --- a/doc/user/project/import/index.md +++ b/doc/user/project/import/index.md @@ -5,7 +5,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Migrate projects to a GitLab instance +# Migrate projects to a GitLab instance **(FREE)** See these documents to migrate to GitLab: diff --git a/doc/user/project/import/manifest.md b/doc/user/project/import/manifest.md index 94eba319a17..131732d2bae 100644 --- a/doc/user/project/import/manifest.md +++ b/doc/user/project/import/manifest.md @@ -5,7 +5,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Import multiple repositories by uploading a manifest file +# Import multiple repositories by uploading a manifest file **(FREE)** > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/28811) in GitLab 11.2. diff --git a/doc/user/project/import/perforce.md b/doc/user/project/import/perforce.md index 8040eb07c93..f3843396b79 100644 --- a/doc/user/project/import/perforce.md +++ b/doc/user/project/import/perforce.md @@ -5,7 +5,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Migrating from Perforce Helix +# Migrating from Perforce Helix **(FREE)** [Perforce Helix](https://www.perforce.com/) provides a set of tools which also include a centralized, proprietary version control system similar to Git. diff --git a/doc/user/project/import/phabricator.md b/doc/user/project/import/phabricator.md index 30a63a72cf9..6a1370f3301 100644 --- a/doc/user/project/import/phabricator.md +++ b/doc/user/project/import/phabricator.md @@ -5,7 +5,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Import Phabricator tasks into a GitLab project +# Import Phabricator tasks into a GitLab project **(FREE SELF)** > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/60562) in GitLab 12.0. diff --git a/doc/user/project/import/repo_by_url.md b/doc/user/project/import/repo_by_url.md index 3ff612c51a7..e504f3678a7 100644 --- a/doc/user/project/import/repo_by_url.md +++ b/doc/user/project/import/repo_by_url.md @@ -5,7 +5,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Import project from repository by URL +# Import project from repository by URL **(FREE)** You can import your existing repositories by providing the Git URL: diff --git a/doc/user/project/import/svn.md b/doc/user/project/import/svn.md index e39976e00f6..78f683ab675 100644 --- a/doc/user/project/import/svn.md +++ b/doc/user/project/import/svn.md @@ -5,7 +5,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Migrating from SVN to GitLab +# Migrating from SVN to GitLab **(FREE)** Subversion (SVN) is a central version control system (VCS) while Git is a distributed version control system. There are some major differences diff --git a/doc/user/project/import/tfvc.md b/doc/user/project/import/tfvc.md index 705df686fe0..910be690d0b 100644 --- a/doc/user/project/import/tfvc.md +++ b/doc/user/project/import/tfvc.md @@ -1,11 +1,11 @@ --- -stage: none -group: unassigned +stage: Manage +group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments type: concepts --- -# Migrate from TFVC to Git +# Migrate from TFVC to Git **(FREE)** Team Foundation Server (TFS), renamed [Azure DevOps Server](https://azure.microsoft.com/en-us/services/devops/server/) in 2019, is a set of tools developed by Microsoft which also includes diff --git a/doc/user/project/issues/csv_import.md b/doc/user/project/issues/csv_import.md index de7a36a4886..02a4f6a4384 100644 --- a/doc/user/project/issues/csv_import.md +++ b/doc/user/project/issues/csv_import.md @@ -4,7 +4,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Importing issues from CSV +# Importing issues from CSV **(FREE)** > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/23532) in GitLab 11.7. diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index ebc77bb5b61..2d0eaf75ac4 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -5,7 +5,7 @@ info: "To determine the technical writer assigned to the Stage/Group associated type: reference, howto --- -# Project import/export +# Project import/export **(FREE)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/3050) in GitLab 8.9. > - From GitLab 10.0, administrators can disable the project export option on the GitLab instance. diff --git a/doc/user/project/time_tracking.md b/doc/user/project/time_tracking.md index df76c4682f3..b2eb6c15a81 100644 --- a/doc/user/project/time_tracking.md +++ b/doc/user/project/time_tracking.md @@ -82,6 +82,10 @@ To remove all the time spent at once, use `/remove_time_spent`. You can view a breakdown of time spent on an issue or merge request. +Prerequisites: + +- A minimum of [Reporter](../permissions.md#project-members-permissions) access to a private project in GitLab. + To view a time tracking report, go to an issue or a merge request and select **Time tracking report** in the right sidebar. diff --git a/lib/banzai/filter/absolute_link_filter.rb b/lib/banzai/filter/absolute_link_filter.rb index a9bdb004c4b..cc7bf3ed556 100644 --- a/lib/banzai/filter/absolute_link_filter.rb +++ b/lib/banzai/filter/absolute_link_filter.rb @@ -6,10 +6,13 @@ module Banzai module Filter # HTML filter that converts relative urls into absolute ones. class AbsoluteLinkFilter < HTML::Pipeline::Filter + CSS = 'a.gfm' + XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze + def call 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') end diff --git a/lib/banzai/filter/ascii_doc_post_processing_filter.rb b/lib/banzai/filter/ascii_doc_post_processing_filter.rb index 09f0fd7df45..83c729e13b5 100644 --- a/lib/banzai/filter/ascii_doc_post_processing_filter.rb +++ b/lib/banzai/filter/ascii_doc_post_processing_filter.rb @@ -3,14 +3,20 @@ module Banzai module 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 - doc.search('[data-math-style]').each do |node| + doc.xpath(XPATH_MATH).each do |node| node.set_attribute('class', 'code math js-render-math') end - doc.search('[data-mermaid-style]').each do |node| + doc.xpath(XPATH_MERM).each do |node| node.set_attribute('class', 'js-render-mermaid') end + doc end end diff --git a/lib/banzai/filter/base_relative_link_filter.rb b/lib/banzai/filter/base_relative_link_filter.rb index fba41496cad..b2eaeb69f61 100644 --- a/lib/banzai/filter/base_relative_link_filter.rb +++ b/lib/banzai/filter/base_relative_link_filter.rb @@ -7,6 +7,9 @@ module Banzai class BaseRelativeLinkFilter < HTML::Pipeline::Filter 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 def linkable_attributes @@ -35,7 +38,7 @@ module Banzai def fetch_linkable_attributes 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')] end diff --git a/lib/banzai/filter/color_filter.rb b/lib/banzai/filter/color_filter.rb index 0aca7441638..58e9b8cdba1 100644 --- a/lib/banzai/filter/color_filter.rb +++ b/lib/banzai/filter/color_filter.rb @@ -7,8 +7,11 @@ module Banzai class ColorFilter < HTML::Pipeline::Filter COLOR_CHIP_CLASS = 'gfm-color_chip' + CSS = 'code' + XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze + def call - doc.css('code').each do |node| + doc.xpath(XPATH).each do |node| color = ColorParser.parse(node.content) node << color_chip(color) if color end diff --git a/lib/banzai/filter/custom_emoji_filter.rb b/lib/banzai/filter/custom_emoji_filter.rb index 3171231dc9b..a5f1a22c483 100644 --- a/lib/banzai/filter/custom_emoji_filter.rb +++ b/lib/banzai/filter/custom_emoji_filter.rb @@ -11,7 +11,7 @@ module Banzai return doc unless 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 next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb index 8952a3ff6b4..9d24bf028b6 100644 --- a/lib/banzai/filter/emoji_filter.rb +++ b/lib/banzai/filter/emoji_filter.rb @@ -11,7 +11,7 @@ module Banzai IGNORE_UNICODE_EMOJIS = %w(™ © ®).freeze def call - doc.search(".//text()").each do |node| + doc.xpath('descendant-or-self::text()').each do |node| content = node.to_html next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS) diff --git a/lib/banzai/filter/footnote_filter.rb b/lib/banzai/filter/footnote_filter.rb index 5474242e03c..0f856dc0eb9 100644 --- a/lib/banzai/filter/footnote_filter.rb +++ b/lib/banzai/filter/footnote_filter.rb @@ -23,17 +23,23 @@ module Banzai FOOTNOTE_LINK_REFERENCE_PATTERN = /\A#{FOOTNOTE_LINK_ID_PREFIX}\d+\z/.freeze 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 - 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 first_footnote.parent.wrap('
') rand_suffix = "-#{random_number}" 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) - 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]) link_node[:href] += rand_suffix diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb index 6de9f2b86f6..0548d5a9997 100644 --- a/lib/banzai/filter/gollum_tags_filter.rb +++ b/lib/banzai/filter/gollum_tags_filter.rb @@ -60,7 +60,7 @@ module Banzai IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set 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 unless node.content =~ TAGS_PATTERN diff --git a/lib/banzai/filter/image_lazy_load_filter.rb b/lib/banzai/filter/image_lazy_load_filter.rb index d8b9eb29cf5..916c135b777 100644 --- a/lib/banzai/filter/image_lazy_load_filter.rb +++ b/lib/banzai/filter/image_lazy_load_filter.rb @@ -6,8 +6,11 @@ module Banzai # HTML filter that moves the value of image `src` attributes to `data-src` # so they can be lazy loaded. class ImageLazyLoadFilter < HTML::Pipeline::Filter + CSS = 'img' + XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze + def call - doc.xpath('descendant-or-self::img').each do |img| + doc.xpath(XPATH).each do |img| img.add_class('lazy') img['data-src'] = img['src'] img['src'] = LazyImageTagHelper.placeholder_image diff --git a/lib/banzai/filter/inline_diff_filter.rb b/lib/banzai/filter/inline_diff_filter.rb index 5a1c0bee32d..e47ff15e7b7 100644 --- a/lib/banzai/filter/inline_diff_filter.rb +++ b/lib/banzai/filter/inline_diff_filter.rb @@ -7,7 +7,7 @@ module Banzai IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set 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) content = node.to_html diff --git a/lib/banzai/filter/inline_metrics_redactor_filter.rb b/lib/banzai/filter/inline_metrics_redactor_filter.rb index 2259115acfc..b256815ae84 100644 --- a/lib/banzai/filter/inline_metrics_redactor_filter.rb +++ b/lib/banzai/filter/inline_metrics_redactor_filter.rb @@ -8,6 +8,7 @@ module Banzai include Gitlab::Utils::StrongMemoize METRICS_CSS_CLASS = '.js-render-metrics' + XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(METRICS_CSS_CLASS).freeze EMBED_LIMIT = 100 Route = Struct.new(:regex, :permission) @@ -41,7 +42,7 @@ module Banzai # @return [Nokogiri::XML::NodeSet] def nodes strong_memoize(:nodes) do - nodes = doc.css(METRICS_CSS_CLASS) + nodes = doc.xpath(XPATH) nodes.drop(EMBED_LIMIT).each(&:remove) nodes diff --git a/lib/banzai/filter/kroki_filter.rb b/lib/banzai/filter/kroki_filter.rb index dbd4de32a47..3803302c324 100644 --- a/lib/banzai/filter/kroki_filter.rb +++ b/lib/banzai/filter/kroki_filter.rb @@ -15,10 +15,11 @@ module Banzai .map { |diagram_type| %(pre[lang="#{diagram_type}"] > code) } .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" - doc.css(diagram_selectors).each do |node| + doc.xpath(xpath).each do |node| diagram_type = node.parent['lang'] img_tag = Nokogiri::HTML::DocumentFragment.parse(%()) node.parent.replace(img_tag) diff --git a/lib/banzai/filter/markdown_post_escape_filter.rb b/lib/banzai/filter/markdown_post_escape_filter.rb index ad32e9afbf5..b69afdcfebe 100644 --- a/lib/banzai/filter/markdown_post_escape_filter.rb +++ b/lib/banzai/filter/markdown_post_escape_filter.rb @@ -8,6 +8,11 @@ module Banzai NOT_LITERAL_REGEX = %r{#{LITERAL_KEYWORD}-((%5C|\\).+?)-#{LITERAL_KEYWORD}}.freeze SPAN_REGEX = %r{(.*?)}.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 return doc unless result[:escaped_literals] @@ -24,12 +29,12 @@ module Banzai # Banzai::Renderer::CommonMark::HTML. However, we eventually want to use # the built-in compiled renderer, rather than the ruby version, for speed. # 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['title'].value = node.attributes['title'].value.gsub(SPAN_REGEX, '\1') if node.attributes['title'] 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'] end diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb index 2247984b86d..53dafe45fb3 100644 --- a/lib/banzai/filter/math_filter.rb +++ b/lib/banzai/filter/math_filter.rb @@ -10,6 +10,11 @@ module Banzai # HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$. # 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. STYLE_ATTRIBUTE = 'data-math-style' @@ -21,7 +26,7 @@ module Banzai DOLLAR_SIGN = '$' def call - doc.css('code').each do |code| + doc.xpath(XPATH_CODE).each do |code| closing = code.next opening = code.previous @@ -39,7 +44,7 @@ module Banzai end end - doc.css('pre.code.language-math').each do |el| + doc.xpath(XPATH_MATH).each do |el| el[STYLE_ATTRIBUTE] = 'display' el[:class] += " #{TAG_CLASS}" end diff --git a/lib/banzai/filter/mermaid_filter.rb b/lib/banzai/filter/mermaid_filter.rb index f0adb83af8a..aaaf851ccf0 100644 --- a/lib/banzai/filter/mermaid_filter.rb +++ b/lib/banzai/filter/mermaid_filter.rb @@ -4,8 +4,11 @@ module Banzai module Filter class MermaidFilter < HTML::Pipeline::Filter + CSS = 'pre[lang="mermaid"] > code' + XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze + def call - doc.css('pre[lang="mermaid"] > code').add_class('js-render-mermaid') + doc.xpath(XPATH).add_class('js-render-mermaid') doc end diff --git a/lib/banzai/filter/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb index 37d4126c1ba..93370178a61 100644 --- a/lib/banzai/filter/plantuml_filter.rb +++ b/lib/banzai/filter/plantuml_filter.rb @@ -8,12 +8,15 @@ module Banzai # HTML that replaces all `code plantuml` tags with PlantUML img tags. # class PlantumlFilter < HTML::Pipeline::Filter + CSS = 'pre > code[lang="plantuml"]' + XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze + 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 - doc.css('pre > code[lang="plantuml"]').each do |node| + doc.xpath(XPATH).each do |node| img_tag = Nokogiri::HTML::DocumentFragment.parse( Asciidoctor::PlantUml::Processor.plantuml_content(node.content, {})) node.parent.replace(img_tag) diff --git a/lib/banzai/filter/suggestion_filter.rb b/lib/banzai/filter/suggestion_filter.rb index 56a14ec0737..aa1fcb1021c 100644 --- a/lib/banzai/filter/suggestion_filter.rb +++ b/lib/banzai/filter/suggestion_filter.rb @@ -7,10 +7,13 @@ module Banzai # Class used for tagging elements that should be rendered TAG_CLASS = 'js-render-suggestion' + CSS = 'pre.language-suggestion > code' + XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze + def call 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) end diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb index b16ea689d2e..f1440c13d47 100644 --- a/lib/banzai/filter/syntax_highlight_filter.rb +++ b/lib/banzai/filter/syntax_highlight_filter.rb @@ -14,8 +14,11 @@ module Banzai PARAMS_DELIMITER = ':' 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 - 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) end diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb index b362607aed2..13ca9cde567 100644 --- a/lib/banzai/filter/table_of_contents_filter.rb +++ b/lib/banzai/filter/table_of_contents_filter.rb @@ -19,6 +19,9 @@ module Banzai class TableOfContentsFilter < HTML::Pipeline::Filter include Gitlab::Utils::Markdown + CSS = 'h1, h2, h3, h4, h5, h6' + XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze + def call return doc if context[:no_header_anchors] @@ -27,7 +30,7 @@ module Banzai headers = Hash.new(0) 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 id = string_to_anchor(node.text) diff --git a/lib/banzai/filter/truncate_source_filter.rb b/lib/banzai/filter/truncate_source_filter.rb index 44f88b253d9..a21d4a44295 100644 --- a/lib/banzai/filter/truncate_source_filter.rb +++ b/lib/banzai/filter/truncate_source_filter.rb @@ -3,12 +3,29 @@ module Banzai module Filter class TruncateSourceFilter < HTML::Pipeline::TextFilter + CHARACTER_COUNT_LIMIT = 1.megabyte + USER_MSG_LIMIT = 10_000 + 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 # 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 diff --git a/lib/banzai/filter/wiki_link_filter.rb b/lib/banzai/filter/wiki_link_filter.rb index 44f13612fde..2b95d87ff8e 100644 --- a/lib/banzai/filter/wiki_link_filter.rb +++ b/lib/banzai/filter/wiki_link_filter.rb @@ -10,14 +10,21 @@ module Banzai class WikiLinkFilter < HTML::Pipeline::Filter 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 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') process_link(attr, el) diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index c6997288b65..4489fc9f3b2 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -84,7 +84,7 @@ module Gitlab Gitlab::Auth::UniqueIpsLimiter.limit_user! do user = User.by_login(login) - break if user && !user.can?(:log_in) + break if user && !can_user_login_with_non_expired_password?(user) authenticators = [] @@ -182,7 +182,7 @@ module Gitlab if valid_oauth_token?(token) 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) end @@ -200,7 +200,7 @@ module Gitlab 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)) end end @@ -285,7 +285,7 @@ module Gitlab return unless build.project.builds_enabled? 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 Gitlab::Auth::Result.new(build.user, build.project, :build, build_authentication_abilities) @@ -380,6 +380,10 @@ module Gitlab user.increment_failed_attempts! end + + def can_user_login_with_non_expired_password?(user) + user.can?(:log_in) && !user.password_expired? + end end end end diff --git a/lib/gitlab/auth/user_access_denied_reason.rb b/lib/gitlab/auth/user_access_denied_reason.rb index 36b54ba2e46..6639000dba8 100644 --- a/lib/gitlab/auth/user_access_denied_reason.rb +++ b/lib/gitlab/auth/user_access_denied_reason.rb @@ -23,6 +23,9 @@ module Gitlab "Your primary email address is not confirmed. "\ "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}" + when :password_expired + "Your password expired. "\ + "Please access GitLab from a web browser to update your password." else "Your account has been blocked." end @@ -41,6 +44,8 @@ module Gitlab :deactivated elsif !@user.confirmed? :unconfirmed + elsif @user.password_expired? + :password_expired else :blocked end diff --git a/lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group.rb b/lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group.rb new file mode 100644 index 00000000000..f5ba9e63333 --- /dev/null +++ b/lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group.rb @@ -0,0 +1,129 @@ +# 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 diff --git a/lib/gitlab/diff/suggestions_parser.rb b/lib/gitlab/diff/suggestions_parser.rb index f3e6fc455ac..6f126147113 100644 --- a/lib/gitlab/diff/suggestions_parser.rb +++ b/lib/gitlab/diff/suggestions_parser.rb @@ -6,6 +6,9 @@ module Gitlab # Matches for instance "-1", "+1" or "-1+2". SUGGESTION_CONTEXT = /^(\-(?\d+))?(\+(?\d+))?$/.freeze + CSS = 'pre.language-suggestion' + XPATH = Gitlab::Utils::Nokogiri.css_to_xpath(CSS).freeze + class << self # Returns an array of Gitlab::Diff::Suggestion which represents each # suggestion in the given text. @@ -17,7 +20,7 @@ module Gitlab no_original_data: true, suggestions_filter_enabled: supports_suggestion) doc = Nokogiri::HTML(html) - suggestion_nodes = doc.search('pre.language-suggestion') + suggestion_nodes = doc.xpath(XPATH) return [] if suggestion_nodes.empty? diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb index d70e5c3594c..77e0e2ca96c 100644 --- a/lib/gitlab/utils.rb +++ b/lib/gitlab/utils.rb @@ -197,6 +197,24 @@ module Gitlab rescue Addressable::URI::InvalidURIError, TypeError 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. # # Unlike `Hash#invert`, where the last encountered pair wins, and which has the diff --git a/lib/gitlab/utils/nokogiri.rb b/lib/gitlab/utils/nokogiri.rb new file mode 100644 index 00000000000..4b37bb7e5ea --- /dev/null +++ b/lib/gitlab/utils/nokogiri.rb @@ -0,0 +1,24 @@ +# 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 diff --git a/lib/gitlab/x509/signature.rb b/lib/gitlab/x509/signature.rb index c83213e973b..a6761e211fa 100644 --- a/lib/gitlab/x509/signature.rb +++ b/lib/gitlab/x509/signature.rb @@ -23,7 +23,7 @@ module Gitlab end def user - User.find_by_any_email(@email) + strong_memoize(:user) { User.find_by_any_email(@email) } end def verified_signature @@ -31,9 +31,13 @@ module Gitlab end 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 else :unverified diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 0df958aa774..c244c486c5d 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -27077,6 +27077,9 @@ msgstr "" msgid "Redirect to SAML provider to test configuration" msgstr "" +msgid "Redirecting" +msgstr "" + msgid "Redis" msgstr "" diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb index 5fc5cdfc9b9..0e25f6a96d7 100644 --- a/spec/controllers/oauth/authorizations_controller_spec.rb +++ b/spec/controllers/oauth/authorizations_controller_spec.rb @@ -70,77 +70,60 @@ RSpec.describe Oauth::AuthorizationsController do describe 'GET #new' do subject { get :new, params: params } - include_examples 'OAuth Authorizations require confirmed user' include_examples "Implicit grant can't be used in confidential application" - context 'rendering of views based on the ownership of the application' do - shared_examples 'render views' do + context 'when the user is confirmed' do + let(:confirmed_at) { 1.hour.ago } + + context 'when there is already an access token for the application with a matching scope' do + before do + scopes = Doorkeeper::OAuth::Scopes.from_string('api') + + allow(Doorkeeper.configuration).to receive(:scopes).and_return(scopes) + + create(:oauth_access_token, application: application, resource_owner_id: user.id, scopes: scopes) + end + + it 'authorizes the request and shows the user a page that redirects' do + subject + + expect(request.session['user_return_to']).to be_nil + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('doorkeeper/authorizations/redirect') + end + end + + context 'without valid params' do + it 'returns 200 code and renders error view' do + get :new + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('doorkeeper/authorizations/error') + end + end + + context 'with valid params' do 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' - end - - context 'when auth app owner is a subgroup' do - let(:subgroup) { create(:group, parent: group) } - let(:application) { create(:oauth_application, owner_id: subgroup.id, owner_type: 'Namespace') } - - it_behaves_like 'render views' - end - end - - context 'when there is no owner associated' do - let(:application) { create(:oauth_application, owner_id: nil, owner_type: nil) } - - it 'renders view' do + 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 + application.update!(trusted: true) + request.session['user_return_to'] = 'http://example.com' + + subject + + expect(request.session['user_return_to']).to be_nil + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template('doorkeeper/authorizations/redirect') + end end end - - context 'without valid params' do - it 'returns 200 code and renders error view' do - get :new - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to render_template('doorkeeper/authorizations/error') - end - end - - it 'deletes session.user_return_to and redirects when skip authorization' do - application.update!(trusted: true) - request.session['user_return_to'] = 'http://example.com' - - subject - - expect(request.session['user_return_to']).to be_nil - expect(response).to have_gitlab_http_status(:found) - end end describe 'POST #create' do diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb index 072a5f1f402..148ee64fb08 100644 --- a/spec/factories/environments.rb +++ b/spec/factories/environments.rb @@ -16,19 +16,19 @@ FactoryBot.define do end trait :production do - tier { :production } + name { 'production' } end trait :staging do - tier { :staging } + name { 'staging' } end trait :testing do - tier { :testing } + name { 'testing' } end trait :development do - tier { :development } + name { 'development' } end trait :with_review_app do |environment| diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index e60d9d6ab69..1d7099ba443 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -394,6 +394,25 @@ RSpec.describe 'Login' do gitlab_sign_in(user) 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 context 'with invalid username and password' do diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js index d250ffed1a9..deeee5d6589 100644 --- a/spec/frontend/notebook/cells/markdown_spec.js +++ b/spec/frontend/notebook/cells/markdown_spec.js @@ -39,7 +39,7 @@ describe('Markdown component', () => { expect(vm.$el.querySelector('.markdown h1')).not.toBeNull(); }); - it('sanitizes output', async () => { + it('sanitizes Markdown output', async () => { Object.assign(cell, { source: [ '[XSS](data:text/html;base64,PHNjcmlwdD5hbGVydChkb2N1bWVudC5kb21haW4pPC9zY3JpcHQ+Cg==)\n', @@ -50,6 +50,17 @@ describe('Markdown component', () => { expect(vm.$el.querySelector('a').getAttribute('href')).toBeNull(); }); + it('sanitizes HTML', async () => { + const findLink = () => vm.$el.querySelector('.xss-link'); + Object.assign(cell, { + source: ['XSS\n'], + }); + + await vm.$nextTick(); + expect(findLink().getAttribute('data-remote')).toBe(null); + expect(findLink().getAttribute('data-type')).toBe(null); + }); + describe('tables', () => { beforeEach(() => { json = getJSONFixture('blob/notebook/markdown-table.json'); diff --git a/spec/graphql/resolvers/timelog_resolver_spec.rb b/spec/graphql/resolvers/timelog_resolver_spec.rb index 585cd657e35..bb4938c751f 100644 --- a/spec/graphql/resolvers/timelog_resolver_spec.rb +++ b/spec/graphql/resolvers/timelog_resolver_spec.rb @@ -11,26 +11,27 @@ RSpec.describe Resolvers::TimelogResolver do context "with a group" do let_it_be(:current_user) { create(:user) } - let_it_be(:group) { create(:group) } - let_it_be(:project) { create(:project, :public, group: group) } - - before_all do - group.add_developer(current_user) - project.add_developer(current_user) - end - - before do - group.clear_memoization(:timelogs) - end + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :empty_repo, :public, group: group) } describe '#resolve' do - let_it_be(:issue) { create(:issue, project: project) } - let_it_be(:issue2) { create(:issue, project: project) } - let_it_be(:timelog1) { create(:issue_timelog, issue: issue, spent_at: 2.days.ago.beginning_of_day) } - let_it_be(:timelog2) { create(:issue_timelog, issue: issue2, spent_at: 2.days.ago.end_of_day) } - let_it_be(:timelog3) { create(:issue_timelog, issue: issue2, spent_at: 10.days.ago) } + let_it_be(:short_time_ago) { 5.days.ago.beginning_of_day } + let_it_be(:medium_time_ago) { 15.days.ago.beginning_of_day } - let(:args) { { start_time: 6.days.ago, end_time: 2.days.ago.noon } } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:merge_request) { create(:merge_request, source_project: project) } + + let_it_be(:timelog1) { create(:issue_timelog, issue: issue, spent_at: short_time_ago.beginning_of_day) } + let_it_be(:timelog2) { create(:issue_timelog, issue: issue, spent_at: short_time_ago.end_of_day) } + let_it_be(:timelog3) { create(:merge_request_timelog, merge_request: merge_request, spent_at: medium_time_ago) } + + let(:args) { { start_time: short_time_ago, end_time: short_time_ago.noon } } + + it 'finds all timelogs' do + timelogs = resolve_timelogs + + expect(timelogs).to contain_exactly(timelog1, timelog2, timelog3) + end it 'finds all timelogs within given dates' do timelogs = resolve_timelogs(**args) @@ -38,15 +39,28 @@ RSpec.describe Resolvers::TimelogResolver do expect(timelogs).to contain_exactly(timelog1) end - it 'return nothing when user has insufficient permissions' do - user = create(:user) - group.add_guest(current_user) + context 'when only start_date is present' do + let(:args) { { start_date: short_time_ago } } - expect(resolve_timelogs(user: user, **args)).to be_empty + it 'finds timelogs until the end of day of end_date' do + timelogs = resolve_timelogs(**args) + + expect(timelogs).to contain_exactly(timelog1, timelog2) + end + end + + context 'when only end_date is present' do + let(:args) { { end_date: medium_time_ago } } + + it 'finds timelogs until the end of day of end_date' do + timelogs = resolve_timelogs(**args) + + expect(timelogs).to contain_exactly(timelog3) + end end context 'when start_time and end_date are present' do - let(:args) { { start_time: 6.days.ago, end_date: 2.days.ago } } + let(:args) { { start_time: short_time_ago, end_date: short_time_ago } } it 'finds timelogs until the end of day of end_date' do timelogs = resolve_timelogs(**args) @@ -56,7 +70,7 @@ RSpec.describe Resolvers::TimelogResolver do end context 'when start_date and end_time are present' do - let(:args) { { start_date: 6.days.ago, end_time: 2.days.ago.noon } } + let(:args) { { start_date: short_time_ago, end_time: short_time_ago.noon } } it 'finds all timelogs within start_date and end_time' do timelogs = resolve_timelogs(**args) @@ -68,95 +82,32 @@ RSpec.describe Resolvers::TimelogResolver do context 'when arguments are invalid' do let_it_be(:error_class) { Gitlab::Graphql::Errors::ArgumentError } - context 'when no time or date arguments are present' do - let(:args) { {} } - - it 'returns correct error' do - expect { resolve_timelogs(**args) } - .to raise_error(error_class, /Start and End arguments must be present/) - end - end - - context 'when only start_time is present' do - let(:args) { { start_time: 6.days.ago } } - - it 'returns correct error' do - expect { resolve_timelogs(**args) } - .to raise_error(error_class, /Both Start and End arguments must be present/) - end - end - - context 'when only end_time is present' do - let(:args) { { end_time: 2.days.ago } } - - it 'returns correct error' do - expect { resolve_timelogs(**args) } - .to raise_error(error_class, /Both Start and End arguments must be present/) - end - end - - context 'when only start_date is present' do - let(:args) { { start_date: 6.days.ago } } - - it 'returns correct error' do - expect { resolve_timelogs(**args) } - .to raise_error(error_class, /Both Start and End arguments must be present/) - end - end - - context 'when only end_date is present' do - let(:args) { { end_date: 2.days.ago } } - - it 'returns correct error' do - expect { resolve_timelogs(**args) } - .to raise_error(error_class, /Both Start and End arguments must be present/) - end - end - context 'when start_time and start_date are present' do - let(:args) { { start_time: 6.days.ago, start_date: 6.days.ago } } + let(:args) { { start_time: short_time_ago, start_date: short_time_ago } } it 'returns correct error' do expect { resolve_timelogs(**args) } - .to raise_error(error_class, /Both Start and End arguments must be present/) + .to raise_error(error_class, /Provide either a start date or time, but not both/) end end context 'when end_time and end_date are present' do - let(:args) { { end_time: 2.days.ago, end_date: 2.days.ago } } + let(:args) { { end_time: short_time_ago, end_date: short_time_ago } } it 'returns correct error' do expect { resolve_timelogs(**args) } - .to raise_error(error_class, /Both Start and End arguments must be present/) - end - end - - context 'when three arguments are present' do - let(:args) { { start_date: 6.days.ago, end_date: 2.days.ago, end_time: 2.days.ago } } - - it 'returns correct error' do - expect { resolve_timelogs(**args) } - .to raise_error(error_class, /Only Time or Date arguments must be present/) + .to raise_error(error_class, /Provide either an end date or time, but not both/) end end context 'when start argument is after end argument' do - let(:args) { { start_time: 2.days.ago, end_time: 6.days.ago } } + let(:args) { { start_time: short_time_ago, end_time: medium_time_ago } } it 'returns correct error' do expect { resolve_timelogs(**args) } .to raise_error(error_class, /Start argument must be before End argument/) end end - - context 'when time range is more than 60 days' do - let(:args) { { start_time: 3.months.ago, end_time: 2.days.ago } } - - it 'returns correct error' do - expect { resolve_timelogs(**args) } - .to raise_error(error_class, /The time range period cannot contain more than 60 days/) - end - end end end end diff --git a/spec/graphql/types/timelog_type_spec.rb b/spec/graphql/types/timelog_type_spec.rb index 791c2fdb046..1344af89fb6 100644 --- a/spec/graphql/types/timelog_type_spec.rb +++ b/spec/graphql/types/timelog_type_spec.rb @@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['Timelog'] do it { expect(described_class.graphql_name).to eq('Timelog') } it { expect(described_class).to have_graphql_fields(fields) } - it { expect(described_class).to require_graphql_authorizations(:read_group_timelogs) } + it { expect(described_class).to require_graphql_authorizations(:read_issue) } describe 'user field' do subject { described_class.fields['user'] } diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb index 00a59f037e0..e946857ac77 100644 --- a/spec/helpers/markup_helper_spec.rb +++ b/spec/helpers/markup_helper_spec.rb @@ -418,6 +418,13 @@ FooBar describe '#markup' do 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 expect(content.encoding.name).to eq('UTF-8') expect(helper.markup('foo.rst', content).encoding.name).to eq('UTF-8') diff --git a/spec/initializers/lograge_spec.rb b/spec/initializers/lograge_spec.rb index abb1673bb88..421f6373eff 100644 --- a/spec/initializers/lograge_spec.rb +++ b/spec/initializers/lograge_spec.rb @@ -173,6 +173,27 @@ RSpec.describe 'lograge', type: :request do 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 'when RequestStore is enabled', :request_store do it 'includes db counters' do diff --git a/spec/lib/banzai/filter/truncate_source_filter_spec.rb b/spec/lib/banzai/filter/truncate_source_filter_spec.rb index d5eb8b738b1..8970aa1d382 100644 --- a/spec/lib/banzai/filter/truncate_source_filter_spec.rb +++ b/spec/lib/banzai/filter/truncate_source_filter_spec.rb @@ -8,24 +8,68 @@ RSpec.describe Banzai::Filter::TruncateSourceFilter do let(:short_text) { 'foo' * 10 } let(:long_text) { ([short_text] * 10).join(' ') } - it 'does nothing when limit is unspecified' do - output = filter(long_text) - - expect(output).to eq(long_text) + before do + stub_const("#{described_class}::CHARACTER_COUNT_LIMIT", 50) + stub_const("#{described_class}::USER_MSG_LIMIT", 20) end - it 'does nothing to a short-enough text' do - output = filter(short_text, limit: short_text.bytesize) + context 'when markdown belongs to a blob' do + it 'does nothing when limit is unspecified' do + output = filter(long_text, text_source: :blob) - expect(output).to eq(short_text) + 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 - it 'truncates UTF-8 text by bytes, on a character boundary' do - utf8_text = '日本語の文字が大きい' - truncated = '日...' + 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(filter(utf8_text, limit: truncated.bytesize)).to eq(truncated) - 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(output).to eq(long_text) + 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 + output = filter(short_text, limit: short_text.bytesize) + + expect(output).to eq(short_text) + end + + it 'truncates UTF-8 text by bytes, on a character boundary' do + utf8_text = '日本語の文字が大きい' + truncated = '日...' + + expect(filter(utf8_text, limit: truncated.bytesize)).to eq(truncated) + 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) + end end end diff --git a/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb b/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb index 5a3ceccfae1..e8df395564a 100644 --- a/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb @@ -36,9 +36,11 @@ RSpec.describe Banzai::Pipeline::PostProcessPipeline do end let(:doc) { HTML::Pipeline.parse(html) } + let(:non_related_xpath_calls) { 2 } 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 end diff --git a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb index d3c6cde5590..102d6fba97f 100644 --- a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb +++ b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb @@ -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.') } 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 diff --git a/spec/lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group_spec.rb b/spec/lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group_spec.rb new file mode 100644 index 00000000000..e14328b6150 --- /dev/null +++ b/spec/lib/gitlab/background_migration/update_users_where_two_factor_auth_required_from_group_spec.rb @@ -0,0 +1,84 @@ +# 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 diff --git a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb index 175b12637e6..ad89f1f5cda 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/environment_spec.rb @@ -128,7 +128,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Environment do context 'when environment has already been created' do before do - create(:environment, :staging, project: project, name: 'customer-portal') + create(:environment, project: project, name: 'customer-portal', tier: :staging) end it 'does not overwrite the specified deployment tier' do diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index ae9c697e0b9..3d6c04fd484 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -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}") 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 before do project.add_guest(user) @@ -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}") 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 expect(project.repository).to receive(:clean_stale_repository_files).and_call_original expect { push_access_check }.not_to raise_error diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index c7e77a34582..dc7fedd6b4f 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -624,6 +624,7 @@ metrics_setting: - project protected_environments: - project +- group - deploy_access_levels deploy_access_levels: - protected_environment diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 0be1b09851f..2173bee6b4b 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -701,6 +701,7 @@ ProjectSetting: ProtectedEnvironment: - id - project_id +- group_id - name - created_at - updated_at diff --git a/spec/lib/gitlab/utils/nokogiri_spec.rb b/spec/lib/gitlab/utils/nokogiri_spec.rb new file mode 100644 index 00000000000..90f137f53c8 --- /dev/null +++ b/spec/lib/gitlab/utils/nokogiri_spec.rb @@ -0,0 +1,34 @@ +# 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 diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index 11dba610faf..a7ccce0aaab 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -417,6 +417,29 @@ RSpec.describe Gitlab::Utils do 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 it 'invert keys with array values' do hash = { diff --git a/spec/lib/gitlab/x509/signature_spec.rb b/spec/lib/gitlab/x509/signature_spec.rb index 2ac9c1f3a3b..7ba15faf910 100644 --- a/spec/lib/gitlab/x509/signature_spec.rb +++ b/spec/lib/gitlab/x509/signature_spec.rb @@ -12,20 +12,30 @@ RSpec.describe Gitlab::X509::Signature do end shared_examples "a verified signature" do - it 'returns a verified signature if email does match' do - signature = described_class.new( + let_it_be(:user) { create(:user, email: X509Helpers::User1.certificate_email) } + + subject(:signature) do + described_class.new( X509Helpers::User1.signed_commit_signature, X509Helpers::User1.signed_commit_base_data, X509Helpers::User1.certificate_email, 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.x509_issuer).to have_attributes(issuer_attributes) expect(signature.verified_signature).to be_truthy expect(signature.verification_status).to eq(:verified) 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 signature = described_class.new( X509Helpers::User1.signed_commit_signature, @@ -55,13 +65,6 @@ RSpec.describe Gitlab::X509::Signature do end 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) signature.x509_certificate.revoked! @@ -253,23 +256,25 @@ RSpec.describe Gitlab::X509::Signature do end describe '#user' 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 - ) + subject do + described_class.new( + X509Helpers::User1.signed_tag_signature, + X509Helpers::User1.signed_tag_base_data, + X509Helpers::User1.certificate_email, + X509Helpers::User1.signed_commit_time + ).user + end context 'if email is assigned to a user' do let!(:user) { create(:user, email: X509Helpers::User1.certificate_email) } it 'returns user' do - expect(signature.user).to eq(user) + is_expected.to eq(user) end end 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 @@ -292,6 +297,17 @@ RSpec.describe Gitlab::X509::Signature do end 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 before do store = OpenSSL::X509::Store.new @@ -301,19 +317,18 @@ RSpec.describe Gitlab::X509::Signature do end 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.x509_issuer).to have_attributes(issuer_attributes) expect(signature.verified_signature).to be_truthy expect(signature.verification_status).to eq(:verified) 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 signature = described_class.new( X509Helpers::User1.signed_tag_signature, @@ -343,13 +358,6 @@ RSpec.describe Gitlab::X509::Signature do end 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) signature.x509_certificate.revoked! @@ -368,13 +376,6 @@ RSpec.describe Gitlab::X509::Signature do end 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.x509_issuer).to have_attributes(issuer_attributes) expect(signature.verified_signature).to be_falsey diff --git a/spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb b/spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb new file mode 100644 index 00000000000..d3154596b26 --- /dev/null +++ b/spec/migrations/20210601080039_group_protected_environments_add_index_and_constraint_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration!('group_protected_environments_add_index_and_constraint') + +RSpec.describe GroupProtectedEnvironmentsAddIndexAndConstraint do + let(:migration) { described_class.new } + let(:protected_environments) { table(:protected_environments) } + let(:group) { table(:namespaces).create!(name: 'group', path: 'group') } + let(:project) { table(:projects).create!(name: 'project', path: 'project', namespace_id: group.id) } + + describe '#down' do + it 'deletes only group-level configurations' do + migration.up + + project_protections = [ + protected_environments.create!(project_id: project.id, name: 'production'), + protected_environments.create!(project_id: project.id, name: 'staging') + ] + protected_environments.create!(group_id: group.id, name: 'production') + protected_environments.create!(group_id: group.id, name: 'staging') + + migration.down + + expect(protected_environments.pluck(:id)) + .to match_array project_protections.map(&:id) + end + end +end diff --git a/spec/migrations/backfill_escalation_policies_for_oncall_schedules_spec.rb b/spec/migrations/backfill_escalation_policies_for_oncall_schedules_spec.rb new file mode 100644 index 00000000000..86415b1520e --- /dev/null +++ b/spec/migrations/backfill_escalation_policies_for_oncall_schedules_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20210519220019_backfill_escalation_policies_for_oncall_schedules.rb') + +RSpec.describe BackfillEscalationPoliciesForOncallSchedules do + let_it_be(:projects) { table(:projects) } + let_it_be(:schedules) { table(:incident_management_oncall_schedules) } + let_it_be(:policies) { table(:incident_management_escalation_policies) } + let_it_be(:rules) { table(:incident_management_escalation_rules) } + + # Project with no schedules + let_it_be(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab') } + let_it_be(:project_a) { projects.create!(namespace_id: namespace.id) } + + context 'with backfill-able schedules' do + # Project with one schedule + let_it_be(:project_b) { projects.create!(namespace_id: namespace.id) } + let_it_be(:schedule_b1) { schedules.create!(project_id: project_b.id, iid: 1, name: 'Schedule B1') } + + # Project with multiple schedules + let_it_be(:project_c) { projects.create!(namespace_id: namespace.id) } + let_it_be(:schedule_c1) { schedules.create!(project_id: project_c.id, iid: 1, name: 'Schedule C1') } + let_it_be(:schedule_c2) { schedules.create!(project_id: project_c.id, iid: 2, name: 'Schedule C2') } + + # Project with a single schedule which already has a policy + let_it_be(:project_d) { projects.create!(namespace_id: namespace.id) } + let_it_be(:schedule_d1) { schedules.create!(project_id: project_d.id, iid: 1, name: 'Schedule D1') } + let_it_be(:policy_d1) { policies.create!(project_id: project_d.id, name: 'Policy D1') } + let_it_be(:rule_d1) { rules.create!(policy_id: policy_d1.id, oncall_schedule_id: schedule_d1.id, status: 2, elapsed_time_seconds: 60) } + + # Project with a multiple schedule, one of which already has a policy + let_it_be(:project_e) { projects.create!(namespace_id: namespace.id) } + let_it_be(:schedule_e1) { schedules.create!(project_id: project_e.id, iid: 1, name: 'Schedule E1') } + let_it_be(:schedule_e2) { schedules.create!(project_id: project_e.id, iid: 2, name: 'Schedule E2') } + let_it_be(:policy_e1) { policies.create!(project_id: project_e.id, name: 'Policy E1') } + let_it_be(:rule_e1) { rules.create!(policy_id: policy_e1.id, oncall_schedule_id: schedule_e2.id, status: 2, elapsed_time_seconds: 60) } + + # Project with a multiple schedule, with multiple policies + let_it_be(:project_f) { projects.create!(namespace_id: namespace.id) } + let_it_be(:schedule_f1) { schedules.create!(project_id: project_f.id, iid: 1, name: 'Schedule F1') } + let_it_be(:schedule_f2) { schedules.create!(project_id: project_f.id, iid: 2, name: 'Schedule F2') } + let_it_be(:policy_f1) { policies.create!(project_id: project_f.id, name: 'Policy F1') } + let_it_be(:rule_f1) { rules.create!(policy_id: policy_f1.id, oncall_schedule_id: schedule_f1.id, status: 2, elapsed_time_seconds: 60) } + let_it_be(:rule_f2) { rules.create!(policy_id: policy_f1.id, oncall_schedule_id: schedule_f2.id, status: 2, elapsed_time_seconds: 60) } + let_it_be(:policy_f2) { policies.create!(project_id: project_f.id, name: 'Policy F2') } + let_it_be(:rule_f3) { rules.create!(policy_id: policy_f2.id, oncall_schedule_id: schedule_f2.id, status: 1, elapsed_time_seconds: 10) } + + it 'backfills escalation policies correctly' do + expect { migrate! } + .to change(policies, :count).by(2) + .and change(rules, :count).by(3) + + new_policy_b1, new_policy_c1 = new_polices = policies.last(2) + new_rules = rules.last(3) + + expect(new_polices).to all have_attributes(name: 'On-call Escalation Policy') + expect(new_policy_b1.description).to eq('Immediately notify Schedule B1') + expect(new_policy_c1.description).to eq('Immediately notify Schedule C1') + expect(policies.pluck(:project_id)).to eq([ + project_d.id, + project_e.id, + project_f.id, + project_f.id, + project_b.id, + project_c.id + ]) + + expect(new_rules).to all have_attributes(status: 1, elapsed_time_seconds: 0) + expect(rules.pluck(:policy_id)).to eq([ + rule_d1.policy_id, + rule_e1.policy_id, + rule_f1.policy_id, + rule_f2.policy_id, + rule_f3.policy_id, + new_policy_b1.id, + new_policy_c1.id, + new_policy_c1.id + ]) + expect(rules.pluck(:oncall_schedule_id)).to eq([ + rule_d1.oncall_schedule_id, + rule_e1.oncall_schedule_id, + rule_f1.oncall_schedule_id, + rule_f2.oncall_schedule_id, + rule_f3.oncall_schedule_id, + schedule_b1.id, + schedule_c1.id, + schedule_c2.id + ]) + end + end + + context 'with no schedules' do + it 'does nothing' do + expect { migrate! } + .to not_change(policies, :count) + .and not_change(rules, :count) + end + end +end diff --git a/spec/migrations/schedule_update_users_where_two_factor_auth_required_from_group_spec.rb b/spec/migrations/schedule_update_users_where_two_factor_auth_required_from_group_spec.rb new file mode 100644 index 00000000000..cec141cacc9 --- /dev/null +++ b/spec/migrations/schedule_update_users_where_two_factor_auth_required_from_group_spec.rb @@ -0,0 +1,29 @@ +# 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 diff --git a/spec/models/concerns/has_timelogs_report_spec.rb b/spec/models/concerns/has_timelogs_report_spec.rb deleted file mode 100644 index f0dca47fae1..00000000000 --- a/spec/models/concerns/has_timelogs_report_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe HasTimelogsReport do - let_it_be(:user) { create(:user) } - - let(:group) { create(:group) } - let(:project) { create(:project, :public, group: group) } - let(:issue1) { create(:issue, project: project) } - let(:merge_request1) { create(:merge_request, source_project: project) } - - describe '#timelogs' do - let_it_be(:start_time) { 20.days.ago } - let_it_be(:end_time) { 8.days.ago } - - let!(:timelog1) { create_timelog(15.days.ago, issue: issue1) } - let!(:timelog2) { create_timelog(10.days.ago, merge_request: merge_request1) } - let!(:timelog3) { create_timelog(5.days.ago, issue: issue1) } - - before do - group.add_developer(user) - end - - it 'returns collection of timelogs between given times' do - expect(group.timelogs(start_time, end_time).to_a).to match_array([timelog1, timelog2]) - end - - it 'returns empty collection if times are not present' do - expect(group.timelogs(nil, nil)).to be_empty - end - - it 'returns empty collection if time range is invalid' do - expect(group.timelogs(end_time, start_time)).to be_empty - end - end - - describe '#user_can_access_group_timelogs?' do - it 'returns true if user can access group timelogs' do - group.add_developer(user) - - expect(group).to be_user_can_access_group_timelogs(user) - end - - it 'returns false if user has insufficient permissions' do - group.add_guest(user) - - expect(group).not_to be_user_can_access_group_timelogs(user) - end - end - - def create_timelog(time, issue: nil, merge_request: nil) - create(:timelog, issue: issue, merge_request: merge_request, user: user, spent_at: time) - end -end diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb index c3432907112..bc042f7a639 100644 --- a/spec/models/timelog_spec.rb +++ b/spec/models/timelog_spec.rb @@ -64,25 +64,57 @@ RSpec.describe Timelog do let_it_be(:subgroup_issue) { create(:issue, project: subgroup_project) } let_it_be(:subgroup_merge_request) { create(:merge_request, source_project: subgroup_project) } - let_it_be(:timelog) { create(:issue_timelog, spent_at: 65.days.ago) } - let_it_be(:timelog1) { create(:issue_timelog, spent_at: 15.days.ago, issue: group_issue) } - let_it_be(:timelog2) { create(:issue_timelog, spent_at: 5.days.ago, issue: subgroup_issue) } - let_it_be(:timelog3) { create(:merge_request_timelog, spent_at: 65.days.ago) } - let_it_be(:timelog4) { create(:merge_request_timelog, spent_at: 15.days.ago, merge_request: group_merge_request) } - let_it_be(:timelog5) { create(:merge_request_timelog, spent_at: 5.days.ago, merge_request: subgroup_merge_request) } + let_it_be(:short_time_ago) { 5.days.ago } + let_it_be(:medium_time_ago) { 15.days.ago } + let_it_be(:long_time_ago) { 65.days.ago } - describe 'in_group' do + let_it_be(:timelog) { create(:issue_timelog, spent_at: long_time_ago) } + let_it_be(:timelog1) { create(:issue_timelog, spent_at: medium_time_ago, issue: group_issue) } + let_it_be(:timelog2) { create(:issue_timelog, spent_at: short_time_ago, issue: subgroup_issue) } + let_it_be(:timelog3) { create(:merge_request_timelog, spent_at: long_time_ago) } + let_it_be(:timelog4) { create(:merge_request_timelog, spent_at: medium_time_ago, merge_request: group_merge_request) } + let_it_be(:timelog5) { create(:merge_request_timelog, spent_at: short_time_ago, merge_request: subgroup_merge_request) } + + describe '.in_group' do it 'return timelogs created for group issues and merge requests' do expect(described_class.in_group(group)).to contain_exactly(timelog1, timelog2, timelog4, timelog5) end end - describe 'between_times' do - it 'returns collection of timelogs within given times' do - timelogs = described_class.between_times(20.days.ago, 10.days.ago) + describe '.at_or_after' do + it 'returns timelogs at the time limit' do + timelogs = described_class.at_or_after(short_time_ago) - expect(timelogs).to contain_exactly(timelog1, timelog4) + expect(timelogs).to contain_exactly(timelog2, timelog5) end + + it 'returns timelogs after given time' do + timelogs = described_class.at_or_after(just_before(short_time_ago)) + + expect(timelogs).to contain_exactly(timelog2, timelog5) + end + end + + describe '.at_or_before' do + it 'returns timelogs at the time limit' do + timelogs = described_class.at_or_before(long_time_ago) + + expect(timelogs).to contain_exactly(timelog, timelog3) + end + + it 'returns timelogs before given time' do + timelogs = described_class.at_or_before(just_after(long_time_ago)) + + expect(timelogs).to contain_exactly(timelog, timelog3) + end + end + + def just_before(time) + time - 1.day + end + + def just_after(time) + time + 1.day end end end diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb index e677f5558fd..bbbc5d08c07 100644 --- a/spec/policies/global_policy_spec.rb +++ b/spec/policies/global_policy_spec.rb @@ -239,6 +239,14 @@ RSpec.describe GlobalPolicy do it { is_expected.not_to be_allowed(:access_api) } 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 before do enforce_terms @@ -418,6 +426,14 @@ RSpec.describe GlobalPolicy do it { is_expected.not_to be_allowed(:access_git) } 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 describe 'read instance metadata' do @@ -494,6 +510,14 @@ RSpec.describe GlobalPolicy do it { is_expected.not_to be_allowed(:use_slash_commands) } 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 describe 'create_snippet' do diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index f5e389ff338..ee87a2da189 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -923,54 +923,4 @@ RSpec.describe GroupPolicy do it { expect(described_class.new(current_user, subgroup)).to be_allowed(:read_label) } end end - - context 'timelogs' do - context 'with admin' do - let(:current_user) { admin } - - context 'when admin mode is enabled', :enable_admin_mode do - it { is_expected.to be_allowed(:read_group_timelogs) } - end - - context 'when admin mode is disabled' do - it { is_expected.to be_disallowed(:read_group_timelogs) } - end - end - - context 'with owner' do - let(:current_user) { owner } - - it { is_expected.to be_allowed(:read_group_timelogs) } - end - - context 'with maintainer' do - let(:current_user) { maintainer } - - it { is_expected.to be_allowed(:read_group_timelogs) } - end - - context 'with reporter' do - let(:current_user) { reporter } - - it { is_expected.to be_allowed(:read_group_timelogs) } - end - - context 'with guest' do - let(:current_user) { guest } - - it { is_expected.to be_disallowed(:read_group_timelogs) } - end - - context 'with non member' do - let(:current_user) { create(:user) } - - it { is_expected.to be_disallowed(:read_group_timelogs) } - end - - context 'with anonymous' do - let(:current_user) { nil } - - it { is_expected.to be_disallowed(:read_group_timelogs) } - end - end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 46da42a4787..d0abcfbd091 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -1385,54 +1385,4 @@ RSpec.describe ProjectPolicy do end end end - - context 'timelogs' do - context 'with admin' do - let(:current_user) { admin } - - context 'when admin mode enabled', :enable_admin_mode do - it { is_expected.to be_allowed(:read_group_timelogs) } - end - - context 'when admin mode disabled' do - it { is_expected.to be_disallowed(:read_group_timelogs) } - end - end - - context 'with owner' do - let(:current_user) { owner } - - it { is_expected.to be_allowed(:read_group_timelogs) } - end - - context 'with maintainer' do - let(:current_user) { maintainer } - - it { is_expected.to be_allowed(:read_group_timelogs) } - end - - context 'with reporter' do - let(:current_user) { reporter } - - it { is_expected.to be_allowed(:read_group_timelogs) } - end - - context 'with guest' do - let(:current_user) { guest } - - it { is_expected.to be_disallowed(:read_group_timelogs) } - end - - context 'with non member' do - let(:current_user) { non_member } - - it { is_expected.to be_disallowed(:read_group_timelogs) } - end - - context 'with anonymous' do - let(:current_user) { anonymous } - - it { is_expected.to be_disallowed(:read_group_timelogs) } - end - end end diff --git a/spec/requests/api/graphql/group/timelogs_spec.rb b/spec/requests/api/graphql/group/timelogs_spec.rb index 6e21a73afa9..05b6ee3ff89 100644 --- a/spec/requests/api/graphql/group/timelogs_spec.rb +++ b/spec/requests/api/graphql/group/timelogs_spec.rb @@ -17,8 +17,37 @@ RSpec.describe 'Timelogs through GroupQuery' do let(:timelogs_data) { graphql_data['group']['timelogs']['nodes'] } - before do - group.add_developer(user) + context 'when the project is private' do + let_it_be(:group2) { create(:group) } + let_it_be(:project2) { create(:project, :private, group: group2) } + let_it_be(:issue2) { create(:issue, project: project2) } + let_it_be(:timelog3) { create(:timelog, issue: issue2, spent_at: '2019-08-13 14:00:00') } + + subject { post_graphql(query(full_path: group2.full_path), current_user: user) } + + context 'when the user is not a member of the project' do + it 'returns no timelogs' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(graphql_errors).to be_nil + expect(timelog_array.size).to eq 0 + end + end + + context 'when the user is a member of the project' do + before do + project2.add_developer(user) + end + + it 'returns timelogs' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(graphql_errors).to be_nil + expect(timelog_array.size).to eq 1 + end + end end context 'when the request is correct' do @@ -74,18 +103,6 @@ RSpec.describe 'Timelogs through GroupQuery' do expect(timelogs_data).to be_empty end end - - context 'when user has no permission to read group timelogs' do - it 'returns empty result' do - guest = create(:user) - group.add_guest(guest) - post_graphql(query, current_user: guest) - - expect(response).to have_gitlab_http_status(:success) - expect(graphql_errors).to be_nil - expect(timelogs_data).to be_empty - end - end end end @@ -95,7 +112,7 @@ RSpec.describe 'Timelogs through GroupQuery' do end end - def query(timelog_params = params) + def query(timelog_params: params, full_path: group.full_path) timelog_nodes = <<~NODE nodes { spentAt @@ -114,7 +131,7 @@ RSpec.describe 'Timelogs through GroupQuery' do graphql_query_for( :group, - { full_path: group.full_path }, + { full_path: full_path }, query_graphql_field(:timelogs, timelog_params, timelog_nodes) ) end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index a1e28c18769..279c65fc2f4 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -35,6 +35,26 @@ RSpec.describe 'Git HTTP requests' do expect(response.header['WWW-Authenticate']).to start_with('Basic ') 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 context "when authentication succeeds" do @@ -75,6 +95,15 @@ RSpec.describe 'Git HTTP requests' do expect(response.header['WWW-Authenticate']).to start_with('Basic ') 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 context "when authentication succeeds" do @@ -576,6 +605,16 @@ RSpec.describe 'Git HTTP requests' do it_behaves_like 'pulls 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 context 'when user has 2FA enabled' do @@ -649,6 +688,18 @@ RSpec.describe 'Git HTTP requests' do expect(response).to have_gitlab_http_status(:ok) 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 @@ -860,6 +911,16 @@ RSpec.describe 'Git HTTP requests' do expect(response).to have_gitlab_http_status(:not_found) 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 diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 4e18c9cb4ca..0e3a0252638 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -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(:role) { :reporter} - # TODO: This should return a 404 response - # https://gitlab.com/gitlab-org/gitlab/-/issues/292006 - it_behaves_like 'LFS http 200 response' + it_behaves_like 'LFS http 401 response' end context 'when user is blocked' do diff --git a/spec/tasks/gitlab/x509/update_rake_spec.rb b/spec/tasks/gitlab/x509/update_rake_spec.rb index 93e97ab38ad..b166e73935a 100644 --- a/spec/tasks/gitlab/x509/update_rake_spec.rb +++ b/spec/tasks/gitlab/x509/update_rake_spec.rb @@ -8,12 +8,13 @@ RSpec.describe 'gitlab:x509 namespace rake task' do end describe 'update_signatures' do - subject { run_rake_task('gitlab:x509:update_signatures') } - - let(:project) { create :project, :repository, path: X509Helpers::User1.path } + let(:user) { create(:user, email: X509Helpers::User1.certificate_email) } + let(:project) { create(:project, :repository, path: X509Helpers::User1.path, creator: user) } let(:x509_signed_commit) { project.commit_by(oid: '189a6c924013fc3fe40d6f1ec1dc20214183bc97') } 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 x509_commit @@ -22,21 +23,14 @@ RSpec.describe 'gitlab:x509 namespace rake task' do store.add_cert(certificate) 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 - - subject - - x509_commit.reload - expect(x509_commit.verification_status).to eq('verified') + expect { subject }.to change { x509_commit.reload.verification_status }.from('unverified').to('verified') end it 'returns if no signature is available' do - expect_any_instance_of(Gitlab::X509::Commit) do |x509_commit| - expect(x509_commit).not_to receive(:update_signature!) + expect_any_instance_of(Gitlab::X509::Commit).not_to receive(:update_signature!) - subject - end + subject end end end