diff options
Diffstat (limited to 'app')
38 files changed, 360 insertions, 88 deletions
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue index 50f2136325d..3ecd911e814 100644 --- a/app/assets/javascripts/incidents/components/incidents_list.vue +++ b/app/assets/javascripts/incidents/components/incidents_list.vue @@ -39,6 +39,7 @@ import { DEFAULT_PAGE_SIZE, INCIDENT_STATUS_TABS, TH_CREATED_AT_TEST_ID, + TH_INCIDENT_SLA_TEST_ID, TH_SEVERITY_TEST_ID, TH_PUBLISHED_TEST_ID, INCIDENT_DETAILS_PATH, @@ -67,7 +68,7 @@ export default { { key: 'severity', label: s__('IncidentManagement|Severity'), - thClass, + thClass: `${thClass} w-15p`, tdClass: `${tdClass} sortable-cell`, sortable: true, thAttr: TH_SEVERITY_TEST_ID, @@ -75,23 +76,38 @@ export default { { key: 'title', label: s__('IncidentManagement|Incident'), - thClass: `gl-pointer-events-none gl-w-half`, + thClass: `gl-pointer-events-none`, tdClass, }, { key: 'createdAt', label: s__('IncidentManagement|Date created'), - thClass, + thClass: `${thClass} gl-w-eighth`, tdClass: `${tdClass} sortable-cell`, sortable: true, thAttr: TH_CREATED_AT_TEST_ID, }, { + key: 'incidentSla', + label: s__('IncidentManagement|Time to SLA'), + thClass: `gl-pointer-events-none gl-text-right gl-w-eighth`, + tdClass: `${tdClass} gl-text-right`, + thAttr: TH_INCIDENT_SLA_TEST_ID, + }, + { key: 'assignees', label: s__('IncidentManagement|Assignees'), - thClass: 'gl-pointer-events-none', + thClass: 'gl-pointer-events-none w-15p', tdClass, }, + { + key: 'published', + label: s__('IncidentManagement|Published'), + thClass: `${thClass} w-15p`, + tdClass: `${tdClass} sortable-cell`, + sortable: true, + thAttr: TH_PUBLISHED_TEST_ID, + }, ], components: { GlLoadingIcon, @@ -107,6 +123,8 @@ export default { GlTabs, GlTab, PublishedCell: () => import('ee_component/incidents/components/published_cell.vue'), + ServiceLevelAgreementCell: () => + import('ee_component/incidents/components/service_level_agreement_cell.vue'), GlBadge, GlEmptyState, SeverityToken, @@ -126,6 +144,7 @@ export default { 'textQuery', 'authorUsernamesQuery', 'assigneeUsernamesQuery', + 'slaFeatureAvailable', ], apollo: { incidents: { @@ -231,21 +250,12 @@ export default { ); }, availableFields() { - return this.publishedAvailable - ? [ - ...this.$options.fields, - ...[ - { - key: 'published', - label: s__('IncidentManagement|Published'), - thClass, - tdClass: `${tdClass} sortable-cell`, - sortable: true, - thAttr: TH_PUBLISHED_TEST_ID, - }, - ], - ] - : this.$options.fields; + const isHidden = { + published: !this.publishedAvailable, + incidentSla: !this.slaFeatureAvailable, + }; + + return this.$options.fields.filter(({ key }) => !isHidden[key]); }, isEmpty() { return !this.incidents.list?.length; @@ -526,6 +536,10 @@ export default { <time-ago-tooltip :time="item.createdAt" /> </template> + <template v-if="slaFeatureAvailable" #cell(incidentSla)="{ item }"> + <service-level-agreement-cell :sla-due-at="item.slaDueAt" data-testid="incident-sla" /> + </template> + <template #cell(assignees)="{ item }"> <div data-testid="incident-assignees"> <template v-if="hasAssignees(item.assignees)"> diff --git a/app/assets/javascripts/incidents/constants.js b/app/assets/javascripts/incidents/constants.js index bdabf1c3e42..4fccefb66c5 100644 --- a/app/assets/javascripts/incidents/constants.js +++ b/app/assets/javascripts/incidents/constants.js @@ -46,5 +46,6 @@ export const trackIncidentCreateNewOptions = { export const DEFAULT_PAGE_SIZE = 20; export const TH_CREATED_AT_TEST_ID = { 'data-testid': 'incident-management-created-at-sort' }; export const TH_SEVERITY_TEST_ID = { 'data-testid': 'incident-management-severity-sort' }; +export const TH_INCIDENT_SLA_TEST_ID = { 'data-testid': 'incident-management-sla' }; export const TH_PUBLISHED_TEST_ID = { 'data-testid': 'incident-management-published-sort' }; export const INCIDENT_DETAILS_PATH = 'incident'; diff --git a/app/assets/javascripts/incidents/list.js b/app/assets/javascripts/incidents/list.js index aeec4a258b9..15af7432436 100644 --- a/app/assets/javascripts/incidents/list.js +++ b/app/assets/javascripts/incidents/list.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; +import { parseBoolean } from '~/lib/utils/common_utils'; import IncidentsList from './components/incidents_list.vue'; Vue.use(VueApollo); @@ -19,6 +20,7 @@ export default () => { textQuery, authorUsernamesQuery, assigneeUsernamesQuery, + slaFeatureAvailable, } = domEl.dataset; const apolloProvider = new VueApollo({ @@ -33,11 +35,12 @@ export default () => { incidentType, newIssuePath, issuePath, - publishedAvailable, + publishedAvailable: parseBoolean(publishedAvailable), emptyListSvgPath, textQuery, authorUsernamesQuery, assigneeUsernamesQuery, + slaFeatureAvailable: parseBoolean(slaFeatureAvailable), }, apolloProvider, components: { diff --git a/app/assets/javascripts/pages/admin/credentials/index.js b/app/assets/javascripts/pages/admin/credentials/index.js new file mode 100644 index 00000000000..868c8e33077 --- /dev/null +++ b/app/assets/javascripts/pages/admin/credentials/index.js @@ -0,0 +1,3 @@ +import initConfirmModal from '~/confirm_modal'; + +initConfirmModal(); diff --git a/app/assets/javascripts/pages/groups/security/credentials/index.js b/app/assets/javascripts/pages/groups/security/credentials/index.js new file mode 100644 index 00000000000..868c8e33077 --- /dev/null +++ b/app/assets/javascripts/pages/groups/security/credentials/index.js @@ -0,0 +1,3 @@ +import initConfirmModal from '~/confirm_modal'; + +initConfirmModal(); diff --git a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue index 62b7e02c52a..1af1bc18e3e 100644 --- a/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue +++ b/app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue @@ -77,7 +77,7 @@ export default { <template> <labels-select class="block labels js-labels-block" - :allow-label-remove="true" + :allow-label-remove="allowLabelEdit" :allow-label-create="allowLabelCreate" :allow-label-edit="allowLabelEdit" :allow-multiselect="true" diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 55057ba8144..d280f158c14 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -6,6 +6,7 @@ class Admin::UsersController < Admin::ApplicationController before_action :user, except: [:index, :new, :create] before_action :check_impersonation_availability, only: :impersonate before_action :ensure_destroy_prerequisites_met, only: [:destroy] + before_action :check_admin_approval_feature_available!, only: [:approve] feature_category :users @@ -62,6 +63,16 @@ class Admin::UsersController < Admin::ApplicationController end end + def approve + result = Users::ApproveService.new(current_user).execute(user) + + if result[:status] == :success + redirect_back_or_admin_user(notice: _("Successfully approved")) + else + redirect_back_or_admin_user(alert: result[:message]) + end + end + def activate return redirect_back_or_admin_user(notice: _("Error occurred. A blocked user must be unblocked to be activated")) if user.blocked? @@ -82,7 +93,7 @@ class Admin::UsersController < Admin::ApplicationController def block result = Users::BlockService.new(current_user).execute(user) - if result[:status] = :success + if result[:status] == :success redirect_back_or_admin_user(notice: _("Successfully blocked")) else redirect_back_or_admin_user(alert: _("Error occurred. User was not blocked")) @@ -287,6 +298,10 @@ class Admin::UsersController < Admin::ApplicationController 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 + + def check_admin_approval_feature_available! + access_denied! unless Feature.enabled?(:admin_approval_for_new_user_signups) + end end Admin::UsersController.prepend_if_ee('EE::Admin::UsersController') diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 0687c057d47..c85c83688a4 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -2,6 +2,7 @@ class ProfilesController < Profiles::ApplicationController include ActionView::Helpers::SanitizeHelper + include Gitlab::Tracking before_action :user before_action :authorize_change_username!, only: :update_username @@ -65,6 +66,8 @@ class ProfilesController < Profiles::ApplicationController @events = AuditEvent.where(entity_type: "User", entity_id: current_user.id) .order("created_at DESC") .page(params[:page]) + + Gitlab::Tracking.event(self.class.name, 'search_audit_event') end # rubocop: enable CodeReuse/ActiveRecord diff --git a/app/finders/keys_finder.rb b/app/finders/keys_finder.rb index e7e78d71a58..9c357e12205 100644 --- a/app/finders/keys_finder.rb +++ b/app/finders/keys_finder.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true class KeysFinder + delegate :find, :find_by_id, to: :execute + InvalidFingerprint = Class.new(StandardError) GitLabAccessDeniedError = Class.new(StandardError) diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb index 44d869fbd8f..5a42e581867 100644 --- a/app/helpers/profiles_helper.rb +++ b/app/helpers/profiles_helper.rb @@ -29,19 +29,4 @@ module ProfilesHelper def user_profile? params[:controller] == 'users' end - - def ssh_key_delete_modal_data(key, is_admin) - { - path: path_to_key(key, is_admin), - method: 'delete', - qa_selector: 'delete_ssh_key_button', - modal_attributes: { - 'data-qa-selector': 'ssh_key_delete_modal', - title: _('Are you sure you want to delete this SSH key?'), - message: _('This action cannot be undone, and will permanently delete the %{key} SSH key') % { key: key.title }, - okVariant: 'danger', - okTitle: _('Delete') - } - } - end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 63601485daf..a032b1b2bba 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -100,7 +100,7 @@ module SearchHelper end def search_service - @search_service ||= ::SearchService.new(current_user, params) + @search_service ||= ::SearchService.new(current_user, params.merge(confidential: Gitlab::Utils.to_boolean(params[:confidential]))) end private diff --git a/app/helpers/ssh_keys_helper.rb b/app/helpers/ssh_keys_helper.rb new file mode 100644 index 00000000000..381db893943 --- /dev/null +++ b/app/helpers/ssh_keys_helper.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module SshKeysHelper + def ssh_key_delete_modal_data(key, path) + { + path: path, + method: 'delete', + qa_selector: 'delete_ssh_key_button', + modal_attributes: { + 'data-qa-selector': 'ssh_key_delete_modal', + title: _('Are you sure you want to delete this SSH key?'), + message: _('This action cannot be undone, and will permanently delete the %{key} SSH key') % { key: key.title }, + okVariant: 'danger', + okTitle: _('Delete') + } + } + end +end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index c1bca6b4c41..f47937e6d57 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -84,7 +84,7 @@ module UsersHelper def user_badges_in_admin_section(user) [].tap do |badges| - badges << { text: s_('AdminUsers|Blocked'), variant: 'danger' } if user.blocked? + badges << blocked_user_badge(user) if user.blocked? badges << { text: s_('AdminUsers|Admin'), variant: 'success' } if user.admin? badges << { text: s_('AdminUsers|External'), variant: 'secondary' } if user.external? badges << { text: s_("AdminUsers|It's you!"), variant: nil } if current_user == user @@ -106,8 +106,19 @@ module UsersHelper end end + def can_force_email_confirmation?(user) + !user.confirmed? + end + private + def blocked_user_badge(user) + pending_approval_badge = { text: s_('AdminUsers|Pending approval'), variant: 'info' } + return pending_approval_badge if user.blocked_pending_approval? + + { text: s_('AdminUsers|Blocked'), variant: 'danger' } + end + def get_profile_tabs tabs = [] diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index e6e5e671fe8..8a7bd5a7ad9 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -120,6 +120,7 @@ module ApplicationSettingImplementation repository_checks_enabled: true, repository_storages_weighted: { default: 100 }, repository_storages: ['default'], + require_admin_approval_after_user_signup: false, require_two_factor_authentication: false, restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], rsa_key_restriction: 0, diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb index eccf21551f0..978a54bdee7 100644 --- a/app/models/concerns/has_repository.rb +++ b/app/models/concerns/has_repository.rb @@ -84,7 +84,11 @@ module HasRepository end def default_branch_from_preferences - empty_repo? ? Gitlab::CurrentSettings.default_branch_name : nil + return unless empty_repo? + + group_branch_default_name = group&.default_branch_name if respond_to?(:group) + + group_branch_default_name || Gitlab::CurrentSettings.default_branch_name end def reload_default_branch diff --git a/app/models/group.rb b/app/models/group.rb index 5a18441e0ad..5d0934899d1 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -552,6 +552,10 @@ class Group < Namespace owners.first || parent&.default_owner || owner end + def default_branch_name + namespace_settings&.default_branch_name + end + def access_level_roles GroupMember.access_level_roles end diff --git a/app/models/user.rb b/app/models/user.rb index ae8da5726d6..8db449116eb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -293,6 +293,7 @@ class User < ApplicationRecord transition active: :blocked transition deactivated: :blocked transition ldap_blocked: :blocked + transition blocked_pending_approval: :blocked end event :ldap_block do @@ -338,7 +339,8 @@ class User < ApplicationRecord # Scopes scope :admins, -> { where(admin: true) } - scope :blocked, -> { with_states(:blocked, :ldap_blocked, :blocked_pending_approval) } + scope :blocked, -> { with_states(:blocked, :ldap_blocked) } + scope :blocked_pending_approval, -> { with_states(:blocked_pending_approval) } scope :external, -> { where(external: true) } scope :confirmed, -> { where.not(confirmed_at: nil) } scope :active, -> { with_state(:active).non_internal } @@ -538,6 +540,8 @@ class User < ApplicationRecord admins when 'blocked' blocked + when 'blocked_pending_approval' + blocked_pending_approval when 'two_factor_disabled' without_two_factor when 'two_factor_enabled' diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index de69636b078..c1ea4dddb51 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -98,6 +98,7 @@ class GlobalPolicy < BasePolicy rule { admin }.policy do enable :read_custom_attribute enable :update_custom_attribute + enable :approve_user end # We can't use `read_statistics` because the user may have different permissions for different projects diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb index dc7c4654208..a37011d0100 100644 --- a/app/serializers/deployment_entity.rb +++ b/app/serializers/deployment_entity.rb @@ -17,6 +17,7 @@ class DeploymentEntity < Grape::Entity end end + expose :status expose :created_at expose :deployed_at expose :tag diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb index 70f5c7e2ea7..aad574aeaf5 100644 --- a/app/services/groups/transfer_service.rb +++ b/app/services/groups/transfer_service.rb @@ -38,6 +38,7 @@ module Groups # Overridden in EE def post_update_hooks(updated_project_ids) refresh_project_authorizations + refresh_descendant_groups if @new_parent_group end def ensure_allowed_transfer @@ -101,6 +102,8 @@ module Groups @group.visibility_level = @new_parent_group.visibility_level end + update_two_factor_authentication if @new_parent_group + @group.parent = @new_parent_group @group.clear_memoization(:self_and_ancestors_ids) @@ -129,8 +132,26 @@ module Groups projects_to_update .update_all(visibility_level: @new_parent_group.visibility_level) end + + def update_two_factor_authentication + return if namespace_parent_allows_two_factor_auth + + @group.require_two_factor_authentication = false + end + + def refresh_descendant_groups + return if namespace_parent_allows_two_factor_auth + + if @group.descendants.where(require_two_factor_authentication: true).any? + DisallowTwoFactorForSubgroupsWorker.perform_async(@group.id) + end + end # rubocop: enable CodeReuse/ActiveRecord + def namespace_parent_allows_two_factor_auth + @new_parent_group.namespace_settings.allow_mfa_for_subgroups + end + def ensure_ownership return if @new_parent_group return unless @group.owners.empty? diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index 08d113bc0f3..d5fe89cfd59 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -4,6 +4,8 @@ module Groups class UpdateService < Groups::BaseService include UpdateVisibilityLevel + SETTINGS_PARAMS = [:allow_mfa_for_subgroups].freeze + def execute reject_parent_id! remove_unallowed_params @@ -21,6 +23,8 @@ module Groups return false unless update_shared_runners + handle_changes + before_assignment_hook(group, params) handle_namespace_settings @@ -89,6 +93,19 @@ module Groups # don't enqueue immediately to prevent todos removal in case of a mistake TodosDestroyer::GroupPrivateWorker.perform_in(Todo::WAIT_FOR_DELETE, group.id) end + + update_two_factor_requirement_for_subgroups + end + + def update_two_factor_requirement_for_subgroups + settings = group.namespace_settings + + return if settings.allow_mfa_for_subgroups + + if settings.previous_changes.include?(:allow_mfa_for_subgroups) + # enque in batches members update + DisallowTwoFactorForSubgroupsWorker.perform_async(group.id) + end end def reject_parent_id! @@ -101,6 +118,21 @@ module Groups params.delete(:default_branch_protection) unless can?(current_user, :update_default_branch_protection, group) end + def handle_changes + handle_settings_update + end + + def handle_settings_update + settings_params = params.slice(*allowed_settings_params) + allowed_settings_params.each { |param| params.delete(param) } + + ::NamespaceSettings::UpdateService.new(current_user, group, settings_params).execute + end + + def allowed_settings_params + SETTINGS_PARAMS + end + def valid_share_with_group_lock_change? return true unless changing_share_with_group_lock? return true if can?(current_user, :change_share_with_group_lock, group) diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 6fc8e8f8935..8f18a23aa0f 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -147,7 +147,7 @@ module Projects def create_readme commit_attrs = { - branch_name: Gitlab::CurrentSettings.default_branch_name.presence || 'master', + branch_name: @project.default_branch || 'master', commit_message: 'Initial commit', file_path: 'README.md', file_content: "# #{@project.name}\n\n#{@project.description}" @@ -165,10 +165,9 @@ module Projects @project.create_or_update_import_data(data: @import_data[:data], credentials: @import_data[:credentials]) if @import_data if @project.save - unless @project.gitlab_project_import? - Service.create_from_active_default_integrations(@project, :project_id, with_templates: true) - @project.create_labels - end + Service.create_from_active_default_integrations(@project, :project_id, with_templates: true) + + @project.create_labels unless @project.gitlab_project_import? unless @project.import? raise 'Failed to create repository' unless @project.create_repository diff --git a/app/services/users/approve_service.rb b/app/services/users/approve_service.rb new file mode 100644 index 00000000000..228cfbd6947 --- /dev/null +++ b/app/services/users/approve_service.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Users + class ApproveService < BaseService + def initialize(current_user) + @current_user = current_user + end + + def execute(user) + return error(_('You are not allowed to approve a user')) unless allowed? + return error(_('The user you are trying to approve is not pending an approval')) unless approval_required?(user) + + if user.activate + # Resends confirmation email if the user isn't confirmed yet. + # Please see Devise's implementation of `resend_confirmation_instructions` for detail. + user.resend_confirmation_instructions + user.accept_pending_invitations! if user.active_for_authentication? + + success + else + error(user.errors.full_messages.uniq.join('. ')) + end + end + + private + + attr_reader :current_user + + def allowed? + can?(current_user, :approve_user) + end + + def approval_required?(user) + user.blocked_pending_approval? + end + end +end diff --git a/app/views/admin/users/_approve_user.html.haml b/app/views/admin/users/_approve_user.html.haml new file mode 100644 index 00000000000..b4d960d909c --- /dev/null +++ b/app/views/admin/users/_approve_user.html.haml @@ -0,0 +1,7 @@ +.card.border-info + .card-header.gl-bg-blue-500.gl-text-white + = s_('AdminUsers|This user has requested access') + .card-body + = render partial: 'admin/users/user_approve_effects' + %br + = link_to s_('AdminUsers|Approve user'), approve_admin_user_path(user), method: :put, class: "btn gl-button btn-info", data: { confirm: s_('AdminUsers|Are you sure?') } diff --git a/app/views/admin/users/_block_user.html.haml b/app/views/admin/users/_block_user.html.haml new file mode 100644 index 00000000000..b07a72c3e28 --- /dev/null +++ b/app/views/admin/users/_block_user.html.haml @@ -0,0 +1,11 @@ +.card.border-warning + .card-header.bg-warning.text-white + = s_('AdminUsers|Block this user') + .card-body + = render partial: 'admin/users/user_block_effects' + %br + %button.btn.gl-button.btn-warning{ data: { 'gl-modal-action': 'block', + content: s_('AdminUsers|You can always unblock their account, their data will remain intact.'), + url: block_admin_user_path(user), + username: sanitize_name(user.name) } } + = s_('AdminUsers|Block user') diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index a60dbd51935..4abcdef7e27 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -1,13 +1,20 @@ %h3.page-title = @user.name - - if @user.blocked? - %span.cred (Blocked) + - if @user.blocked_pending_approval? + %span.cred + = s_('AdminUsers|(Pending approval)') + - elsif @user.blocked? + %span.cred + = s_('AdminUsers|(Blocked)') - if @user.internal? - %span.cred (Internal) + %span.cred + = s_('AdminUsers|(Internal)') - if @user.admin - %span.cred (Admin) + %span.cred + = s_('AdminUsers|(Admin)') - if @user.deactivated? - %span.cred (Deactivated) + %span.cred + = s_('AdminUsers|(Deactivated)') = render_if_exists 'admin/users/audtior_user_badge' .float-right diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml index 284307d1d54..70ab95bfa61 100644 --- a/app/views/admin/users/_user.html.haml +++ b/app/views/admin/users/_user.html.haml @@ -35,15 +35,22 @@ %span.small = s_('AdminUsers|Cannot unblock LDAP blocked users') - elsif user.blocked? - = link_to _('Unblock'), unblock_admin_user_path(user), method: :put + - if user.blocked_pending_approval? + = link_to s_('AdminUsers|Approve'), approve_admin_user_path(user), method: :put + %button.btn.btn-default-tertiary{ data: { 'gl-modal-action': 'block', + url: block_admin_user_path(user), + username: sanitize_name(user.name) } } + = s_('AdminUsers|Block') + - else + = link_to _('Unblock'), unblock_admin_user_path(user), method: :put - else - %button.btn.gl-button.btn-default-tertiary{ data: { 'gl-modal-action': 'block', + %button.btn.btn-default-tertiary{ data: { 'gl-modal-action': 'block', url: block_admin_user_path(user), username: sanitize_name(user.name) } } = s_('AdminUsers|Block') - if user.can_be_deactivated? %li - %button.btn.gl-button.btn-default-tertiary{ data: { 'gl-modal-action': 'deactivate', + %button.btn.btn-default-tertiary{ data: { 'gl-modal-action': 'deactivate', url: deactivate_admin_user_path(user), username: sanitize_name(user.name) } } = s_('AdminUsers|Deactivate') @@ -57,13 +64,13 @@ %li.divider - if user.can_be_removed? %li - %button.delete-user-button.btn.gl-button.btn-default-tertiary.text-danger{ data: { 'gl-modal-action': 'delete', + %button.delete-user-button.btn.btn-default-tertiary.text-danger{ data: { 'gl-modal-action': 'delete', delete_user_url: admin_user_path(user), block_user_url: block_admin_user_path(user), username: sanitize_name(user.name) } } = s_('AdminUsers|Delete user') %li - %button.delete-user-button.btn.gl-button.btn-default-tertiary.text-danger{ data: { 'gl-modal-action': 'delete-with-contributions', + %button.delete-user-button.btn.btn-default-tertiary.text-danger{ data: { 'gl-modal-action': 'delete-with-contributions', delete_user_url: admin_user_path(user, hard_delete: true), block_user_url: block_admin_user_path(user), username: sanitize_name(user.name) } } diff --git a/app/views/admin/users/_user_approve_effects.html.haml b/app/views/admin/users/_user_approve_effects.html.haml new file mode 100644 index 00000000000..54e51bf3467 --- /dev/null +++ b/app/views/admin/users/_user_approve_effects.html.haml @@ -0,0 +1,11 @@ +%p + = s_('AdminUsers|Approved users can:') +%ul + %li + = s_('AdminUsers|Log in') + %li + = s_('AdminUsers|Access Git repositories') + %li + = s_('AdminUsers|Access the API') + %li + = s_('AdminUsers|Be added to groups and projects') diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index cf6f9248461..2a5a97becaf 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -30,6 +30,11 @@ = link_to admin_users_path(filter: "blocked") do = s_('AdminUsers|Blocked') %small.badge.badge-pill= limited_counter_with_delimiter(User.blocked) + - if Feature.enabled?(:admin_approval_for_new_user_signups) + = 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") do + = s_('AdminUsers|Pending approval') + %small.badge.badge-pill= limited_counter_with_delimiter(User.blocked_pending_approval) = nav_link(html_options: { class: active_when(params[:filter] == 'deactivated') }) do = link_to admin_users_path(filter: "deactivated") do = s_('AdminUsers|Deactivated') diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index 0ae4d987aab..9c6f151a6b1 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -137,7 +137,7 @@ .col-md-6 - unless @user == current_user - - unless @user.confirmed? + - if can_force_email_confirmation?(@user) .card.border-info .card-header.bg-info.text-white Confirm user @@ -173,28 +173,23 @@ = s_('AdminUsers|Deactivate user') - if @user.blocked? - .card.border-info - .card-header.bg-info.text-white - This user is blocked - .card-body - %p A blocked user cannot: - %ul - %li Log in - %li Access Git repositories - %br - = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: 'Are you sure?' } + - if @user.blocked_pending_approval? + = render 'admin/users/approve_user', user: @user + = render 'admin/users/block_user', user: @user + - else + .card.border-info + .card-header.gl-bg-blue-500.gl-text-white + This user is blocked + .card-body + %p A blocked user cannot: + %ul + %li Log in + %li Access Git repositories + %br + = link_to 'Unblock user', unblock_admin_user_path(@user), method: :put, class: "btn gl-button btn-info", data: { confirm: s_('AdminUsers|Are you sure?') } - elsif !@user.internal? - .card.border-warning - .card-header.bg-warning.text-white - Block this user - .card-body - = render partial: 'admin/users/user_block_effects' - %br - %button.btn.gl-button.btn-warning{ data: { 'gl-modal-action': 'block', - content: 'You can always unblock their account, their data will remain intact.', - url: block_admin_user_path(@user), - username: sanitize_name(@user.name) } } - = s_('AdminUsers|Block user') + = render 'admin/users/block_user', user: @user + - if @user.access_locked? .card.border-info .card-header.bg-info.text-white diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml index 3f0c1596396..eaf00ce6709 100644 --- a/app/views/profiles/keys/_key.html.haml +++ b/app/views/profiles/keys/_key.html.haml @@ -27,6 +27,6 @@ = s_('Profiles|Created%{time_ago}'.html_safe) % { time_ago: time_ago_with_tooltip(key.created_at, html_class: 'gl-ml-2')} - if key.can_delete? .gl-ml-3 - = button_to '#', class: "btn btn-default gl-button btn-default-tertiary js-confirm-modal-button", data: ssh_key_delete_modal_data(key, is_admin) do + = button_to '#', class: "btn btn-default gl-button btn-default-tertiary js-confirm-modal-button", data: ssh_key_delete_modal_data(key, path_to_key(key, is_admin)) do %span.sr-only= _('Delete') = sprite_icon('remove') diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index 2bc7e9eccb8..22d795ca831 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -38,4 +38,4 @@ .col-md-12 .float-right - if @key.can_delete? - = button_to _('Delete'), '#', class: "btn btn-danger gl-button delete-key js-confirm-modal-button", data: ssh_key_delete_modal_data(@key, is_admin) + = button_to _('Delete'), '#', class: "btn btn-danger gl-button delete-key js-confirm-modal-button", data: ssh_key_delete_modal_data(@key, path_to_key(@key, is_admin)) diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index da684c29372..9c5cfe35cda 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -3,7 +3,7 @@ %div - if @user.errors.any? - .alert.alert-danger + .gl-alert.gl-alert-danger %ul - @user.errors.full_messages.each do |msg| %li= msg diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 3d929fe637f..c6d39f5bba0 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -1,5 +1,5 @@ - @content_class = "limit-container-width" unless fluid_layout -- default_branch_name = Gitlab::CurrentSettings.default_branch_name.presence || "master" +- default_branch_name = @project.default_branch || "master" - breadcrumb_title _("Details") - page_title _("Details") diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml index 3eb27f002ef..f21ec45eefb 100644 --- a/app/views/shared/_issuable_meta_data.html.haml +++ b/app/views/shared/_issuable_meta_data.html.haml @@ -5,21 +5,23 @@ - issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count - if issuable_mr > 0 - %li.issuable-mr.d-none.d-sm-block.has-tooltip{ title: _('Related merge requests') } + %li.issuable-mr.gl-display-none.gl-display-sm-block.has-tooltip{ title: _('Related merge requests') } = image_tag('icon-merge-request-unmerged.svg', class: 'icon-merge-request-unmerged') = issuable_mr - if upvotes > 0 - %li.issuable-upvotes.d-none.d-sm-block.has-tooltip{ title: _('Upvotes') } - = sprite_icon('thumb-up', css_class: "vertical-align-middle") + %li.issuable-upvotes.gl-display-none.gl-display-sm-block.has-tooltip{ title: _('Upvotes') } + = sprite_icon('thumb-up', css_class: "gl-vertical-align-middle") = upvotes - if downvotes > 0 - %li.issuable-downvotes.d-none.d-sm-block.has-tooltip{ title: _('Downvotes') } - = sprite_icon('thumb-down', css_class: "vertical-align-middle") + %li.issuable-downvotes.gl-display-none.gl-display-sm-block.has-tooltip{ title: _('Downvotes') } + = sprite_icon('thumb-down', css_class: "gl-vertical-align-middle") = downvotes -%li.issuable-comments.d-none.d-sm-block += render_if_exists 'shared/issuable/blocking_issues_count', issuable: issuable + +%li.issuable-comments.gl-display-none.gl-display-sm-block = link_to issuable_path, class: ['has-tooltip', ('no-comments' if note_count == 0)], title: _('Comments') do = sprite_icon('comments', css_class: 'gl-vertical-align-text-bottom') = note_count diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 1e2cded0618..cf27f4d9c27 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -1409,6 +1409,22 @@ :weight: 1 :idempotent: :tags: [] +- :name: disallow_two_factor_for_group + :feature_category: :subgroups + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] +- :name: disallow_two_factor_for_subgroups + :feature_category: :subgroups + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: email_receiver :feature_category: :issue_tracking :has_external_dependencies: diff --git a/app/workers/disallow_two_factor_for_group_worker.rb b/app/workers/disallow_two_factor_for_group_worker.rb new file mode 100644 index 00000000000..b3cc7a44672 --- /dev/null +++ b/app/workers/disallow_two_factor_for_group_worker.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class DisallowTwoFactorForGroupWorker + include ApplicationWorker + include ExceptionBacktrace + + feature_category :subgroups + idempotent! + + def perform(group_id) + begin + group = Group.find(group_id) + rescue ActiveRecord::RecordNotFound + return + end + + group.update!(require_two_factor_authentication: false) + end +end diff --git a/app/workers/disallow_two_factor_for_subgroups_worker.rb b/app/workers/disallow_two_factor_for_subgroups_worker.rb new file mode 100644 index 00000000000..1ca227030e2 --- /dev/null +++ b/app/workers/disallow_two_factor_for_subgroups_worker.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class DisallowTwoFactorForSubgroupsWorker + include ApplicationWorker + include ExceptionBacktrace + + INTERVAL = 2.seconds.to_i + + feature_category :subgroups + idempotent! + + def perform(group_id) + begin + group = Group.find(group_id) + rescue ActiveRecord::RecordNotFound + return + end + + # rubocop: disable CodeReuse/ActiveRecord + subgroups = group.descendants.where(require_two_factor_authentication: true) # rubocop: disable CodeReuse/ActiveRecord + subgroups.find_each(batch_size: 100).with_index do |subgroup, index| + delay = index * INTERVAL + + with_context(namespace: subgroup) do + DisallowTwoFactorForGroupWorker.perform_in(delay, subgroup.id) + end + end + # rubocop: enable CodeReuse/ActiveRecord + end +end |