From 7d4b2ed7bf75d316577b718c71a9fdef19184539 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 14 May 2021 18:10:34 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .gitlab-ci.yml | 2 + .gitlab/ci/qa.gitlab-ci.yml | 8 +- .gitlab/ci/rails.gitlab-ci.yml | 3 +- .gitlab/ci/review.gitlab-ci.yml | 4 +- .../feature_flags/components/feature_flags.vue | 5 +- app/assets/javascripts/feature_flags/index.js | 4 +- app/controllers/admin/runners_controller.rb | 4 +- app/controllers/admin/users_controller.rb | 23 +++ .../analytics/cycle_analytics/stages_controller.rb | 44 ++++++ .../cycle_analytics/value_streams_controller.rb | 16 +++ .../analytics/cycle_analytics/stage_finder.rb | 37 +++++ app/finders/ci/runners_finder.rb | 7 - app/helpers/users_helper.rb | 46 ++++++ .../analytics/cycle_analytics/project_stage.rb | 3 + .../cycle_analytics/project_value_stream.rb | 22 +++ .../concerns/analytics/cycle_analytics/stage.rb | 1 + app/models/project.rb | 3 +- app/models/user.rb | 11 +- .../cycle_analytics/configuration_entity.rb | 22 +++ .../analytics/cycle_analytics/event_entity.rb | 37 +++++ .../analytics/cycle_analytics/stage_entity.rb | 38 +++++ .../cycle_analytics/value_stream_entity.rb | 24 ++++ .../cycle_analytics/value_stream_serializer.rb | 9 ++ .../cycle_analytics/stages/base_service.rb | 47 ++++++ .../cycle_analytics/stages/list_service.rb | 27 ++++ app/services/users/ban_service.rb | 25 ++++ app/views/admin/users/_ban_user.html.haml | 9 ++ app/views/admin/users/_head.html.haml | 3 + app/views/admin/users/_users.html.haml | 5 + app/views/admin/users/show.html.haml | 15 ++ app/workers/packages/nuget/extraction_worker.rb | 3 +- app/workers/packages/rubygems/extraction_worker.rb | 2 +- .../unreleased/324206-extraction-error-rescue.yml | 5 + .../329208-project-level-value-stream.yml | 5 + .../afontaine-inject-feature-flag-limit.yml | 5 + changelogs/unreleased/ban-user-state-ui.yml | 5 + .../development/ban_user_feature_flag.yml | 8 ++ config/routes/admin.rb | 2 + config/routes/project.rb | 9 ++ .../20210503105022_create_project_value_streams.rb | 27 ++++ ...dd_project_value_stream_id_to_project_stages.rb | 30 ++++ db/schema_migrations/20210503105022 | 1 + db/schema_migrations/20210503105845 | 1 + db/structure.sql | 36 ++++- doc/api/project_import_export.md | 59 ++++++++ doc/development/i18n/externalization.md | 4 + doc/subscriptions/self_managed/index.md | 6 +- .../admin_area/activating_deactivating_users.md | 69 +-------- doc/user/admin_area/approving_users.md | 2 +- doc/user/admin_area/blocking_unblocking_users.md | 51 +------ doc/user/admin_area/index.md | 4 +- doc/user/admin_area/moderate_users.md | 157 +++++++++++++++++++++ .../container_scanning/index.md | 39 +++-- .../application_security/vulnerabilities/index.md | 2 +- doc/user/packages/container_registry/index.md | 7 + doc/user/profile/account/delete_account.md | 2 +- doc/user/upgrade_email_bypass.md | 2 +- lib/sidebars/projects/menus/deployments_menu.rb | 87 ++++++++++++ lib/sidebars/projects/menus/operations_menu.rb | 6 +- .../projects/menus/project_information_menu.rb | 10 +- lib/sidebars/projects/panel.rb | 18 ++- locale/gitlab.pot | 63 +++++++++ scripts/prepare_build.sh | 10 +- scripts/utils.sh | 26 ++++ spec/controllers/admin/runners_controller_spec.rb | 11 ++ spec/controllers/admin/users_controller_spec.rb | 50 +++++++ .../groups/settings/ci_cd_controller_spec.rb | 11 ++ .../cycle_analytics/stages_controller_spec.rb | 67 +++++++++ .../value_streams_controller_spec.rb | 43 ++++++ .../analytics/cycle_analytics/project_stages.rb | 1 + .../cycle_analytics/project_value_streams.rb | 9 ++ spec/factories/users.rb | 4 + spec/features/admin/users/users_spec.rb | 1 + spec/features/projects/navbar_spec.rb | 15 +- spec/features/projects/user_uses_shortcuts_spec.rb | 30 ++-- .../analytics/cycle_analytics/stage_finder_spec.rb | 24 ++++ spec/finders/ci/runners_finder_spec.rb | 43 ------ spec/helpers/users_helper_spec.rb | 10 ++ spec/lib/gitlab/import_export/all_models.yml | 1 + .../projects/menus/deployments_menu_spec.rb | 71 ++++++++++ .../projects/menus/operations_menu_spec.rb | 106 ++++---------- .../menus/project_information_menu_spec.rb | 30 ++-- ...oject_value_stream_id_to_project_stages_spec.rb | 41 ++++++ .../cycle_analytics/project_stage_spec.rb | 1 + .../cycle_analytics/project_value_stream_spec.rb | 39 +++++ spec/models/project_spec.rb | 3 +- spec/models/user_spec.rb | 17 ++- .../analytics/cycle_analytics/stage_entity_spec.rb | 14 ++ .../cycle_analytics/stages/list_service_spec.rb | 25 ++++ spec/services/users/ban_service_spec.rb | 50 +++++++ .../cycle_analytics_stage_shared_examples.rb | 13 ++ .../layouts/nav/sidebar/_project.html.haml_spec.rb | 148 +++++++++++++++++-- .../packages/nuget/extraction_worker_spec.rb | 9 ++ .../packages/rubygems/extraction_worker_spec.rb | 14 ++ 94 files changed, 1828 insertions(+), 340 deletions(-) create mode 100644 app/controllers/projects/analytics/cycle_analytics/stages_controller.rb create mode 100644 app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb create mode 100644 app/finders/analytics/cycle_analytics/stage_finder.rb create mode 100644 app/models/analytics/cycle_analytics/project_value_stream.rb create mode 100644 app/serializers/analytics/cycle_analytics/configuration_entity.rb create mode 100644 app/serializers/analytics/cycle_analytics/event_entity.rb create mode 100644 app/serializers/analytics/cycle_analytics/stage_entity.rb create mode 100644 app/serializers/analytics/cycle_analytics/value_stream_entity.rb create mode 100644 app/serializers/analytics/cycle_analytics/value_stream_serializer.rb create mode 100644 app/services/analytics/cycle_analytics/stages/base_service.rb create mode 100644 app/services/analytics/cycle_analytics/stages/list_service.rb create mode 100644 app/services/users/ban_service.rb create mode 100644 app/views/admin/users/_ban_user.html.haml create mode 100644 changelogs/unreleased/324206-extraction-error-rescue.yml create mode 100644 changelogs/unreleased/329208-project-level-value-stream.yml create mode 100644 changelogs/unreleased/afontaine-inject-feature-flag-limit.yml create mode 100644 changelogs/unreleased/ban-user-state-ui.yml create mode 100644 config/feature_flags/development/ban_user_feature_flag.yml create mode 100644 db/migrate/20210503105022_create_project_value_streams.rb create mode 100644 db/migrate/20210503105845_add_project_value_stream_id_to_project_stages.rb create mode 100644 db/schema_migrations/20210503105022 create mode 100644 db/schema_migrations/20210503105845 create mode 100644 doc/user/admin_area/moderate_users.md create mode 100644 lib/sidebars/projects/menus/deployments_menu.rb create mode 100644 spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb create mode 100644 spec/controllers/projects/analytics/cycle_analytics/value_streams_controller_spec.rb create mode 100644 spec/factories/analytics/cycle_analytics/project_value_streams.rb create mode 100644 spec/finders/analytics/cycle_analytics/stage_finder_spec.rb create mode 100644 spec/lib/sidebars/projects/menus/deployments_menu_spec.rb create mode 100644 spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb create mode 100644 spec/models/analytics/cycle_analytics/project_value_stream_spec.rb create mode 100644 spec/serializers/analytics/cycle_analytics/stage_entity_spec.rb create mode 100644 spec/services/analytics/cycle_analytics/stages/list_service_spec.rb create mode 100644 spec/services/users/ban_service_spec.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bfc4e4f7316..c255d6d87fc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -53,6 +53,8 @@ workflow: variables: RAILS_ENV: "test" NODE_ENV: "test" + BUNDLE_WITHOUT: "production:development" + BUNDLE_INSTALL_FLAGS: "--jobs=$(nproc) --retry=3 --quiet" # we override the max_old_space_size to prevent OOM errors NODE_OPTIONS: --max_old_space_size=3584 SIMPLECOV: "true" diff --git a/.gitlab/ci/qa.gitlab-ci.yml b/.gitlab/ci/qa.gitlab-ci.yml index 788b482f0a6..5097fd28eeb 100644 --- a/.gitlab/ci/qa.gitlab-ci.yml +++ b/.gitlab/ci/qa.gitlab-ci.yml @@ -4,11 +4,13 @@ - .qa-cache stage: test needs: [] + variables: + USE_BUNDLE_INSTALL: "false" + SETUP_DB: "false" before_script: - - '[ "$FOSS_ONLY" = "1" ] && rm -rf ee/ qa/spec/ee/ qa/qa/specs/features/ee/ qa/qa/ee/ qa/qa/ee.rb' + - !reference [.default-before_script, before_script] - cd qa/ - - bundle install --clean --jobs=$(nproc) --path=vendor --retry=3 --without=development --quiet - - bundle check + - bundle_install_script qa:internal: extends: diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml index f534f341841..834ba040501 100644 --- a/.gitlab/ci/rails.gitlab-ci.yml +++ b/.gitlab/ci/rails.gitlab-ci.yml @@ -8,7 +8,8 @@ .minimal-bundle-install: script: - - run_timed_command "bundle install --jobs=$(nproc) --path=vendor --retry=3 --quiet --without default development test production puma unicorn kerberos metrics omnibus ed25519" + - export BUNDLE_WITHOUT="${BUNDLE_WITHOUT}:default:test:puma:unicorn:kerberos:metrics:omnibus:ed25519" + - bundle_install_script .base-script: script: diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml index 78305a651d1..f87a5759836 100644 --- a/.gitlab/ci/review.gitlab-ci.yml +++ b/.gitlab/ci/review.gitlab-ci.yml @@ -218,8 +218,8 @@ danger-review: stage: test needs: [] before_script: - - source ./scripts/utils.sh - - run_timed_command "bundle install --jobs=$(nproc) --path=vendor --retry=3 --quiet --with danger" + - source scripts/utils.sh + - bundle_install_script "--with danger" - run_timed_command "retry yarn install --frozen-lockfile" script: - > diff --git a/app/assets/javascripts/feature_flags/components/feature_flags.vue b/app/assets/javascripts/feature_flags/components/feature_flags.vue index 115d68de5c9..9aa1accb0f2 100644 --- a/app/assets/javascripts/feature_flags/components/feature_flags.vue +++ b/app/assets/javascripts/feature_flags/components/feature_flags.vue @@ -35,8 +35,9 @@ export default { inject: { newUserListPath: { default: '' }, newFeatureFlagPath: { default: '' }, - canUserConfigure: { required: true }, - featureFlagsLimitExceeded: { required: true }, + canUserConfigure: {}, + featureFlagsLimitExceeded: {}, + featureFlagsLimit: {}, }, data() { const scope = getParameterByName('scope') || SCOPES.FEATURE_FLAG_SCOPE; diff --git a/app/assets/javascripts/feature_flags/index.js b/app/assets/javascripts/feature_flags/index.js index a92805d5d85..d2371a2aa8b 100644 --- a/app/assets/javascripts/feature_flags/index.js +++ b/app/assets/javascripts/feature_flags/index.js @@ -24,6 +24,7 @@ export default () => { newFeatureFlagPath, newUserListPath, featureFlagsLimitExceeded, + featureFlagsLimit, } = el.dataset; return new Vue({ @@ -40,7 +41,8 @@ export default () => { canUserConfigure: canUserAdminFeatureFlag !== undefined, newFeatureFlagPath, newUserListPath, - featureFlagsLimitExceeded, + featureFlagsLimitExceeded: featureFlagsLimitExceeded !== undefined, + featureFlagsLimit, }, render(createElement) { return createElement(FeatureFlagsComponent); diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index 576b148fbff..40ec68c1d46 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -7,9 +7,11 @@ class Admin::RunnersController < Admin::ApplicationController feature_category :continuous_integration + NUMBER_OF_RUNNERS_PER_PAGE = 30 + def index finder = Ci::RunnersFinder.new(current_user: current_user, params: params) - @runners = finder.execute + @runners = finder.execute.page(params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE) @active_runners_count = Ci::Runner.online.count @sort = finder.sort_key end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 9f0d911f5ef..2e9229db56c 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -7,6 +7,7 @@ class Admin::UsersController < Admin::ApplicationController before_action :user, except: [:index, :cohorts, :new, :create] before_action :check_impersonation_availability, only: :impersonate before_action :ensure_destroy_prerequisites_met, only: [:destroy] + before_action :check_ban_user_feature_flag, only: [:ban] feature_category :users @@ -130,6 +131,24 @@ class Admin::UsersController < Admin::ApplicationController end end + def ban + result = Users::BanService.new(current_user).execute(user) + + if result[:status] == :success + redirect_back_or_admin_user(notice: _("Successfully banned")) + else + redirect_back_or_admin_user(alert: _("Error occurred. User was not banned")) + end + end + + def unban + if update_user { |user| user.activate } + redirect_back_or_admin_user(notice: _("Successfully unbanned")) + else + redirect_back_or_admin_user(alert: _("Error occurred. User was not unbanned")) + end + end + def unlock if update_user { |user| user.unlock_access! } redirect_back_or_admin_user(alert: _("Successfully unlocked")) @@ -325,6 +344,10 @@ class Admin::UsersController < Admin::ApplicationController access_denied! unless Gitlab.config.gitlab.impersonation_enabled end + def check_ban_user_feature_flag + access_denied! unless Feature.enabled?(:ban_user_feature_flag) + end + def log_impersonation_event Gitlab::AppLogger.info(_("User %{current_user_username} has started impersonating %{username}") % { current_user_username: current_user.username, username: user.username }) end diff --git a/app/controllers/projects/analytics/cycle_analytics/stages_controller.rb b/app/controllers/projects/analytics/cycle_analytics/stages_controller.rb new file mode 100644 index 00000000000..7b4f6739a9b --- /dev/null +++ b/app/controllers/projects/analytics/cycle_analytics/stages_controller.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +class Projects::Analytics::CycleAnalytics::StagesController < Projects::ApplicationController + respond_to :json + + feature_category :planning_analytics + + before_action :authorize_read_cycle_analytics! + before_action :only_default_value_stream_is_allowed! + + def index + result = list_service.execute + + if result.success? + render json: cycle_analytics_configuration(result.payload[:stages]) + else + render json: { message: result.message }, status: result.http_status + end + end + + private + + def only_default_value_stream_is_allowed! + render_404 if params[:value_stream_id] != Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME + end + + def value_stream + Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project) + end + + def list_params + { value_stream: value_stream } + end + + def list_service + Analytics::CycleAnalytics::Stages::ListService.new(parent: @project, current_user: current_user, params: list_params) + end + + def cycle_analytics_configuration(stages) + stage_presenters = stages.map { |s| ::Analytics::CycleAnalytics::StagePresenter.new(s) } + + Analytics::CycleAnalytics::ConfigurationEntity.new(stages: stage_presenters) + end +end diff --git a/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb b/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb new file mode 100644 index 00000000000..03dcb164d94 --- /dev/null +++ b/app/controllers/projects/analytics/cycle_analytics/value_streams_controller.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class Projects::Analytics::CycleAnalytics::ValueStreamsController < Projects::ApplicationController + respond_to :json + + feature_category :planning_analytics + + before_action :authorize_read_cycle_analytics! + + def index + # FOSS users can only see the default value stream + value_streams = [Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(@project)] + + render json: Analytics::CycleAnalytics::ValueStreamSerializer.new.represent(value_streams) + end +end diff --git a/app/finders/analytics/cycle_analytics/stage_finder.rb b/app/finders/analytics/cycle_analytics/stage_finder.rb new file mode 100644 index 00000000000..732e9ff3e00 --- /dev/null +++ b/app/finders/analytics/cycle_analytics/stage_finder.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + class StageFinder + def initialize(parent:, stage_id:) + @parent = parent + @stage_id = stage_id + end + + def execute + build_in_memory_stage_by_name + end + + private + + attr_reader :parent, :stage_id + + def build_in_memory_stage_by_name + parent.cycle_analytics_stages.build(find_in_memory_stage) + end + + def find_in_memory_stage + # raise ActiveRecord::RecordNotFound, so it will behave similarly to AR models and produce 404 response in the controller + raw_stage = Gitlab::Analytics::CycleAnalytics::DefaultStages.all.find do |hash| + hash[:name].eql?(stage_id) + end + + raise(ActiveRecord::RecordNotFound, "Stage with id '#{stage_id}' could not be found") unless raw_stage + + raw_stage + end + end + end +end + +Analytics::CycleAnalytics::StageFinder.prepend_mod_with('Analytics::CycleAnalytics::StageFinder') diff --git a/app/finders/ci/runners_finder.rb b/app/finders/ci/runners_finder.rb index 1b76211c524..60dd977ff94 100644 --- a/app/finders/ci/runners_finder.rb +++ b/app/finders/ci/runners_finder.rb @@ -4,8 +4,6 @@ module Ci class RunnersFinder < UnionFinder include Gitlab::Allowable - NUMBER_OF_RUNNERS_PER_PAGE = 30 - def initialize(current_user:, group: nil, params:) @params = params @group = group @@ -18,7 +16,6 @@ module Ci filter_by_runner_type! filter_by_tag_list! sort! - paginate! @runners.with_tags @@ -77,10 +74,6 @@ module Ci @runners = @runners.order_by(sort_key) end - def paginate! - @runners = @runners.page(@params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE) - end - def filter_by!(scope_name, available_scopes) scope = @params[scope_name] diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 998c697e617..c1d05c2d3cf 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -162,6 +162,49 @@ module UsersHelper header + list end + def user_ban_data(user) + { + path: ban_admin_user_path(user), + method: 'put', + modal_attributes: { + title: s_('AdminUsers|Ban user %{username}?') % { username: sanitize_name(user.name) }, + message: s_('AdminUsers|You can unban their account in the future. Their data remains intact.'), + okVariant: 'warning', + okTitle: s_('AdminUsers|Ban') + }.to_json + } + end + + def user_unban_data(user) + { + path: unban_admin_user_path(user), + method: 'put', + modal_attributes: { + title: s_('AdminUsers|Unban %{username}?') % { username: sanitize_name(user.name) }, + message: s_('AdminUsers|You ban their account in the future if necessary.'), + okVariant: 'info', + okTitle: s_('AdminUsers|Unban') + }.to_json + } + end + + def user_ban_effects + header = tag.p s_('AdminUsers|Banning the user has the following effects:') + + list = tag.ul do + concat tag.li s_('AdminUsers|User will be blocked') + end + + link_start = ''.html_safe % { url: help_page_path("user/admin_area/moderate_users", anchor: "ban-a-user") } + info = tag.p s_('AdminUsers|Learn more about %{link_start}banned users.%{link_end}').html_safe % { link_start: link_start, link_end: ''.html_safe } + + header + list + info + end + + def ban_feature_available? + Feature.enabled?(:ban_user_feature_flag) + end + def user_deactivation_data(user, message) { path: deactivate_admin_user_path(user), @@ -235,6 +278,9 @@ module UsersHelper pending_approval_badge = { text: s_('AdminUsers|Pending approval'), variant: 'info' } return pending_approval_badge if user.blocked_pending_approval? + banned_badge = { text: s_('AdminUsers|Banned'), variant: 'danger' } + return banned_badge if user.banned? + { text: s_('AdminUsers|Blocked'), variant: 'danger' } end diff --git a/app/models/analytics/cycle_analytics/project_stage.rb b/app/models/analytics/cycle_analytics/project_stage.rb index b2c16444a2a..e8b03fa066a 100644 --- a/app/models/analytics/cycle_analytics/project_stage.rb +++ b/app/models/analytics/cycle_analytics/project_stage.rb @@ -7,10 +7,13 @@ module Analytics validates :project, presence: true belongs_to :project + belongs_to :value_stream, class_name: 'Analytics::CycleAnalytics::ProjectValueStream', foreign_key: :project_value_stream_id alias_attribute :parent, :project alias_attribute :parent_id, :project_id + alias_attribute :value_stream_id, :project_value_stream_id + delegate :group, to: :project validate :validate_project_group_for_label_events, if: -> { start_event_label_based? || end_event_label_based? } diff --git a/app/models/analytics/cycle_analytics/project_value_stream.rb b/app/models/analytics/cycle_analytics/project_value_stream.rb new file mode 100644 index 00000000000..3eba7e87b17 --- /dev/null +++ b/app/models/analytics/cycle_analytics/project_value_stream.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class Analytics::CycleAnalytics::ProjectValueStream < ApplicationRecord + belongs_to :project + + has_many :stages, class_name: 'Analytics::CycleAnalytics::ProjectStage' + + validates :project, :name, presence: true + validates :name, length: { minimum: 3, maximum: 100, allow_nil: false }, uniqueness: { scope: :project_id } + + def custom? + false + end + + def stages + [] + end + + def self.build_default_value_stream(project) + new(name: Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME, project: project) + end +end diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb index f1c39dda49d..90d48aa81d0 100644 --- a/app/models/concerns/analytics/cycle_analytics/stage.rb +++ b/app/models/concerns/analytics/cycle_analytics/stage.rb @@ -27,6 +27,7 @@ module Analytics scope :default_stages, -> { where(custom: false) } scope :ordered, -> { order(:relative_position, :id) } scope :for_list, -> { includes(:start_event_label, :end_event_label).ordered } + scope :by_value_stream, -> (value_stream) { where(value_stream_id: value_stream.id) } end def parent=(_) diff --git a/app/models/project.rb b/app/models/project.rb index 6d8f46d9ea6..759633a70d2 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -335,7 +335,8 @@ class Project < ApplicationRecord has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :remote_mirrors, inverse_of: :project - has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage' + has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage', inverse_of: :project + has_many :value_streams, class_name: 'Analytics::CycleAnalytics::ProjectValueStream', inverse_of: :project has_many :external_pull_requests, inverse_of: :project diff --git a/app/models/user.rb b/app/models/user.rb index 4b38ef771a1..4ff6221bcab 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -326,6 +326,7 @@ class User < ApplicationRecord transition deactivated: :blocked transition ldap_blocked: :blocked transition blocked_pending_approval: :blocked + transition banned: :blocked end event :ldap_block do @@ -338,19 +339,24 @@ class User < ApplicationRecord transition blocked: :active transition ldap_blocked: :active transition blocked_pending_approval: :active + transition banned: :active end event :block_pending_approval do transition active: :blocked_pending_approval end + event :ban do + transition active: :banned + end + event :deactivate do # Any additional changes to this event should be also # reflected in app/workers/users/deactivate_dormant_users_worker.rb transition active: :deactivated end - state :blocked, :ldap_blocked, :blocked_pending_approval do + state :blocked, :ldap_blocked, :blocked_pending_approval, :banned do def blocked? true end @@ -377,6 +383,7 @@ class User < ApplicationRecord scope :instance_access_request_approvers_to_be_notified, -> { admins.active.order_recent_sign_in.limit(INSTANCE_ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT) } scope :blocked, -> { with_states(:blocked, :ldap_blocked) } scope :blocked_pending_approval, -> { with_states(:blocked_pending_approval) } + scope :banned, -> { with_states(:banned) } scope :external, -> { where(external: true) } scope :non_external, -> { where(external: false) } scope :confirmed, -> { where.not(confirmed_at: nil) } @@ -598,6 +605,8 @@ class User < ApplicationRecord blocked when 'blocked_pending_approval' blocked_pending_approval + when 'banned' + banned when 'two_factor_disabled' without_two_factor when 'two_factor_enabled' diff --git a/app/serializers/analytics/cycle_analytics/configuration_entity.rb b/app/serializers/analytics/cycle_analytics/configuration_entity.rb new file mode 100644 index 00000000000..45ea7c92758 --- /dev/null +++ b/app/serializers/analytics/cycle_analytics/configuration_entity.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + class ConfigurationEntity < Grape::Entity + include RequestAwareEntity + + expose :events, using: Analytics::CycleAnalytics::EventEntity + expose :stages, using: Analytics::CycleAnalytics::StageEntity + + private + + def events + (stage_events.events - stage_events.internal_events).sort_by(&:name) + end + + def stage_events + Gitlab::Analytics::CycleAnalytics::StageEvents + end + end + end +end diff --git a/app/serializers/analytics/cycle_analytics/event_entity.rb b/app/serializers/analytics/cycle_analytics/event_entity.rb new file mode 100644 index 00000000000..b9abf722c8d --- /dev/null +++ b/app/serializers/analytics/cycle_analytics/event_entity.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + class EventEntity < Grape::Entity + expose :name + expose :identifier + expose :type + expose :can_be_start_event?, as: :can_be_start_event + expose :allowed_end_events + + private + + def type + object.label_based? ? 'label' : 'simple' + end + + def can_be_start_event? + pairing_rules.has_key?(object) + end + + def allowed_end_events + pairing_rules.fetch(object, []).map do |event| + event.identifier unless stage_events.internal_events.include?(event) + end.compact + end + + def pairing_rules + stage_events.pairing_rules + end + + def stage_events + Gitlab::Analytics::CycleAnalytics::StageEvents + end + end + end +end diff --git a/app/serializers/analytics/cycle_analytics/stage_entity.rb b/app/serializers/analytics/cycle_analytics/stage_entity.rb new file mode 100644 index 00000000000..b24148802d0 --- /dev/null +++ b/app/serializers/analytics/cycle_analytics/stage_entity.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + class StageEntity < Grape::Entity + expose :title + expose :hidden + expose :legend + expose :description + expose :id + expose :custom + expose :start_event_identifier, if: -> (s) { s.custom? } + expose :end_event_identifier, if: -> (s) { s.custom? } + expose :start_event_label, using: LabelEntity, if: -> (s) { s.start_event_label_based? } + expose :end_event_label, using: LabelEntity, if: -> (s) { s.end_event_label_based? } + expose :start_event_html_description + expose :end_event_html_description + + def id + object.id || object.name + end + + def start_event_html_description + html_description(object.start_event) + end + + def end_event_html_description + html_description(object.end_event) + end + + private + + def html_description(event) + Banzai::Renderer.render(event.markdown_description, { group: object.group, project: nil }) + end + end + end +end diff --git a/app/serializers/analytics/cycle_analytics/value_stream_entity.rb b/app/serializers/analytics/cycle_analytics/value_stream_entity.rb new file mode 100644 index 00000000000..1943efcc63d --- /dev/null +++ b/app/serializers/analytics/cycle_analytics/value_stream_entity.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + class ValueStreamEntity < Grape::Entity + expose :name + expose :id + expose :is_custom do |object| + object.custom? + end + expose :stages, using: Analytics::CycleAnalytics::StageEntity + + private + + def id + object.id || object.name # use the name `default` if the record is not persisted + end + + def stages + object.stages.map { |s| ::Analytics::CycleAnalytics::StagePresenter.new(s) } # rubocop: disable CodeReuse/Presenter + end + end + end +end diff --git a/app/serializers/analytics/cycle_analytics/value_stream_serializer.rb b/app/serializers/analytics/cycle_analytics/value_stream_serializer.rb new file mode 100644 index 00000000000..ffd7aa882e4 --- /dev/null +++ b/app/serializers/analytics/cycle_analytics/value_stream_serializer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + class ValueStreamSerializer < BaseSerializer + entity ::Analytics::CycleAnalytics::ValueStreamEntity + end + end +end diff --git a/app/services/analytics/cycle_analytics/stages/base_service.rb b/app/services/analytics/cycle_analytics/stages/base_service.rb new file mode 100644 index 00000000000..b676eff0a0b --- /dev/null +++ b/app/services/analytics/cycle_analytics/stages/base_service.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + module Stages + class BaseService + include Gitlab::Allowable + + DEFAULT_VALUE_STREAM_NAME = 'default' + + def initialize(parent:, current_user:, params: {}) + @parent = parent + @current_user = current_user + @params = params + end + + def execute + raise NotImplementedError + end + + private + + attr_reader :parent, :current_user, :params + + def success(stage, http_status = :created) + ServiceResponse.success(payload: { stage: stage }, http_status: http_status) + end + + def forbidden + ServiceResponse.error(message: 'Forbidden', payload: {}, http_status: :forbidden) + end + + def build_default_stages + Gitlab::Analytics::CycleAnalytics::DefaultStages.all.map do |stage_params| + parent.cycle_analytics_stages.build(stage_params.merge(value_stream: value_stream)) + end + end + + def value_stream + @value_stream ||= params[:value_stream] + end + end + end + end +end + +Analytics::CycleAnalytics::Stages::BaseService.prepend_mod_with('Analytics::CycleAnalytics::Stages::BaseService') diff --git a/app/services/analytics/cycle_analytics/stages/list_service.rb b/app/services/analytics/cycle_analytics/stages/list_service.rb new file mode 100644 index 00000000000..a6b94ef8295 --- /dev/null +++ b/app/services/analytics/cycle_analytics/stages/list_service.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + module Stages + class ListService < Analytics::CycleAnalytics::Stages::BaseService + def execute + return forbidden unless allowed? + + success(build_default_stages) + end + + private + + def allowed? + can?(current_user, :read_cycle_analytics, parent) + end + + def success(stages) + ServiceResponse.success(payload: { stages: stages }) + end + end + end + end +end + +Analytics::CycleAnalytics::Stages::ListService.prepend_mod_with('Analytics::CycleAnalytics::Stages::ListService') diff --git a/app/services/users/ban_service.rb b/app/services/users/ban_service.rb new file mode 100644 index 00000000000..247ed14966b --- /dev/null +++ b/app/services/users/ban_service.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Users + class BanService < BaseService + def initialize(current_user) + @current_user = current_user + end + + def execute(user) + if user.ban + log_event(user) + success + else + messages = user.errors.full_messages + error(messages.uniq.join('. ')) + end + end + + private + + def log_event(user) + Gitlab::AppLogger.info(message: "User banned", user: "#{user.username}", email: "#{user.email}", banned_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}") + end + end +end diff --git a/app/views/admin/users/_ban_user.html.haml b/app/views/admin/users/_ban_user.html.haml new file mode 100644 index 00000000000..229c88adb7f --- /dev/null +++ b/app/views/admin/users/_ban_user.html.haml @@ -0,0 +1,9 @@ +- if ban_feature_available? + .card.border-warning + .card-header.bg-warning.gl-text-white + = s_('AdminUsers|Ban user') + .card-body + = user_ban_effects + %br + %button.btn.gl-button.btn-warning.js-confirm-modal-button{ data: user_ban_data(user) } + = s_('AdminUsers|Ban user') diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index ade3581e5b9..be04e87f8b9 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -3,6 +3,9 @@ - if @user.blocked_pending_approval? %span.cred = s_('AdminUsers|(Pending approval)') + - elsif @user.banned? + %span.cred + = s_('AdminUsers|(Banned)') - elsif @user.blocked? %span.cred = s_('AdminUsers|(Blocked)') diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml index efa50ec47f5..e4438f38a47 100644 --- a/app/views/admin/users/_users.html.haml +++ b/app/views/admin/users/_users.html.haml @@ -28,6 +28,11 @@ = link_to admin_users_path(filter: "blocked") do = s_('AdminUsers|Blocked') %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.blocked) + - if ban_feature_available? + = nav_link(html_options: { class: active_when(params[:filter] == 'banned') }) do + = link_to admin_users_path(filter: "banned") do + = s_('AdminUsers|Banned') + %small.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm= limited_counter_with_delimiter(User.banned) = nav_link(html_options: { class: "#{active_when(params[:filter] == 'blocked_pending_approval')} filter-blocked-pending-approval" }) do = link_to admin_users_path(filter: "blocked_pending_approval"), data: { qa_selector: 'pending_approval_tab' } do = s_('AdminUsers|Pending approval') diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 03ddf5c76a0..19cc29668f5 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -176,6 +176,20 @@ - if @user.blocked_pending_approval? = render 'admin/users/approve_user', user: @user = render 'admin/users/reject_pending_user', user: @user + - elsif @user.banned? + .gl-card.border-info.gl-mb-5 + .gl-card-header.gl-bg-blue-500.gl-text-white + = _('This user is banned') + .gl-card-body + %p= _('A banned user cannot:') + %ul + %li= _('Log in') + %li= _('Access Git repositories') + - link_start = ''.html_safe % { url: help_page_path("user/admin_area/moderate_users", anchor: "ban-a-user") } + = s_('AdminUsers|Learn more about %{link_start}banned users.%{link_end}').html_safe % { link_start: link_start, link_end: ''.html_safe } + %p + %button.btn.gl-button.btn-info.js-confirm-modal-button{ data: user_unban_data(@user) } + = s_('AdminUsers|Unban user') - else .gl-card.border-info.gl-mb-5 .gl-card-header.gl-bg-blue-500.gl-text-white @@ -190,6 +204,7 @@ = s_('AdminUsers|Unblock user') - elsif !@user.internal? = render 'admin/users/block_user', user: @user + = render 'admin/users/ban_user', user: @user - if @user.access_locked? .card.border-info.gl-mb-5 diff --git a/app/workers/packages/nuget/extraction_worker.rb b/app/workers/packages/nuget/extraction_worker.rb index 97f900c4ff2..4128b229ebe 100644 --- a/app/workers/packages/nuget/extraction_worker.rb +++ b/app/workers/packages/nuget/extraction_worker.rb @@ -17,8 +17,7 @@ module Packages ::Packages::Nuget::UpdatePackageFromMetadataService.new(package_file).execute - rescue ::Packages::Nuget::MetadataExtractionService::ExtractionError, - ::Packages::Nuget::UpdatePackageFromMetadataService::InvalidMetadataError => e + rescue StandardError => e Gitlab::ErrorTracking.log_exception(e, project_id: package_file.project_id) package_file.package.update_column(:status, :error) end diff --git a/app/workers/packages/rubygems/extraction_worker.rb b/app/workers/packages/rubygems/extraction_worker.rb index a5630e89140..fc32654a2c1 100644 --- a/app/workers/packages/rubygems/extraction_worker.rb +++ b/app/workers/packages/rubygems/extraction_worker.rb @@ -19,7 +19,7 @@ module Packages ::Packages::Rubygems::ProcessGemService.new(package_file).execute - rescue ::Packages::Rubygems::ProcessGemService::ExtractionError => e + rescue StandardError => e Gitlab::ErrorTracking.log_exception(e, project_id: package_file.project_id) package_file.package.update_column(:status, :error) end diff --git a/changelogs/unreleased/324206-extraction-error-rescue.yml b/changelogs/unreleased/324206-extraction-error-rescue.yml new file mode 100644 index 00000000000..dcbf5400a88 --- /dev/null +++ b/changelogs/unreleased/324206-extraction-error-rescue.yml @@ -0,0 +1,5 @@ +--- +title: Log additional package extraction errors +merge_request: 61745 +author: +type: other diff --git a/changelogs/unreleased/329208-project-level-value-stream.yml b/changelogs/unreleased/329208-project-level-value-stream.yml new file mode 100644 index 00000000000..cc5e986e68d --- /dev/null +++ b/changelogs/unreleased/329208-project-level-value-stream.yml @@ -0,0 +1,5 @@ +--- +title: Create database structure to support project value streams +merge_request: 60925 +author: +type: other diff --git a/changelogs/unreleased/afontaine-inject-feature-flag-limit.yml b/changelogs/unreleased/afontaine-inject-feature-flag-limit.yml new file mode 100644 index 00000000000..464cf91a8e1 --- /dev/null +++ b/changelogs/unreleased/afontaine-inject-feature-flag-limit.yml @@ -0,0 +1,5 @@ +--- +title: Inject Feature Flags Limit Value +merge_request: 61621 +author: +type: fixed diff --git a/changelogs/unreleased/ban-user-state-ui.yml b/changelogs/unreleased/ban-user-state-ui.yml new file mode 100644 index 00000000000..6da3d575dc9 --- /dev/null +++ b/changelogs/unreleased/ban-user-state-ui.yml @@ -0,0 +1,5 @@ +--- +title: Ban user state and UI +merge_request: 61292 +author: +type: added diff --git a/config/feature_flags/development/ban_user_feature_flag.yml b/config/feature_flags/development/ban_user_feature_flag.yml new file mode 100644 index 00000000000..6765e82e252 --- /dev/null +++ b/config/feature_flags/development/ban_user_feature_flag.yml @@ -0,0 +1,8 @@ +--- +name: ban_user_feature_flag +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61292 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/330667 +milestone: '13.12' +type: development +group: group::access +default_enabled: false diff --git a/config/routes/admin.rb b/config/routes/admin.rb index e7f851f7de4..2ba00e3bf66 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -21,6 +21,8 @@ namespace :admin do get :keys put :block put :unblock + put :ban + put :unban put :deactivate put :activate put :unlock diff --git a/config/routes/project.rb b/config/routes/project.rb index 43b0e2a5568..d62e2f1b2f2 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -267,6 +267,15 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do end get '/cycle_analytics', to: redirect('%{namespace_id}/%{project_id}/-/value_stream_analytics') + namespace :analytics do + resource :cycle_analytics, only: :show, path: 'value_stream_analytics' + scope module: :cycle_analytics, as: 'cycle_analytics', path: 'value_stream_analytics' do + resources :value_streams, only: [:index] do + resources :stages, only: [:index] + end + end + end + concerns :clusterable namespace :serverless do diff --git a/db/migrate/20210503105022_create_project_value_streams.rb b/db/migrate/20210503105022_create_project_value_streams.rb new file mode 100644 index 00000000000..775ab03ad4b --- /dev/null +++ b/db/migrate/20210503105022_create_project_value_streams.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class CreateProjectValueStreams < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + INDEX_NAME = 'index_analytics_ca_project_value_streams_on_project_id_and_name' + + def up + create_table_with_constraints :analytics_cycle_analytics_project_value_streams do |t| + t.timestamps_with_timezone + t.references(:project, + null: false, + index: false, + foreign_key: { to_table: :projects, on_delete: :cascade } + ) + t.text :name, null: false + t.index [:project_id, :name], unique: true, name: INDEX_NAME + t.text_limit :name, 100 + end + end + + def down + with_lock_retries do + drop_table :analytics_cycle_analytics_project_value_streams + end + end +end diff --git a/db/migrate/20210503105845_add_project_value_stream_id_to_project_stages.rb b/db/migrate/20210503105845_add_project_value_stream_id_to_project_stages.rb new file mode 100644 index 00000000000..d888ab4943c --- /dev/null +++ b/db/migrate/20210503105845_add_project_value_stream_id_to_project_stages.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class AddProjectValueStreamIdToProjectStages < ActiveRecord::Migration[6.0] + disable_ddl_transaction! + + INDEX_NAME = 'index_analytics_ca_project_stages_on_value_stream_id' + + class ProjectValueStream < ActiveRecord::Base + self.table_name = 'analytics_cycle_analytics_project_stages' + + include EachBatch + end + + def up + ProjectValueStream.reset_column_information + # The table was never used, there is no user-facing code that modifies the table, it should be empty. + # Since there is no functionality present that depends on this data, it's safe to delete the rows. + ProjectValueStream.each_batch(of: 100) do |relation| + relation.delete_all + end + + transaction do + add_reference :analytics_cycle_analytics_project_stages, :project_value_stream, null: false, index: { name: INDEX_NAME }, foreign_key: { on_delete: :cascade, to_table: :analytics_cycle_analytics_project_value_streams }, type: :bigint # rubocop: disable Migration/AddReference, Rails/NotNullColumn + end + end + + def down + remove_reference :analytics_cycle_analytics_project_stages, :project_value_stream + end +end diff --git a/db/schema_migrations/20210503105022 b/db/schema_migrations/20210503105022 new file mode 100644 index 00000000000..ada5b2db7da --- /dev/null +++ b/db/schema_migrations/20210503105022 @@ -0,0 +1 @@ +de8bf6c02589bf308914d43e5cd44dae91d3bbabcdaafcebdb96fba0a09b20bc \ No newline at end of file diff --git a/db/schema_migrations/20210503105845 b/db/schema_migrations/20210503105845 new file mode 100644 index 00000000000..ff2c910491b --- /dev/null +++ b/db/schema_migrations/20210503105845 @@ -0,0 +1 @@ +2fdcb66e511d8322ea8fc4de66ecce859f8e91b2a9da22336281a1e784d9b4a5 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 1337d16a635..f3e3840085b 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9051,7 +9051,8 @@ CREATE TABLE analytics_cycle_analytics_project_stages ( end_event_label_id bigint, hidden boolean DEFAULT false NOT NULL, custom boolean DEFAULT true NOT NULL, - name character varying(255) NOT NULL + name character varying(255) NOT NULL, + project_value_stream_id bigint NOT NULL ); CREATE SEQUENCE analytics_cycle_analytics_project_stages_id_seq @@ -9063,6 +9064,24 @@ CREATE SEQUENCE analytics_cycle_analytics_project_stages_id_seq ALTER SEQUENCE analytics_cycle_analytics_project_stages_id_seq OWNED BY analytics_cycle_analytics_project_stages.id; +CREATE TABLE analytics_cycle_analytics_project_value_streams ( + id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL, + project_id bigint NOT NULL, + name text NOT NULL, + CONSTRAINT check_9b1970a898 CHECK ((char_length(name) <= 100)) +); + +CREATE SEQUENCE analytics_cycle_analytics_project_value_streams_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE analytics_cycle_analytics_project_value_streams_id_seq OWNED BY analytics_cycle_analytics_project_value_streams.id; + CREATE TABLE analytics_devops_adoption_segment_selections ( id bigint NOT NULL, segment_id bigint NOT NULL, @@ -19369,6 +19388,8 @@ ALTER TABLE ONLY analytics_cycle_analytics_group_value_streams ALTER COLUMN id S ALTER TABLE ONLY analytics_cycle_analytics_project_stages ALTER COLUMN id SET DEFAULT nextval('analytics_cycle_analytics_project_stages_id_seq'::regclass); +ALTER TABLE ONLY analytics_cycle_analytics_project_value_streams ALTER COLUMN id SET DEFAULT nextval('analytics_cycle_analytics_project_value_streams_id_seq'::regclass); + ALTER TABLE ONLY analytics_devops_adoption_segment_selections ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_segment_selections_id_seq'::regclass); ALTER TABLE ONLY analytics_devops_adoption_segments ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_segments_id_seq'::regclass); @@ -20449,6 +20470,9 @@ ALTER TABLE ONLY analytics_cycle_analytics_group_value_streams ALTER TABLE ONLY analytics_cycle_analytics_project_stages ADD CONSTRAINT analytics_cycle_analytics_project_stages_pkey PRIMARY KEY (id); +ALTER TABLE ONLY analytics_cycle_analytics_project_value_streams + ADD CONSTRAINT analytics_cycle_analytics_project_value_streams_pkey PRIMARY KEY (id); + ALTER TABLE ONLY analytics_devops_adoption_segment_selections ADD CONSTRAINT analytics_devops_adoption_segment_selections_pkey PRIMARY KEY (id); @@ -22262,6 +22286,10 @@ CREATE INDEX index_analytics_ca_project_stages_on_relative_position ON analytics CREATE INDEX index_analytics_ca_project_stages_on_start_event_label_id ON analytics_cycle_analytics_project_stages USING btree (start_event_label_id); +CREATE INDEX index_analytics_ca_project_stages_on_value_stream_id ON analytics_cycle_analytics_project_stages USING btree (project_value_stream_id); + +CREATE UNIQUE INDEX index_analytics_ca_project_value_streams_on_project_id_and_name ON analytics_cycle_analytics_project_value_streams USING btree (project_id, name); + CREATE INDEX index_analytics_cycle_analytics_group_stages_custom_only ON analytics_cycle_analytics_group_stages USING btree (id) WHERE (custom = true); CREATE UNIQUE INDEX index_analytics_devops_adoption_segments_on_namespace_id ON analytics_devops_adoption_segments USING btree (namespace_id); @@ -26519,6 +26547,9 @@ ALTER TABLE ONLY namespace_admin_notes ALTER TABLE ONLY web_hook_logs_archived ADD CONSTRAINT fk_rails_666826e111 FOREIGN KEY (web_hook_id) REFERENCES web_hooks(id) ON DELETE CASCADE; +ALTER TABLE ONLY analytics_cycle_analytics_project_value_streams + ADD CONSTRAINT fk_rails_669f4ba293 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; + ALTER TABLE ONLY jira_imports ADD CONSTRAINT fk_rails_675d38c03b FOREIGN KEY (label_id) REFERENCES labels(id) ON DELETE SET NULL; @@ -26621,6 +26652,9 @@ ALTER TABLE ONLY ci_subscriptions_projects ALTER TABLE ONLY terraform_states ADD CONSTRAINT fk_rails_78f54ca485 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY analytics_cycle_analytics_project_stages + ADD CONSTRAINT fk_rails_796a7dbc9c FOREIGN KEY (project_value_stream_id) REFERENCES analytics_cycle_analytics_project_value_streams(id) ON DELETE CASCADE; + ALTER TABLE ONLY software_license_policies ADD CONSTRAINT fk_rails_7a7a2a92de FOREIGN KEY (software_license_id) REFERENCES software_licenses(id) ON DELETE CASCADE; diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md index b95f8c2984d..a4ad496b667 100644 --- a/doc/api/project_import_export.md +++ b/doc/api/project_import_export.md @@ -196,6 +196,65 @@ NOTE: The maximum import file size can be set by the Administrator, default is `0` (unlimited).. As an administrator, you can modify the maximum import file size. To do so, use the `max_import_size` option in the [Application settings API](settings.md#change-application-settings) or the [Admin UI](../user/admin_area/settings/account_and_limit_settings.md). Default [modified](https://gitlab.com/gitlab-org/gitlab/-/issues/251106) from 50MB to 0 in GitLab 13.8. +## Import a file from a remote object storage + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/282503) in GitLab 13.12 in [Beta](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta). + +This endpoint is behind a feature flag that is disabled by default. + +To enable this endpoint: + +```ruby +Feature.enable(:import_project_from_remote_file) +``` + +To disable this endpoint: + +```ruby +Feature.disable(:import_project_from_remote_file) +``` + +```plaintext +POST /projects/remote-import +``` + +| Attribute | Type | Required | Description | +| ----------------- | -------------- | -------- | ---------------------------------------- | +| `namespace` | integer/string | no | The ID or path of the namespace to import the project to. Defaults to the current user's namespace. | +| `name` | string | no | The name of the project to import. If not provided, defaults to the path of the project. | +| `url` | string | yes | URL for the file to import. | +| `path` | string | yes | Name and path for the new project. | +| `overwrite` | boolean | no | Whether to overwrite a project with the same path when importing. Defaults to `false`. | +| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md). | + +The passed override parameters take precedence over all values defined in the export file. + +```shell +curl --request POST \ + --header "PRIVATE-TOKEN: " \ + --url "https://gitlab.example.com/api/v4/projects/remote-import" \ + --data '{"url":"https://remoteobject/file?token=123123","path":"remote-project"}' +``` + +```json +{ + "id": 1, + "description": null, + "name": "remote-project", + "name_with_namespace": "Administrator / remote-project", + "path": "remote-project", + "path_with_namespace": "root/remote-project", + "created_at": "2018-02-13T09:05:58.023Z", + "import_status": "scheduled", + "correlation_id": "mezklWso3Za", + "failed_relations": [], + "import_error": null +} +``` + +The `ContentType` header must return a valid number. The maximum file size is 10 gigabytes. +The `ContentLength` header must be `application/gzip`. + ## Import status Get the status of an import. diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md index f3d09903108..b177a7e0138 100644 --- a/doc/development/i18n/externalization.md +++ b/doc/development/i18n/externalization.md @@ -795,6 +795,10 @@ aren't in the message with ID `1 pipeline`. ## Adding a new language +A new language should only be added as an option in User Preferences once at least 10% of the +strings have been translated and approved. Even though a larger number of strings may have been +translated, only the approved translations display in the GitLab UI. + NOTE: [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/221012) in GitLab 13.3: Languages with less than 2% of translations are not available in the UI. diff --git a/doc/subscriptions/self_managed/index.md b/doc/subscriptions/self_managed/index.md index db0d54ff14c..648ad0c70a5 100644 --- a/doc/subscriptions/self_managed/index.md +++ b/doc/subscriptions/self_managed/index.md @@ -48,8 +48,8 @@ using [Seat Link](#seat-link). A _billable user_ counts against the number of subscription seats. Every user is considered a billable user, with the following exceptions: -- [Deactivated users](../../user/admin_area/activating_deactivating_users.md#deactivating-a-user) and - [blocked users](../../user/admin_area/blocking_unblocking_users.md) don't count as billable users in the current subscription. When they are either deactivated or blocked they release a _billable user_ seat. However, they may +- [Deactivated users](../../user/admin_area/moderate_users.md#deactivating-a-user) and + [blocked users](../../user/admin_area/moderate_users.md#blocking-a-user) don't count as billable users in the current subscription. When they are either deactivated or blocked they release a _billable user_ seat. However, they may count toward overages in the subscribed seat count. - Users who are [pending approval](../../user/admin_area/approving_users.md). - Members with Guest permissions on an Ultimate subscription. @@ -183,7 +183,7 @@ Starting 30 days before a subscription expires, GitLab notifies administrators o We recommend following these steps during renewal: -1. Prune any inactive or unwanted users by [blocking them](../../user/admin_area/blocking_unblocking_users.md#blocking-a-user). +1. Prune any inactive or unwanted users by [blocking them](../../user/admin_area/moderate_users.md#blocking-a-user). 1. Determine if you have a need for user growth in the upcoming subscription. 1. Log in to the [Customers Portal](https://customers.gitlab.com/customers/sign_in) and select the **Renew** button beneath your existing subscription. diff --git a/doc/user/admin_area/activating_deactivating_users.md b/doc/user/admin_area/activating_deactivating_users.md index 144ee2dbf98..cafc7caf981 100644 --- a/doc/user/admin_area/activating_deactivating_users.md +++ b/doc/user/admin_area/activating_deactivating_users.md @@ -1,69 +1,8 @@ --- -stage: Manage -group: Access -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: howto +redirect_to: 'moderate_users.md' --- -# Activating and deactivating users +This document was moved to [another location](moderate_users.md). -GitLab administrators can deactivate and activate users. - -## Deactivating a user - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22257) in GitLab 12.4. - -In order to temporarily prevent access by a GitLab user that has no recent activity, administrators -can choose to deactivate the user. - -Deactivating a user is functionally identical to [blocking a user](blocking_unblocking_users.md), -with the following differences: - -- It does not prohibit the user from logging back in via the UI. -- Once a deactivated user logs back into the GitLab UI, their account is set to active. - -A deactivated user: - -- Cannot access Git repositories or the API. -- Will not receive any notifications from GitLab. -- Will not be able to use [slash commands](../../integration/slash_commands.md). - -Personal projects, and group and user history of the deactivated user will be left intact. - -A user can be deactivated from the Admin Area. To do this: - -1. Navigate to **Admin Area > Overview > Users**. -1. Select a user. -1. Under the **Account** tab, click **Deactivate user**. - -Please note that for the deactivation option to be visible to an admin, the user: - -- Must be currently active. -- Must not have signed in, or have any activity, in the last 90 days. - -Users can also be deactivated using the [GitLab API](../../api/users.md#deactivate-user). - -NOTE: -A deactivated user does not consume a [seat](../../subscriptions/self_managed/index.md#billable-users). - -## Activating a user - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22257) in GitLab 12.4. - -A deactivated user can be activated from the Admin Area. - -To do this: - -1. Navigate to **Admin Area > Overview > Users**. -1. Click on the **Deactivated** tab. -1. Select a user. -1. Under the **Account** tab, click **Activate user**. - -Users can also be activated using the [GitLab API](../../api/users.md#activate-user). - -NOTE: -Activating a user changes the user's state to active and consumes a -[seat](../../subscriptions/self_managed/index.md#billable-users). - -NOTE: -A deactivated user can also activate their account themselves by logging back in via the UI. + + diff --git a/doc/user/admin_area/approving_users.md b/doc/user/admin_area/approving_users.md index 9141d7f488d..2b3b90cb1a4 100644 --- a/doc/user/admin_area/approving_users.md +++ b/doc/user/admin_area/approving_users.md @@ -21,7 +21,7 @@ When a user registers for an account while this setting is enabled: A user pending approval: -- Is functionally identical to a [blocked](blocking_unblocking_users.md) user. +- Is functionally identical to a [blocked](moderate_users.md#blocking-a-user) user. - Cannot sign in. - Cannot access Git repositories or the GitLab API. - Does not receive any notifications from GitLab. diff --git a/doc/user/admin_area/blocking_unblocking_users.md b/doc/user/admin_area/blocking_unblocking_users.md index 14a5b5085cb..cafc7caf981 100644 --- a/doc/user/admin_area/blocking_unblocking_users.md +++ b/doc/user/admin_area/blocking_unblocking_users.md @@ -1,51 +1,8 @@ --- -stage: Manage -group: Access -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: howto +redirect_to: 'moderate_users.md' --- -# Blocking and unblocking users +This document was moved to [another location](moderate_users.md). -GitLab administrators block and unblock users. - -## Blocking a user - -In order to completely prevent access of a user to the GitLab instance, administrators can choose to -block the user. - -Users can be blocked [via an abuse report](review_abuse_reports.md#blocking-users), -or directly from the Admin Area. To do this: - -1. Navigate to **Admin Area > Overview > Users**. -1. Select a user. -1. Under the **Account** tab, click **Block user**. - -A blocked user: - -- Cannot log in. -- Cannot access Git repositories or the API. -- Does not receive any notifications from GitLab. -- Cannot use [slash commands](../../integration/slash_commands.md). - -Personal projects, and group and user history of the blocked user are left intact. - -Users can also be blocked using the [GitLab API](../../api/users.md#block-user). - -NOTE: -A blocked user does not consume a [seat](../../subscriptions/self_managed/index.md#billable-users). - -## Unblocking a user - -A blocked user can be unblocked from the Admin Area. To do this: - -1. Navigate to **Admin Area > Overview > Users**. -1. Click on the **Blocked** tab. -1. Select a user. -1. Under the **Account** tab, click **Unblock user**. - -Users can also be unblocked using the [GitLab API](../../api/users.md#unblock-user). - -NOTE: -Unblocking a user changes the user's state to active and consumes a -[seat](../../subscriptions/self_managed/index.md#billable-users). + + diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md index b0a84d981c9..5d1fde1c767 100644 --- a/doc/user/admin_area/index.md +++ b/doc/user/admin_area/index.md @@ -117,8 +117,8 @@ To list users matching a specific criteria, click on one of the following tabs o - **2FA Enabled** - **2FA Disabled** - **External** -- **[Blocked](blocking_unblocking_users.md)** -- **[Deactivated](activating_deactivating_users.md)** +- **[Blocked](moderate_users.md#blocking-a-user)** +- **[Deactivated](moderate_users.md#deactivating-a-user)** - **Without projects** For each user, the following are listed: diff --git a/doc/user/admin_area/moderate_users.md b/doc/user/admin_area/moderate_users.md new file mode 100644 index 00000000000..c04003dd75f --- /dev/null +++ b/doc/user/admin_area/moderate_users.md @@ -0,0 +1,157 @@ +--- +stage: Manage +group: Access +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: howto +--- + +# Moderate users + +GitLab administrators can moderate user access by blocking, banning, or deactivating users. + +## Blocking and unblocking users + +GitLab administrators can block and unblock users. + +### Blocking a user + +In order to completely prevent access of a user to the GitLab instance, +administrators can choose to block the user. + +Users can be blocked [via an abuse report](review_abuse_reports.md#blocking-users), +or directly from the Admin Area. To do this: + +1. Navigate to **Admin Area > Overview > Users**. +1. Select a user. +1. Under the **Account** tab, click **Block user**. + +A blocked user: + +- Cannot log in. +- Cannot access Git repositories or the API. +- Does not receive any notifications from GitLab. +- Cannot use [slash commands](../../integration/slash_commands.md). + +Personal projects, and group and user history of the blocked user are left intact. + +Users can also be blocked using the [GitLab API](../../api/users.md#block-user). + +NOTE: +A blocked user does not consume a [seat](../../subscriptions/self_managed/index.md#billable-users). + +### Unblocking a user + +A blocked user can be unblocked from the Admin Area. To do this: + +1. Navigate to **Admin Area > Overview > Users**. +1. Click on the **Blocked** tab. +1. Select a user. +1. Under the **Account** tab, click **Unblock user**. + +Users can also be unblocked using the [GitLab API](../../api/users.md#unblock-user). + +NOTE: +Unblocking a user changes the user's state to active and consumes a +[seat](../../subscriptions/self_managed/index.md#billable-users). + +## Activating and deactivating users + +GitLab administrators can deactivate and activate users. + +### Deactivating a user + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22257) in GitLab 12.4. + +In order to temporarily prevent access by a GitLab user that has no recent activity, +administrators can choose to deactivate the user. + +Deactivating a user is functionally identical to [blocking a user](#blocking-and-unblocking-users), +with the following differences: + +- It does not prohibit the user from logging back in via the UI. +- Once a deactivated user logs back into the GitLab UI, their account is set to active. + +A deactivated user: + +- Cannot access Git repositories or the API. +- Will not receive any notifications from GitLab. +- Will not be able to use [slash commands](../../integration/slash_commands.md). + +Personal projects, and group and user history of the deactivated user are left intact. + +A user can be deactivated from the Admin Area. To do this: + +1. Navigate to **Admin Area > Overview > Users**. +1. Select a user. +1. Under the **Account** tab, click **Deactivate user**. + +Please note that for the deactivation option to be visible to an admin, the user: + +- Must be currently active. +- Must not have signed in, or have any activity, in the last 90 days. + +Users can also be deactivated using the [GitLab API](../../api/users.md#deactivate-user). + +NOTE: +A deactivated user does not consume a [seat](../../subscriptions/self_managed/index.md#billable-users). + +### Activating a user + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22257) in GitLab 12.4. + +A deactivated user can be activated from the Admin Area. + +To do this: + +1. Navigate to **Admin Area > Overview > Users**. +1. Click on the **Deactivated** tab. +1. Select a user. +1. Under the **Account** tab, click **Activate user**. + +Users can also be activated using the [GitLab API](../../api/users.md#activate-user). + +NOTE: +Activating a user changes the user's state to active and consumes a +[seat](../../subscriptions/self_managed/index.md#billable-users). + +NOTE: +A deactivated user can also activate their account themselves by logging back in via the UI. + +## Ban and unban users + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/327353) in GitLab 13.12. + +GitLab administrators can ban users. + +NOTE: +This feature is behind a feature flag that is disabled by default. GitLab administrators +with access to the GitLab Rails console can [enable](../../administration/feature_flags.md) +this feature for your GitLab instance. + +### Ban a user + +To completely block a user, administrators can choose to ban the user. + +Users can be banned using the Admin Area. To do this: + +1. Navigate to **Admin Area > Overview > Users**. +1. Select a user. +1. Under the **Account** tab, click **Ban user**. + +NOTE: +This feature is a work in progress. Currently, banning a user +only blocks them and does not hide their comments or issues. +This functionality will be implemented in follow up issues. + +### Unban a user + +A banned user can be unbanned using the Admin Area. To do this: + +1. Navigate to **Admin Area > Overview > Users**. +1. Click on the **Banned** tab. +1. Select a user. +1. Under the **Account** tab, click **Unban user**. + +NOTE: +Unbanning a user changes the user's state to active and consumes a +[seat](../../subscriptions/self_managed/index.md#billable-users). diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md index 6848126e163..10d276bd224 100644 --- a/doc/user/application_security/container_scanning/index.md +++ b/doc/user/application_security/container_scanning/index.md @@ -501,29 +501,38 @@ For details on saving and transporting Docker images as a file, see Docker's doc #### Automating container scanning vulnerability database updates with a pipeline -For those using Clair, it can be worthwhile to set up a [scheduled pipeline](../../../ci/pipelines/schedules.md) -to build a new version of the vulnerabilities database on a preset schedule. Automating -this with a pipeline means you do not have to do it manually each time. You can use the following -`.gitlab-yml.ci` as a template: +We recommend that you set up a [scheduled pipeline](../../../ci/pipelines/schedules.md) +to fetch the latest vulnerabilities database on a preset schedule. Because the Clair scanner is +deprecated, the latest vulnerabilities are currently only available for the Trivy scanner. +Automating this with a pipeline means you do not have to do it manually each time. You can use the +following `.gitlab-yml.ci` example as a template. ```yaml -image: docker:stable +variables: + # If using Clair, uncomment the following 2 lines and comment the Trivy lines below + # SOURCE_IMAGE: arminc/clair-db:latest + # TARGET_IMAGE: $CI_REGISTRY/$CI_PROJECT_PATH/clair-vulnerabilities-db -stages: - - build + # If using Trivy, uncomment the following 3 lines and comment the Clair lines above + CS_MAJOR_VERSION: 4 # ensure that this value matches the one you use in your scanning jobs + SOURCE_IMAGE: registry.gitlab.com/gitlab-org/security-products/analyzers/container-scanning:$CS_MAJOR_VERSION + TARGET_IMAGE: $CI_REGISTRY/$CI_PROJECT_PATH/gitlab-container-scanning -build_latest_vulnerabilities: - stage: build +image: docker:stable + +update-vulnerabilities-db: services: - - docker:19.03.12-dind + - docker:19-dind script: - - docker pull arminc/clair-db:latest - - docker tag arminc/clair-db:latest $CI_REGISTRY/namespace/clair-vulnerabilities-db - - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY - - docker push $CI_REGISTRY/namespace/clair-vulnerabilities-db + - docker pull $SOURCE_IMAGE + - docker tag $SOURCE_IMAGE $TARGET_IMAGE + - echo "$CI_REGISTRY_PASSWORD" | docker login $CI_REGISTRY --username $CI_REGISTRY_USER --password-stdin + - docker push $TARGET_IMAGE ``` -The above template works for a GitLab Docker registry running on a local installation, however, if you're using a non-GitLab Docker registry, you need to change the `$CI_REGISTRY` value and the `docker login` credentials to match the details of your local registry. +The above template works for a GitLab Docker registry running on a local installation. However, if +you're using a non-GitLab Docker registry, you must change the `$CI_REGISTRY` value and the +`docker login` credentials to match your local registry's details. ## Running the standalone container scanning tool diff --git a/doc/user/application_security/vulnerabilities/index.md b/doc/user/application_security/vulnerabilities/index.md index b96451fa67d..965b856504d 100644 --- a/doc/user/application_security/vulnerabilities/index.md +++ b/doc/user/application_security/vulnerabilities/index.md @@ -182,7 +182,7 @@ The following vulnerability scanners and their databases are regularly updated: | Secure scanning tool | Vulnerabilities database updates | |:----------------------------------------------------------------|----------------------------------| -| [Container Scanning](../container_scanning/index.md) | Uses `clair`. The latest `clair-db` version is used for each job by running the [`latest` Docker image tag](https://gitlab.com/gitlab-org/gitlab/blob/438a0a56dc0882f22bdd82e700554525f552d91b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml#L37). The `clair-db` database [is updated daily according to the author](https://github.com/arminc/clair-local-scan#clair-server-or-local). | +| [Container Scanning](../container_scanning/index.md) | Uses either `trivy` or `clair`. For the `trivy` scanner, a job runs on a daily basis to build a new image with the latest vulnerability database updates from the [upstream `trivy-db`](https://github.com/aquasecurity/trivy-db). For the `clair` scanner, the latest `clair-db` version is used; `clair-db` database [is updated daily according to the author](https://github.com/arminc/clair-local-scan#clair-server-or-local). | | [Dependency Scanning](../dependency_scanning/index.md) | Relies on `bundler-audit` (for Ruby gems), `retire.js` (for npm packages), and `gemnasium` (the GitLab tool for all libraries). Both `bundler-audit` and `retire.js` fetch their vulnerabilities data from GitHub repositories, so vulnerabilities added to `ruby-advisory-db` and `retire.js` are immediately available. The tools themselves are updated once per month if there's a new version. The [Gemnasium DB](https://gitlab.com/gitlab-org/security-products/gemnasium-db) is updated at least once a week. See our [current measurement of time from CVE being issued to our product being updated](https://about.gitlab.com/handbook/engineering/development/performance-indicators/#cve-issue-to-update). | | [Dynamic Application Security Testing (DAST)](../dast/index.md) | The scanning engine is updated on a periodic basis. See the [version of the underlying tool `zaproxy`](https://gitlab.com/gitlab-org/security-products/dast/blob/master/Dockerfile#L1). The scanning rules are downloaded at scan runtime. | | [Static Application Security Testing (SAST)](../sast/index.md) | Relies exclusively on [the tools GitLab wraps](../sast/index.md#supported-languages-and-frameworks). The underlying analyzers are updated at least once per month if a relevant update is available. The vulnerabilities database is updated by the upstream tools. | diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md index 3b0c59ac31c..6d7b009bb09 100644 --- a/doc/user/packages/container_registry/index.md +++ b/doc/user/packages/container_registry/index.md @@ -698,6 +698,13 @@ You can, however, remove the Container Registry for a project: The **Packages & Registries > Container Registry** entry is removed from the project's sidebar. +## Manifest lists and garbage collection + +Manifest lists are commonly used for creating multi-architecture images. If you rely on manifest +lists, you should tag all the individual manifests referenced by a list in their respective +repositories, and not just the manifest list itself. This ensures that those manifests aren't +garbage collected, as long as they have at least one tag pointing to them. + ## Troubleshooting the GitLab Container Registry ### Docker connection error diff --git a/doc/user/profile/account/delete_account.md b/doc/user/profile/account/delete_account.md index c0560269f00..361353a0f8c 100644 --- a/doc/user/profile/account/delete_account.md +++ b/doc/user/profile/account/delete_account.md @@ -69,7 +69,7 @@ username of the original user. When using the **Delete user and contributions** option, **all** associated records are removed. This includes all of the items mentioned above including issues, merge requests, notes/comments, and more. Consider -[blocking a user](../../admin_area/blocking_unblocking_users.md) +[blocking a user](../../admin_area/moderate_users.md#blocking-a-user) or using the **Delete user** option instead. When a user is deleted from an [abuse report](../../admin_area/review_abuse_reports.md) diff --git a/doc/user/upgrade_email_bypass.md b/doc/user/upgrade_email_bypass.md index 199c1a47e04..7d2f2395815 100644 --- a/doc/user/upgrade_email_bypass.md +++ b/doc/user/upgrade_email_bypass.md @@ -73,7 +73,7 @@ Your account has been blocked. Fatal: Could not read from remote repository Your primary email address is not confirmed. ``` -You can assure your users that they have not been [Blocked](admin_area/blocking_unblocking_users.md) by an administrator. +You can assure your users that they have not been [Blocked](admin_area/moderate_users.md#blocking-and-unblocking-users) by an administrator. When affected users see this message, they must confirm their email address before they can commit code. ## What do I need to know as an administrator of a GitLab self-managed Instance? diff --git a/lib/sidebars/projects/menus/deployments_menu.rb b/lib/sidebars/projects/menus/deployments_menu.rb new file mode 100644 index 00000000000..f3d13e12258 --- /dev/null +++ b/lib/sidebars/projects/menus/deployments_menu.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module Sidebars + module Projects + module Menus + class DeploymentsMenu < ::Sidebars::Menu + override :configure_menu_items + def configure_menu_items + return false if Feature.disabled?(:sidebar_refactor, context.current_user, default_enabled: :yaml) + + add_item(feature_flags_menu_item) + add_item(environments_menu_item) + add_item(releases_menu_item) + + true + end + + override :link + def link + renderable_items.first.link + end + + override :extra_container_html_options + def extra_container_html_options + { + class: 'shortcuts-deployments' + } + end + + override :title + def title + _('Deployments') + end + + override :sprite_icon + def sprite_icon + 'environment' + end + + private + + def feature_flags_menu_item + unless can?(context.current_user, :read_feature_flag, context.project) + return ::Sidebars::NilMenuItem.new(item_id: :feature_flags) + end + + ::Sidebars::MenuItem.new( + title: _('Feature Flags'), + link: project_feature_flags_path(context.project), + active_routes: { controller: :feature_flags }, + container_html_options: { class: 'shortcuts-feature-flags' }, + item_id: :feature_flags + ) + end + + def environments_menu_item + unless can?(context.current_user, :read_environment, context.project) + return ::Sidebars::NilMenuItem.new(item_id: :environments) + end + + ::Sidebars::MenuItem.new( + title: _('Environments'), + link: project_environments_path(context.project), + active_routes: { controller: :environments }, + container_html_options: { class: 'shortcuts-environments' }, + item_id: :environments + ) + end + + def releases_menu_item + if !can?(context.current_user, :read_release, context.project) || + context.project.empty_repo? + return ::Sidebars::NilMenuItem.new(item_id: :releases) + end + + ::Sidebars::MenuItem.new( + title: _('Releases'), + link: project_releases_path(context.project), + item_id: :releases, + active_routes: { controller: :releases }, + container_html_options: { class: 'shortcuts-deployments-releases' } + ) + end + end + end + end +end diff --git a/lib/sidebars/projects/menus/operations_menu.rb b/lib/sidebars/projects/menus/operations_menu.rb index 3503dcaa0c1..8d72a9ff258 100644 --- a/lib/sidebars/projects/menus/operations_menu.rb +++ b/lib/sidebars/projects/menus/operations_menu.rb @@ -196,7 +196,8 @@ module Sidebars end def environments_menu_item - unless can?(context.current_user, :read_environment, context.project) + if Feature.enabled?(:sidebar_refactor, context.current_user, default_enabled: :yaml) || + !can?(context.current_user, :read_environment, context.project) return ::Sidebars::NilMenuItem.new(item_id: :environments) end @@ -210,7 +211,8 @@ module Sidebars end def feature_flags_menu_item - unless can?(context.current_user, :read_feature_flag, context.project) + if Feature.enabled?(:sidebar_refactor, context.current_user, default_enabled: :yaml) || + !can?(context.current_user, :read_feature_flag, context.project) return ::Sidebars::NilMenuItem.new(item_id: :feature_flags) end diff --git a/lib/sidebars/projects/menus/project_information_menu.rb b/lib/sidebars/projects/menus/project_information_menu.rb index 82b0971f747..80b3acecc2f 100644 --- a/lib/sidebars/projects/menus/project_information_menu.rb +++ b/lib/sidebars/projects/menus/project_information_menu.rb @@ -84,9 +84,7 @@ module Sidebars end def releases_menu_item - if !can?(context.current_user, :read_release, context.project) || context.project.empty_repo? - return ::Sidebars::NilMenuItem.new(item_id: :releases) - end + return ::Sidebars::NilMenuItem.new(item_id: :releases) unless show_releases? ::Sidebars::MenuItem.new( title: _('Releases'), @@ -97,6 +95,12 @@ module Sidebars ) end + def show_releases? + Feature.disabled?(:sidebar_refactor, context.current_user, default_enabled: :yaml) && + can?(context.current_user, :read_release, context.project) && + !context.project.empty_repo? + end + def labels_menu_item if Feature.disabled?(:sidebar_refactor, context.current_user) return ::Sidebars::NilMenuItem.new(item_id: :labels) diff --git a/lib/sidebars/projects/panel.rb b/lib/sidebars/projects/panel.rb index 261d51626cc..3754d381dcc 100644 --- a/lib/sidebars/projects/panel.rb +++ b/lib/sidebars/projects/panel.rb @@ -7,7 +7,17 @@ module Sidebars def configure_menus set_scope_menu(Sidebars::Projects::Menus::ScopeMenu.new(context)) set_hidden_menu(Sidebars::Projects::Menus::HiddenMenu.new(context)) + add_menus + end + + override :aria_label + def aria_label + _('Project navigation') + end + + private + def add_menus add_menu(Sidebars::Projects::Menus::ProjectInformationMenu.new(context)) add_menu(Sidebars::Projects::Menus::LearnGitlabMenu.new(context)) add_menu(Sidebars::Projects::Menus::RepositoryMenu.new(context)) @@ -17,6 +27,7 @@ module Sidebars add_menu(Sidebars::Projects::Menus::MergeRequestsMenu.new(context)) add_menu(Sidebars::Projects::Menus::CiCdMenu.new(context)) add_menu(Sidebars::Projects::Menus::SecurityComplianceMenu.new(context)) + add_menu(Sidebars::Projects::Menus::DeploymentsMenu.new(context)) add_menu(Sidebars::Projects::Menus::OperationsMenu.new(context)) add_menu(Sidebars::Projects::Menus::InfrastructureMenu.new(context)) add_menu(Sidebars::Projects::Menus::PackagesRegistriesMenu.new(context)) @@ -28,13 +39,6 @@ module Sidebars add_menu(Sidebars::Projects::Menus::SettingsMenu.new(context)) end - override :aria_label - def aria_label - _('Project navigation') - end - - private - def confluence_or_wiki_menu confluence_menu = ::Sidebars::Projects::Menus::ConfluenceMenu.new(context) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 01a5e40941a..f54303ad457 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1375,6 +1375,9 @@ msgstr "" msgid "A Let's Encrypt account will be configured for this GitLab installation using your email address. You will receive emails to warn of expiring certificates." msgstr "" +msgid "A banned user cannot:" +msgstr "" + msgid "A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages" msgstr "" @@ -2437,6 +2440,9 @@ msgstr "" msgid "AdminUsers|(Admin)" msgstr "" +msgid "AdminUsers|(Banned)" +msgstr "" + msgid "AdminUsers|(Blocked)" msgstr "" @@ -2503,6 +2509,21 @@ msgstr "" msgid "AdminUsers|Automatically marked as default internal user" msgstr "" +msgid "AdminUsers|Ban" +msgstr "" + +msgid "AdminUsers|Ban user" +msgstr "" + +msgid "AdminUsers|Ban user %{username}?" +msgstr "" + +msgid "AdminUsers|Banned" +msgstr "" + +msgid "AdminUsers|Banning the user has the following effects:" +msgstr "" + msgid "AdminUsers|Be added to groups and projects" msgstr "" @@ -2590,6 +2611,9 @@ msgstr "" msgid "AdminUsers|It's you!" msgstr "" +msgid "AdminUsers|Learn more about %{link_start}banned users.%{link_end}" +msgstr "" + msgid "AdminUsers|Log in" msgstr "" @@ -2671,6 +2695,15 @@ msgstr "" msgid "AdminUsers|To confirm, type %{username}" msgstr "" +msgid "AdminUsers|Unban" +msgstr "" + +msgid "AdminUsers|Unban %{username}?" +msgstr "" + +msgid "AdminUsers|Unban user" +msgstr "" + msgid "AdminUsers|Unblock" msgstr "" @@ -2686,6 +2719,9 @@ msgstr "" msgid "AdminUsers|Unlock user %{username}?" msgstr "" +msgid "AdminUsers|User will be blocked" +msgstr "" + msgid "AdminUsers|User will not be able to access git repositories" msgstr "" @@ -2722,6 +2758,9 @@ msgstr "" msgid "AdminUsers|You are about to permanently delete the user %{username}. This will delete all of the issues, merge requests, and groups linked to them. To avoid data loss, consider using the %{strongStart}block user%{strongEnd} feature instead. Once you %{strongStart}Delete user%{strongEnd}, it cannot be undone or recovered." msgstr "" +msgid "AdminUsers|You ban their account in the future if necessary." +msgstr "" + msgid "AdminUsers|You can always block their account again if needed." msgstr "" @@ -2734,6 +2773,9 @@ msgstr "" msgid "AdminUsers|You can always unblock their account, their data will remain intact." msgstr "" +msgid "AdminUsers|You can unban their account in the future. Their data remains intact." +msgstr "" + msgid "AdminUsers|You cannot remove your own admin rights." msgstr "" @@ -4985,6 +5027,9 @@ msgstr "" msgid "BillingPlans|@%{user_name} you are currently using the %{plan_name}." msgstr "" +msgid "BillingPlans|Compare all plans" +msgstr "" + msgid "BillingPlans|Congratulations, your free trial is activated." msgstr "" @@ -5018,6 +5063,9 @@ msgstr "" msgid "BillingPlans|To manage the plan for this group, visit the billing section of %{parent_billing_page_link}." msgstr "" +msgid "BillingPlans|Upgrade to GitLab %{planNameForUpgrade}" +msgstr "" + msgid "BillingPlans|While GitLab is ending availability of the Bronze plan, you can still renew your Bronze subscription one additional time before %{eoa_bronze_plan_end_date}. We are also offering a limited time free upgrade to our Premium Plan (up to 25 users)! Learn more about the changes and offers in our %{announcement_link}." msgstr "" @@ -12898,12 +12946,18 @@ msgstr "" msgid "Error occurred. A blocked user must be unblocked to be activated" msgstr "" +msgid "Error occurred. User was not banned" +msgstr "" + msgid "Error occurred. User was not blocked" msgstr "" msgid "Error occurred. User was not confirmed" msgstr "" +msgid "Error occurred. User was not unbanned" +msgstr "" + msgid "Error occurred. User was not unblocked" msgstr "" @@ -31249,6 +31303,9 @@ msgstr "" msgid "Successfully approved" msgstr "" +msgid "Successfully banned" +msgstr "" + msgid "Successfully blocked" msgstr "" @@ -31273,6 +31330,9 @@ msgstr "" msgid "Successfully synced %{synced_timeago}." msgstr "" +msgid "Successfully unbanned" +msgstr "" + msgid "Successfully unblocked" msgstr "" @@ -33382,6 +33442,9 @@ msgstr "" msgid "This user has the %{access} role in the %{name} project." msgstr "" +msgid "This user is banned" +msgstr "" + msgid "This user is blocked" msgstr "" diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh index cd41aa0ff14..5753a0af4f8 100644 --- a/scripts/prepare_build.sh +++ b/scripts/prepare_build.sh @@ -2,17 +2,9 @@ export SETUP_DB=${SETUP_DB:-true} export USE_BUNDLE_INSTALL=${USE_BUNDLE_INSTALL:-true} -export BUNDLE_INSTALL_FLAGS=${BUNDLE_INSTALL_FLAGS:-"--without=production development --jobs=$(nproc) --path=vendor --retry=3 --quiet"} if [ "$USE_BUNDLE_INSTALL" != "false" ]; then - bundle --version - bundle config set clean 'true' - run_timed_command "bundle install ${BUNDLE_INSTALL_FLAGS}" - run_timed_command "bundle check" - # When we test multiple versions of PG in the same pipeline, we have a single `setup-test-env` - # job but the `pg` gem needs to be rebuilt since it includes extensions (https://guides.rubygems.org/gems-with-extensions). - # Uncomment the following line if multiple versions of PG are tested in the same pipeline. - run_timed_command "bundle pristine pg" + bundle_install_script fi cp config/gitlab.yml.example config/gitlab.yml diff --git a/scripts/utils.sh b/scripts/utils.sh index 2e9839e4df8..d4436e1171d 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -13,6 +13,32 @@ function retry() { return 1 } +function bundle_install_script() { + local extra_install_args="${1}" + + if [[ "${extra_install_args}" =~ "--without" ]]; then + echoerr "The '--without' flag shouldn't be passed as it would replace the default \${BUNDLE_WITHOUT} (currently set to '${BUNDLE_WITHOUT}')." + echoerr "Set the 'BUNDLE_WITHOUT' variable instead, e.g. '- export BUNDLE_WITHOUT=\"\${BUNDLE_WITHOUT}:any:other:group:not:to:install\"'." + exit 1; + fi; + + bundle --version + bundle config set path 'vendor' + bundle config set clean 'true' + + echo $BUNDLE_WITHOUT + bundle config + + run_timed_command "bundle install ${BUNDLE_INSTALL_FLAGS} ${extra_install_args} && bundle check" + + if [[ $(bundle info pg) ]]; then + # When we test multiple versions of PG in the same pipeline, we have a single `setup-test-env` + # job but the `pg` gem needs to be rebuilt since it includes extensions (https://guides.rubygems.org/gems-with-extensions). + # Uncomment the following line if multiple versions of PG are tested in the same pipeline. + run_timed_command "bundle pristine pg" + fi +} + function setup_db_user_only() { source scripts/create_postgres_user.sh } diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb index 45ea8949bf2..3984784f045 100644 --- a/spec/controllers/admin/runners_controller_spec.rb +++ b/spec/controllers/admin/runners_controller_spec.rb @@ -34,6 +34,17 @@ RSpec.describe Admin::RunnersController do expect(response.body).to have_content('tag1') expect(response.body).to have_content('tag2') end + + it 'paginates runners' do + stub_const("Admin::RunnersController::NUMBER_OF_RUNNERS_PER_PAGE", 1) + + create(:ci_runner) + + get :index + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:runners).count).to be(1) + end end describe '#show' do diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 1afd20f5021..722c9c322cc 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -365,6 +365,56 @@ RSpec.describe Admin::UsersController do end end + describe 'PUT ban/:id' do + context 'when ban_user_feature_flag is enabled' do + it 'bans user' do + put :ban, params: { id: user.username } + + user.reload + expect(user.banned?).to be_truthy + expect(flash[:notice]).to eq _('Successfully banned') + end + + context 'when unsuccessful' do + let(:user) { create(:user, :blocked) } + + it 'does not ban user' do + put :ban, params: { id: user.username } + + user.reload + expect(user.banned?).to be_falsey + expect(flash[:alert]).to eq _('Error occurred. User was not banned') + end + end + end + + context 'when ban_user_feature_flag is not enabled' do + before do + stub_feature_flags(ban_user_feature_flag: false) + end + + it 'does not ban user, renders 404' do + put :ban, params: { id: user.username } + + user.reload + expect(user.banned?).to be_falsey + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + describe 'PUT unban/:id' do + let(:banned_user) { create(:user, :banned) } + + it 'unbans user' do + put :unban, params: { id: banned_user.username } + + banned_user.reload + expect(banned_user.banned?).to be_falsey + expect(flash[:notice]).to eq _('Successfully unbanned') + end + end + describe 'PUT unlock/:id' do before do request.env["HTTP_REFERER"] = "/" diff --git a/spec/controllers/groups/settings/ci_cd_controller_spec.rb b/spec/controllers/groups/settings/ci_cd_controller_spec.rb index db16579c3e7..f225d798886 100644 --- a/spec/controllers/groups/settings/ci_cd_controller_spec.rb +++ b/spec/controllers/groups/settings/ci_cd_controller_spec.rb @@ -32,6 +32,17 @@ RSpec.describe Groups::Settings::CiCdController do expect(response).to render_template(:show) expect(assigns(:group_runners)).to match_array([runner_group, runner_project_1, runner_project_2, runner_project_3]) end + + it 'paginates runners' do + stub_const("Groups::Settings::CiCdController::NUMBER_OF_RUNNERS_PER_PAGE", 1) + + create(:ci_runner) + + get :show, params: { group_id: group } + + expect(response).to have_gitlab_http_status(:ok) + expect(assigns(:group_runners).count).to be(1) + end end context 'when user is not owner' do diff --git a/spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb b/spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb new file mode 100644 index 00000000000..3bb841c7c9f --- /dev/null +++ b/spec/controllers/projects/analytics/cycle_analytics/stages_controller_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Analytics::CycleAnalytics::StagesController do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + + let(:params) { { namespace_id: group, project_id: project, value_stream_id: 'default' } } + + before do + sign_in(user) + end + + describe 'GET index' do + context 'when user is member of the project' do + before do + project.add_developer(user) + end + + it 'succeeds' do + get :index, params: params + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'exposes the default stages' do + get :index, params: params + + expect(json_response['stages'].size).to eq(Gitlab::Analytics::CycleAnalytics::DefaultStages.all.size) + end + + context 'when list service fails' do + it 'renders 403' do + expect_next_instance_of(Analytics::CycleAnalytics::Stages::ListService) do |list_service| + expect(list_service).to receive(:allowed?).and_return(false) + end + + get :index, params: params + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + end + + context 'when invalid value stream id is given' do + before do + params[:value_stream_id] = 1 + end + + it 'renders 404' do + get :index, params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when user is not member of the project' do + it 'renders 404' do + get :index, params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end +end diff --git a/spec/controllers/projects/analytics/cycle_analytics/value_streams_controller_spec.rb b/spec/controllers/projects/analytics/cycle_analytics/value_streams_controller_spec.rb new file mode 100644 index 00000000000..5b434eb2011 --- /dev/null +++ b/spec/controllers/projects/analytics/cycle_analytics/value_streams_controller_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::Analytics::CycleAnalytics::ValueStreamsController do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + + let(:params) { { namespace_id: group, project_id: project } } + + before do + sign_in(user) + end + + describe 'GET index' do + context 'when user is member of the project' do + before do + project.add_developer(user) + end + + it 'succeeds' do + get :index, params: params + + expect(response).to have_gitlab_http_status(:ok) + end + + it 'exposes the default value stream' do + get :index, params: params + + expect(json_response.first['name']).to eq('default') + end + end + + context 'when user is not member of the project' do + it 'renders 404' do + get :index, params: params + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end +end diff --git a/spec/factories/analytics/cycle_analytics/project_stages.rb b/spec/factories/analytics/cycle_analytics/project_stages.rb index 3a481bd20fd..e673c4957b0 100644 --- a/spec/factories/analytics/cycle_analytics/project_stages.rb +++ b/spec/factories/analytics/cycle_analytics/project_stages.rb @@ -6,6 +6,7 @@ FactoryBot.define do sequence(:name) { |n| "Stage ##{n}" } hidden { false } issue_stage + value_stream { association(:cycle_analytics_project_value_stream, project: project) } trait :issue_stage do start_event_identifier { Gitlab::Analytics::CycleAnalytics::StageEvents::IssueCreated.identifier } diff --git a/spec/factories/analytics/cycle_analytics/project_value_streams.rb b/spec/factories/analytics/cycle_analytics/project_value_streams.rb new file mode 100644 index 00000000000..45a6470b0aa --- /dev/null +++ b/spec/factories/analytics/cycle_analytics/project_value_streams.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :cycle_analytics_project_value_stream, class: 'Analytics::CycleAnalytics::ProjectValueStream' do + sequence(:name) { |n| "Value Stream ##{n}" } + + project + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 7ac44d55687..476c57f2d80 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -27,6 +27,10 @@ FactoryBot.define do after(:build) { |user, _| user.block_pending_approval! } end + trait :banned do + after(:build) { |user, _| user.ban! } + end + trait :ldap_blocked do after(:build) { |user, _| user.ldap_block! } end diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb index 36907d4aa60..d3931373ee3 100644 --- a/spec/features/admin/users/users_spec.rb +++ b/spec/features/admin/users/users_spec.rb @@ -85,6 +85,7 @@ RSpec.describe 'Admin::Users' do expect(page).to have_link('2FA Disabled', href: admin_users_path(filter: 'two_factor_disabled')) expect(page).to have_link('External', href: admin_users_path(filter: 'external')) expect(page).to have_link('Blocked', href: admin_users_path(filter: 'blocked')) + expect(page).to have_link('Banned', href: admin_users_path(filter: 'banned')) expect(page).to have_link('Deactivated', href: admin_users_path(filter: 'deactivated')) expect(page).to have_link('Without projects', href: admin_users_path(filter: 'wop')) end diff --git a/spec/features/projects/navbar_spec.rb b/spec/features/projects/navbar_spec.rb index 74f1c639af3..edd33f503fd 100644 --- a/spec/features/projects/navbar_spec.rb +++ b/spec/features/projects/navbar_spec.rb @@ -91,8 +91,6 @@ RSpec.describe 'Project navbar' do _('Error Tracking'), _('Alerts'), _('Incidents'), - _('Environments'), - _('Feature Flags'), _('Product Analytics') ] end @@ -102,7 +100,6 @@ RSpec.describe 'Project navbar' do nav_item: _('Project information'), nav_sub_items: [ _('Activity'), - _('Releases'), _('Labels') ] } @@ -133,6 +130,18 @@ RSpec.describe 'Project navbar' do } ) + insert_after_nav_item( + _('Security & Compliance'), + new_nav_item: { + nav_item: _('Deployments'), + nav_sub_items: [ + _('Feature Flags'), + _('Environments'), + _('Releases') + ] + } + ) + visit project_path(project) end diff --git a/spec/features/projects/user_uses_shortcuts_spec.rb b/spec/features/projects/user_uses_shortcuts_spec.rb index cf97897d976..53466bc59d5 100644 --- a/spec/features/projects/user_uses_shortcuts_spec.rb +++ b/spec/features/projects/user_uses_shortcuts_spec.rb @@ -178,33 +178,45 @@ RSpec.describe 'User uses shortcuts', :js do end end - context 'when navigating to the Operations pages' do - it 'redirects to the Metrics page' do + context 'when navigating to the Deployments page' do + it 'redirects to the Environments page' do find('body').native.send_key('g') - find('body').native.send_key('l') + find('body').native.send_key('e') - expect(page).to have_active_navigation('Operations') - expect(page).to have_active_sub_navigation('Metrics') + expect(page).to have_active_navigation('Deployments') + expect(page).to have_active_sub_navigation('Environments') end + end - it 'redirects to the Environments page' do + context 'when navigating to the Operations pages' do + it 'redirects to the Metrics page' do find('body').native.send_key('g') - find('body').native.send_key('e') + find('body').native.send_key('l') expect(page).to have_active_navigation('Operations') - expect(page).to have_active_sub_navigation('Environments') + expect(page).to have_active_sub_navigation('Metrics') end context 'when feature flag :sidebar_refactor is disabled' do - it 'redirects to the Kubernetes page with active Operations' do + before do stub_feature_flags(sidebar_refactor: false) + end + it 'redirects to the Kubernetes page with active Operations' do find('body').native.send_key('g') find('body').native.send_key('k') expect(page).to have_active_navigation('Operations') expect(page).to have_active_sub_navigation('Kubernetes') end + + it 'redirects to the Environments page' do + find('body').native.send_key('g') + find('body').native.send_key('e') + + expect(page).to have_active_navigation('Operations') + expect(page).to have_active_sub_navigation('Environments') + end end end diff --git a/spec/finders/analytics/cycle_analytics/stage_finder_spec.rb b/spec/finders/analytics/cycle_analytics/stage_finder_spec.rb new file mode 100644 index 00000000000..0275205028a --- /dev/null +++ b/spec/finders/analytics/cycle_analytics/stage_finder_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Analytics::CycleAnalytics::StageFinder do + let(:project) { build(:project) } + + let(:stage_id) { { id: Gitlab::Analytics::CycleAnalytics::DefaultStages.names.first } } + + subject { described_class.new(parent: project, stage_id: stage_id[:id]).execute } + + context 'when looking up in-memory default stage by name exists' do + it { expect(subject).not_to be_persisted } + it { expect(subject.name).to eq(stage_id[:id]) } + end + + context 'when in-memory default stage cannot be found' do + before do + stage_id[:id] = 'unknown_default_stage' + end + + it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) } + end +end diff --git a/spec/finders/ci/runners_finder_spec.rb b/spec/finders/ci/runners_finder_spec.rb index 34639f9b7bd..4df026f2f5f 100644 --- a/spec/finders/ci/runners_finder_spec.rb +++ b/spec/finders/ci/runners_finder_spec.rb @@ -72,17 +72,6 @@ RSpec.describe Ci::RunnersFinder do end end - context 'paginate' do - it 'returns the runners for the specified page' do - stub_const('Ci::RunnersFinder::NUMBER_OF_RUNNERS_PER_PAGE', 1) - runner1 = create :ci_runner, created_at: '2018-07-12 07:00' - runner2 = create :ci_runner, created_at: '2018-07-12 08:00' - - expect(described_class.new(current_user: admin, params: { page: 1 }).execute).to eq [runner2] - expect(described_class.new(current_user: admin, params: { page: 2 }).execute).to eq [runner1] - end - end - context 'non admin user' do it 'returns no runners' do user = create :user @@ -172,38 +161,6 @@ RSpec.describe Ci::RunnersFinder do end end - context 'paginate' do - using RSpec::Parameterized::TableSyntax - - let(:runners) do - [[runner_project_7, runner_project_6, runner_project_5], - [runner_project_4, runner_project_3, runner_project_2], - [runner_project_1, runner_sub_group_4, runner_sub_group_3], - [runner_sub_group_2, runner_sub_group_1, runner_group]] - end - - where(:page, :index) do - 1 | 0 - 2 | 1 - 3 | 2 - 4 | 3 - end - - before do - stub_const('Ci::RunnersFinder::NUMBER_OF_RUNNERS_PER_PAGE', 3) - - group.add_owner(user) - end - - with_them do - let(:params) { { page: page } } - - it 'returns the runners for the specified page' do - expect(subject).to eq(runners[index]) - end - end - end - context 'filter by search term' do let(:params) { { search: 'runner_project_search' } } diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb index 46073b37a3b..862fd58df04 100644 --- a/spec/helpers/users_helper_spec.rb +++ b/spec/helpers/users_helper_spec.rb @@ -136,6 +136,16 @@ RSpec.describe UsersHelper do end end + context 'with a banned user' do + it 'returns the banned badge' do + banned_user = create(:user, :banned) + + badges = helper.user_badges_in_admin_section(banned_user) + + expect(filter_ee_badges(badges)).to eq([text: 'Banned', variant: 'danger']) + end + end + context 'with an admin user' do it "returns the admin badge" do admin_user = create(:admin) diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 89db1ffa8dd..8253cb90ce5 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -352,6 +352,7 @@ project: - cluster_project - creator - cycle_analytics_stages +- value_streams - group - namespace - management_clusters diff --git a/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb b/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb new file mode 100644 index 00000000000..4a60dfde674 --- /dev/null +++ b/spec/lib/sidebars/projects/menus/deployments_menu_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Projects::Menus::DeploymentsMenu do + let_it_be(:project) { create(:project, :repository) } + + let(:user) { project.owner } + let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) } + + describe '#render?' do + subject { described_class.new(context) } + + context 'when menu does not have any menu items' do + it 'returns false' do + allow(subject).to receive(:has_renderable_items?).and_return(false) + + expect(subject.render?).to be false + end + end + + context 'when menu has menu items' do + it 'returns true' do + expect(subject.render?).to be true + end + end + end + + describe 'Menu Items' do + subject { described_class.new(context).renderable_items.index { |e| e.item_id == item_id } } + + shared_examples 'access rights checks' do + specify { is_expected.not_to be_nil } + + describe 'when the user does not have access' do + let(:user) { nil } + + specify { is_expected.to be_nil } + end + end + + shared_examples 'feature flag :sidebar_refactor disabled' do + before do + stub_feature_flags(sidebar_refactor: false) + end + + specify { is_expected.to be_nil } + end + + describe 'Feature Flags' do + let(:item_id) { :feature_flags } + + it_behaves_like 'access rights checks' + it_behaves_like 'feature flag :sidebar_refactor disabled' + end + + describe 'Environments' do + let(:item_id) { :environments } + + it_behaves_like 'access rights checks' + it_behaves_like 'feature flag :sidebar_refactor disabled' + end + + describe 'Releases' do + let(:item_id) { :releases } + + it_behaves_like 'access rights checks' + it_behaves_like 'feature flag :sidebar_refactor disabled' + end + end +end diff --git a/spec/lib/sidebars/projects/menus/operations_menu_spec.rb b/spec/lib/sidebars/projects/menus/operations_menu_spec.rb index 5f52ae0fd7a..4168b577d5e 100644 --- a/spec/lib/sidebars/projects/menus/operations_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/operations_menu_spec.rb @@ -56,9 +56,7 @@ RSpec.describe Sidebars::Projects::Menus::OperationsMenu do context 'Menu items' do subject { described_class.new(context).renderable_items.index { |e| e.item_id == item_id } } - describe 'Metrics Dashboard' do - let(:item_id) { :metrics } - + shared_examples 'access rights checks' do specify { is_expected.not_to be_nil } describe 'when the user does not have access' do @@ -68,153 +66,109 @@ RSpec.describe Sidebars::Projects::Menus::OperationsMenu do end end - describe 'Logs' do - let(:item_id) { :logs } + describe 'Metrics Dashboard' do + let(:item_id) { :metrics } - specify { is_expected.not_to be_nil } + it_behaves_like 'access rights checks' + end - describe 'when the user does not have access' do - let(:user) { nil } + describe 'Logs' do + let(:item_id) { :logs } - specify { is_expected.to be_nil } - end + it_behaves_like 'access rights checks' end describe 'Tracing' do let(:item_id) { :tracing } - specify { is_expected.not_to be_nil } - - describe 'when the user does not have access' do - let(:user) { nil } - - specify { is_expected.to be_nil } - end + it_behaves_like 'access rights checks' end describe 'Error Tracking' do let(:item_id) { :error_tracking } - specify { is_expected.not_to be_nil } - - describe 'when the user does not have access' do - let(:user) { nil } - - specify { is_expected.to be_nil } - end + it_behaves_like 'access rights checks' end describe 'Alert Management' do let(:item_id) { :alert_management } - specify { is_expected.not_to be_nil } - - describe 'when the user does not have access' do - let(:user) { nil } - - specify { is_expected.to be_nil } - end + it_behaves_like 'access rights checks' end describe 'Incidents' do let(:item_id) { :incidents } - specify { is_expected.not_to be_nil } - - describe 'when the user does not have access' do - let(:user) { nil } - - specify { is_expected.to be_nil } - end + it_behaves_like 'access rights checks' end describe 'Serverless' do let(:item_id) { :serverless } - context 'when feature flag :sidebar_refactor is enabled' do - specify { is_expected.to be_nil } - end + specify { is_expected.to be_nil } context 'when feature flag :sidebar_refactor is disabled' do before do stub_feature_flags(sidebar_refactor: false) end - specify { is_expected.not_to be_nil } - - describe 'when the user does not have access' do - let(:user) { nil } - - specify { is_expected.to be_nil } - end + it_behaves_like 'access rights checks' end end describe 'Terraform' do let(:item_id) { :terraform } - context 'when feature flag :sidebar_refactor is enabled' do - specify { is_expected.to be_nil } - end + specify { is_expected.to be_nil } context 'when feature flag :sidebar_refactor is disabled' do before do stub_feature_flags(sidebar_refactor: false) end - specify { is_expected.not_to be_nil } - - describe 'when the user does not have access' do - let(:user) { nil } - - specify { is_expected.to be_nil } - end + it_behaves_like 'access rights checks' end end describe 'Kubernetes' do let(:item_id) { :kubernetes } - context 'when feature flag :sidebar_refactor is enabled' do - specify { is_expected.to be_nil } - end + specify { is_expected.to be_nil } context 'when feature flag :sidebar_refactor is disabled' do before do stub_feature_flags(sidebar_refactor: false) end - specify { is_expected.not_to be_nil } - - describe 'when the user does not have access' do - let(:user) { nil } - - specify { is_expected.to be_nil } - end + it_behaves_like 'access rights checks' end end describe 'Environments' do let(:item_id) { :environments } - specify { is_expected.not_to be_nil } + specify { is_expected.to be_nil } - describe 'when the user does not have access' do - let(:user) { nil } + context 'when feature flag :sidebar_refactor is disabled' do + before do + stub_feature_flags(sidebar_refactor: false) + end - specify { is_expected.to be_nil } + it_behaves_like 'access rights checks' end end describe 'Feature Flags' do let(:item_id) { :feature_flags } - specify { is_expected.not_to be_nil } + specify { is_expected.to be_nil } - describe 'when the user does not have access' do - let(:user) { nil } + context 'when feature flag :sidebar_refactor is disabled' do + before do + stub_feature_flags(sidebar_refactor: false) + end - specify { is_expected.to be_nil } + it_behaves_like 'access rights checks' end end diff --git a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb index d58766ae5e0..d081f3a8b91 100644 --- a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb @@ -14,26 +14,30 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do describe 'Releases' do let(:item_id) { :releases } - context 'when project repository is empty' do - it 'does not include releases menu item' do - allow(project).to receive(:empty_repo?).and_return(true) + specify { is_expected.to be_nil } - is_expected.to be_nil + context 'when feature flag :sidebar_refactor is disabled' do + before do + stub_feature_flags(sidebar_refactor: false) end - end - context 'when project repository is not empty' do - context 'when user can download code' do - it 'includes releases menu item' do - is_expected.to be_present + context 'when project repository is empty' do + it 'does not include releases menu item' do + allow(project).to receive(:empty_repo?).and_return(true) + + is_expected.to be_nil end end - context 'when user cannot download code' do - let(:user) { nil } + context 'when project repository is not empty' do + context 'when user can download code' do + specify { is_expected.not_to be_nil } + end - it 'does not include releases menu item' do - is_expected.to be_nil + context 'when user cannot download code' do + let(:user) { nil } + + specify { is_expected.to be_nil } end end end diff --git a/spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb b/spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb new file mode 100644 index 00000000000..6e1cc63e42a --- /dev/null +++ b/spec/migrations/20210503105845_add_project_value_stream_id_to_project_stages_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require Rails.root.join('db', 'migrate', '20210503105845_add_project_value_stream_id_to_project_stages.rb') + +RSpec.describe AddProjectValueStreamIdToProjectStages, schema: 20210503105022 do + let(:stages) { table(:analytics_cycle_analytics_project_stages) } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + + let(:namespace) { table(:namespaces).create!(name: 'ns1', path: 'nsq1') } + + before do + project = projects.create!(name: 'p1', namespace_id: namespace.id) + + stages.create!( + project_id: project.id, + created_at: Time.now, + updated_at: Time.now, + start_event_identifier: 1, + end_event_identifier: 2, + name: 'stage 1' + ) + + stages.create!( + project_id: project.id, + created_at: Time.now, + updated_at: Time.now, + start_event_identifier: 3, + end_event_identifier: 4, + name: 'stage 2' + ) + end + + it 'deletes the existing rows' do + migrate! + + expect(stages.count).to eq(0) + end +end diff --git a/spec/models/analytics/cycle_analytics/project_stage_spec.rb b/spec/models/analytics/cycle_analytics/project_stage_spec.rb index fce31af619c..9efe90e7d41 100644 --- a/spec/models/analytics/cycle_analytics/project_stage_spec.rb +++ b/spec/models/analytics/cycle_analytics/project_stage_spec.rb @@ -17,6 +17,7 @@ RSpec.describe Analytics::CycleAnalytics::ProjectStage do end it_behaves_like 'value stream analytics stage' do + let(:factory) { :cycle_analytics_project_stage } let(:parent) { build(:project) } let(:parent_name) { :project } end diff --git a/spec/models/analytics/cycle_analytics/project_value_stream_spec.rb b/spec/models/analytics/cycle_analytics/project_value_stream_spec.rb new file mode 100644 index 00000000000..d84ecedc634 --- /dev/null +++ b/spec/models/analytics/cycle_analytics/project_value_stream_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Analytics::CycleAnalytics::ProjectValueStream, type: :model do + describe 'associations' do + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:stages) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_length_of(:name).is_at_most(100) } + + it 'validates uniqueness of name' do + project = create(:project) + create(:cycle_analytics_project_value_stream, name: 'test', project: project) + + value_stream = build(:cycle_analytics_project_value_stream, name: 'test', project: project) + + expect(value_stream).to be_invalid + expect(value_stream.errors.messages).to eq(name: [I18n.t('errors.messages.taken')]) + end + end + + it 'is not custom' do + expect(described_class.new).not_to be_custom + end + + describe '.build_default_value_stream' do + it 'builds the default value stream' do + project = build(:project) + + value_stream = described_class.build_default_value_stream(project) + expect(value_stream.name).to eq('default') + end + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index e5ed8d89145..43cea0ddcd1 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -113,7 +113,8 @@ RSpec.describe Project, factory_default: :keep do it { is_expected.to have_many(:lfs_file_locks) } it { is_expected.to have_many(:project_deploy_tokens) } it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) } - it { is_expected.to have_many(:cycle_analytics_stages) } + it { is_expected.to have_many(:cycle_analytics_stages).inverse_of(:project) } + it { is_expected.to have_many(:value_streams).inverse_of(:project) } it { is_expected.to have_many(:external_pull_requests) } it { is_expected.to have_many(:sourced_pipelines) } it { is_expected.to have_many(:source_pipelines) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0c35b1265ba..cb34917f073 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -728,6 +728,7 @@ RSpec.describe User do let_it_be(:blocked_user) { create(:user, :blocked) } let_it_be(:ldap_blocked_user) { create(:omniauth_user, :ldap_blocked) } let_it_be(:blocked_pending_approval_user) { create(:user, :blocked_pending_approval) } + let_it_be(:banned_user) { create(:user, :banned) } describe '.blocked' do subject { described_class.blocked } @@ -738,7 +739,7 @@ RSpec.describe User do ldap_blocked_user ) - expect(subject).not_to include(active_user, blocked_pending_approval_user) + expect(subject).not_to include(active_user, blocked_pending_approval_user, banned_user) end end @@ -749,6 +750,14 @@ RSpec.describe User do expect(subject).to contain_exactly(blocked_pending_approval_user) end end + + describe '.banned' do + subject { described_class.banned } + + it 'returns only banned users' do + expect(subject).to contain_exactly(banned_user) + end + end end describe ".with_two_factor" do @@ -1934,6 +1943,12 @@ RSpec.describe User do expect(described_class.filter_items('blocked')).to include user end + it 'filters by banned' do + expect(described_class).to receive(:banned).and_return([user]) + + expect(described_class.filter_items('banned')).to include user + end + it 'filters by blocked pending approval' do expect(described_class).to receive(:blocked_pending_approval).and_return([user]) diff --git a/spec/serializers/analytics/cycle_analytics/stage_entity_spec.rb b/spec/serializers/analytics/cycle_analytics/stage_entity_spec.rb new file mode 100644 index 00000000000..90cc7f7827b --- /dev/null +++ b/spec/serializers/analytics/cycle_analytics/stage_entity_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Analytics::CycleAnalytics::StageEntity do + let(:stage) { build(:cycle_analytics_project_stage, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) } + + subject(:entity_json) { described_class.new(Analytics::CycleAnalytics::StagePresenter.new(stage)).as_json } + + it 'exposes start and end event descriptions' do + expect(entity_json).to have_key(:start_event_html_description) + expect(entity_json).to have_key(:end_event_html_description) + end +end diff --git a/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb b/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb new file mode 100644 index 00000000000..24f0123ed3b --- /dev/null +++ b/spec/services/analytics/cycle_analytics/stages/list_service_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Analytics::CycleAnalytics::Stages::ListService do + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + + let(:value_stream) { Analytics::CycleAnalytics::ProjectValueStream.build_default_value_stream(project) } + let(:stages) { subject.payload[:stages] } + + subject { described_class.new(parent: project, current_user: user).execute } + + before_all do + project.add_reporter(user) + end + + it 'returns only the default stages' do + expect(stages.size).to eq(Gitlab::Analytics::CycleAnalytics::DefaultStages.all.size) + end + + it 'provides the default stages as non-persisted objects' do + expect(stages.map(&:id)).to all(be_nil) + end +end diff --git a/spec/services/users/ban_service_spec.rb b/spec/services/users/ban_service_spec.rb new file mode 100644 index 00000000000..0e6ac615da5 --- /dev/null +++ b/spec/services/users/ban_service_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::BanService do + let(:current_user) { create(:admin) } + + subject(:service) { described_class.new(current_user) } + + describe '#execute' do + subject(:operation) { service.execute(user) } + + context 'when successful' do + let(:user) { create(:user) } + + it { is_expected.to eq(status: :success) } + + it "bans the user" do + expect { operation }.to change { user.state }.to('banned') + end + + it "blocks the user" do + expect { operation }.to change { user.blocked? }.from(false).to(true) + end + + it 'logs ban in application logs' do + allow(Gitlab::AppLogger).to receive(:info) + + operation + + expect(Gitlab::AppLogger).to have_received(:info).with(message: "User banned", user: "#{user.username}", email: "#{user.email}", banned_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}") + end + end + + context 'when failed' do + let(:user) { create(:user, :blocked) } + + it 'returns error result' do + aggregate_failures 'error result' do + expect(operation[:status]).to eq(:error) + expect(operation[:message]).to match(/State cannot transition/) + end + end + + it "does not change the user's state" do + expect { operation }.not_to change { user.state } + end + end + end +end diff --git a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb index 17948d648cb..d23f95b2e9e 100644 --- a/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb +++ b/spec/support/shared_examples/models/cycle_analytics_stage_shared_examples.rb @@ -58,6 +58,19 @@ RSpec.shared_examples 'value stream analytics stage' do it { expect(stage).not_to be_valid } end + + # rubocop: disable Rails/SaveBang + describe '.by_value_stream' do + it 'finds stages by value stream' do + stage1 = create(factory) + create(factory) # other stage with different value stream + + result = described_class.by_value_stream(stage1.value_stream) + + expect(result).to eq([stage1]) + end + end + # rubocop: enable Rails/SaveBang end describe '#subject_class' do diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb index 694ba0f1f4f..d633a3d7a9d 100644 --- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb @@ -66,10 +66,20 @@ RSpec.describe 'layouts/nav/sidebar/_project' do end describe 'Releases' do - it 'has a link to the project releases path' do + it 'does not have a link to the project releases path' do render - expect(rendered).to have_link('Releases', href: project_releases_path(project), class: 'shortcuts-project-releases') + expect(rendered).not_to have_link('Releases', href: project_releases_path(project), class: 'shortcuts-project-releases') + end + + context 'when feature flag :sidebar refactor is disabled' do + it 'has a link to the project releases path' do + stub_feature_flags(sidebar_refactor: false) + + render + + expect(rendered).to have_link('Releases', href: project_releases_path(project), class: 'shortcuts-project-releases') + end end end @@ -417,6 +427,86 @@ RSpec.describe 'layouts/nav/sidebar/_project' do end end + describe 'Deployments' do + let(:page) { Nokogiri::HTML.parse(rendered) } + + describe 'Feature Flags' do + it 'has a link to the feature flags page' do + render + + expect(page.at_css('.shortcuts-deployments').parent.css('[aria-label="Feature Flags"]')).not_to be_empty + expect(rendered).to have_link('Feature Flags', href: project_feature_flags_path(project)) + end + + describe 'when the user does not have access' do + let(:user) { nil } + + it 'does not have a link to the feature flags page' do + render + + expect(rendered).not_to have_link('Feature Flags') + end + end + + context 'when feature flag :sidebar_refactor is disabled' do + it 'does not have a Feature Flags menu item' do + stub_feature_flags(sidebar_refactor: false) + + render + + expect(rendered).not_to have_selector('.shortcuts-deployments') + end + end + end + + describe 'Environments' do + it 'has a link to the environments page' do + render + + expect(page.at_css('.shortcuts-deployments').parent.css('[aria-label="Environments"]')).not_to be_empty + expect(rendered).to have_link('Environments', href: project_environments_path(project)) + end + + describe 'when the user does not have access' do + let(:user) { nil } + + it 'does not have a link to the environments page' do + render + + expect(rendered).not_to have_link('Environments') + end + end + + context 'when feature flag :sidebar_refactor is disabled' do + it 'does not have a Environments menu item' do + stub_feature_flags(sidebar_refactor: false) + + render + + expect(rendered).not_to have_selector('.shortcuts-deployments') + end + end + end + + describe 'Releases' do + it 'has a link to the project releases path' do + render + + expect(rendered).to have_link('Releases', href: project_releases_path(project), class: 'shortcuts-deployments-releases') + end + + context 'when feature flag :sidebar refactor is disabled' do + it 'does not have a link to the project releases path' do + stub_feature_flags(sidebar_refactor: false) + + render + + expect(rendered).not_to have_link('Releases', href: project_releases_path(project), class: 'shortcuts-deployments-releases') + end + end + end + end + describe 'Operations' do it 'top level navigation link is visible for user with permissions' do render @@ -610,37 +700,67 @@ RSpec.describe 'layouts/nav/sidebar/_project' do end describe 'Environments' do - it 'has a link to the environments page' do + let(:page) { Nokogiri::HTML.parse(rendered) } + + it 'does not have a link to the environments page' do render - expect(rendered).to have_link('Environments', href: project_environments_path(project)) + expect(page.at_css('.shortcuts-operations').parent.css('[aria-label="Environments"]')).to be_empty end - describe 'when the user does not have access' do - let(:user) { nil } + context 'when feature flag :sidebar_refactor is disabled' do + before do + stub_feature_flags(sidebar_refactor: false) + end - it 'does not have a link to the environments page' do + it 'has a link to the environments page' do render - expect(rendered).not_to have_link('Environments') + expect(page.at_css('.shortcuts-operations').parent.css('[aria-label="Environments"]')).not_to be_empty + expect(rendered).to have_link('Environments', href: project_environments_path(project)) + end + + describe 'when the user does not have access' do + let(:user) { nil } + + it 'does not have a link to the environments page' do + render + + expect(rendered).not_to have_link('Environments') + end end end end describe 'Feature Flags' do - it 'has a link to the feature flags page' do + let(:page) { Nokogiri::HTML.parse(rendered) } + + it 'does not have a link to the feature flags page' do render - expect(rendered).to have_link('Feature Flags', href: project_feature_flags_path(project)) + expect(page.at_css('.shortcuts-operations').parent.css('[aria-label="Feature Flags"]')).to be_empty end - describe 'when the user does not have access' do - let(:user) { nil } + context 'when feature flag :sidebar_refactor is disabled' do + before do + stub_feature_flags(sidebar_refactor: false) + end - it 'does not have a link to the feature flags page' do + it 'has a link to the feature flags page' do render - expect(rendered).not_to have_link('Feature Flags') + expect(page.at_css('.shortcuts-operations').parent.css('[aria-label="Feature Flags"]')).not_to be_empty + expect(rendered).to have_link('Feature Flags', href: project_feature_flags_path(project)) + end + + describe 'when the user does not have access' do + let(:user) { nil } + + it 'does not have a link to the feature flags page' do + render + + expect(rendered).not_to have_link('Feature Flags') + end end end end diff --git a/spec/workers/packages/nuget/extraction_worker_spec.rb b/spec/workers/packages/nuget/extraction_worker_spec.rb index 3cc2c79176b..5186c037dc5 100644 --- a/spec/workers/packages/nuget/extraction_worker_spec.rb +++ b/spec/workers/packages/nuget/extraction_worker_spec.rb @@ -103,5 +103,14 @@ RSpec.describe Packages::Nuget::ExtractionWorker, type: :worker do it_behaves_like 'handling the metadata error' end end + + context 'handles a processing an unaccounted for error' do + before do + expect(::Packages::Nuget::UpdatePackageFromMetadataService).to receive(:new) + .and_raise(Zip::Error) + end + + it_behaves_like 'handling the metadata error', exception_class: Zip::Error + end end end diff --git a/spec/workers/packages/rubygems/extraction_worker_spec.rb b/spec/workers/packages/rubygems/extraction_worker_spec.rb index 6f65dceacf7..0e67f3ac62e 100644 --- a/spec/workers/packages/rubygems/extraction_worker_spec.rb +++ b/spec/workers/packages/rubygems/extraction_worker_spec.rb @@ -37,6 +37,20 @@ RSpec.describe Packages::Rubygems::ExtractionWorker, type: :worker do expect(package.reload).to be_error end + it 'handles processing an unaccounted for error', :aggregate_failures do + expect(::Packages::Rubygems::ProcessGemService).to receive(:new) + .and_raise(Zip::Error) + + expect(Gitlab::ErrorTracking).to receive(:log_exception).with( + instance_of(Zip::Error), + project_id: package.project_id + ) + + subject + + expect(package.reload).to be_error + end + context 'returns when there is no package file' do let(:package_file_id) { 999999 } -- cgit v1.2.3