diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 9f0d911f5ef34fe35ff81ff5a25b7217874dfa40..2e9229db56cc311ca0f133993013281a19387e87 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 @@ def unblock
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 @@ def check_impersonation_availability
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/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 998c697e6176e0f1d1acd6b585bef1f92e9ecd5a..c1d05c2d3cfb8f4c0d62019d06850b595a57a574 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -162,6 +162,49 @@ def user_block_effects
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 @@ def blocked_user_badge(user)
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/user.rb b/app/models/user.rb
index 4b38ef771a125ed19b5cd1cfb84a4280fa9de834..4ff6221bcab34ec0e2b5053f4ca2d71069520d64 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -326,6 +326,7 @@ def max_access_for_group
transition deactivated: :blocked
transition ldap_blocked: :blocked
transition blocked_pending_approval: :blocked
+ transition banned: :blocked
end
event :ldap_block do
@@ -338,19 +339,24 @@ def max_access_for_group
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 @@ def blocked?
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 @@ def filter_items(filter_name)
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/services/users/ban_service.rb b/app/services/users/ban_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..247ed14966bedb4583b58f90fbd34bdb06fc79b9
--- /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 0000000000000000000000000000000000000000..229c88adb7f7a4b7c44541b57a8c6f18ad119ad9
--- /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 ade3581e5b90c9069d1075e55ad01f7204b44459..be04e87f8b9cbe29fed52b23fafc8903047c6519 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 efa50ec47f5b678f10dff987447ae206818850a8..e4438f38a47577c566f2608cecd58c4831e2e06c 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 03ddf5c76a027559ca977d5050b5bed1fbc4526f..19cc29668f59e0ffae7d059b3bfe36466e7efc23 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/changelogs/unreleased/ban-user-state-ui.yml b/changelogs/unreleased/ban-user-state-ui.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6da3d575dc9a20dd1072b3923c442163dbfe9673
--- /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 0000000000000000000000000000000000000000..6765e82e25205bb6f78e3a5ebe9afe9c466e9477
--- /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 e7f851f7de4d76cbc9599946dbf26e647aba382d..2ba00e3bf665373fdc9178fff49df5a6006e3aeb 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -21,6 +21,8 @@
get :keys
put :block
put :unblock
+ put :ban
+ put :unban
put :deactivate
put :activate
put :unlock
diff --git a/doc/subscriptions/self_managed/index.md b/doc/subscriptions/self_managed/index.md
index db0d54ff14c3357e1ba3ffd5135177fb486bbca4..648ad0c70a56d2f97f8582d45ba932eaacaa0b15 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 144ee2dbf9873249e0124da2a120b18e40463ccc..cafc7caf981ea00597d28d2dac5e10c5b0b5010d 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 9141d7f488de2df5fbda71005a5c777838842c4b..2b3b90cb1a49ccace60249ab4c02aa499781624b 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 14a5b5085cb069943cba832c3fb5d87983445dc8..cafc7caf981ea00597d28d2dac5e10c5b0b5010d 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 01926f289375871e591f6e1a7837f8654684a1f8..60c1ecbd10f479b1db4d44eb564e34dcfe179d26 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 0000000000000000000000000000000000000000..c04003dd75f4b5befc2bc66d65a99a5ce2aec224
--- /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/profile/account/delete_account.md b/doc/user/profile/account/delete_account.md
index c0560269f00b0bdacb084f35c12135b8801fc0c9..361353a0f8c092dfaeced326fce8730bb88202e0 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 199c1a47e04d9b0ae90c1cb51925aabcd9cd1a70..7d2f2395815cb1e7a95d6635628c42c5465c1040 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/locale/gitlab.pot b/locale/gitlab.pot
index 2afaf90e74046c34c8b344a6de74603218ff47d4..b2c11642456e55354b4d5203ea7e3f4106f1fb80 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1373,6 +1373,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 ""
@@ -2435,6 +2438,9 @@ msgstr ""
msgid "AdminUsers|(Admin)"
msgstr ""
+msgid "AdminUsers|(Banned)"
+msgstr ""
+
msgid "AdminUsers|(Blocked)"
msgstr ""
@@ -2501,6 +2507,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 ""
@@ -2588,6 +2609,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 ""
@@ -2669,6 +2693,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 ""
@@ -2684,6 +2717,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 ""
@@ -2720,6 +2756,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 ""
@@ -2732,6 +2771,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 ""
@@ -12905,12 +12947,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 ""
@@ -31247,6 +31295,9 @@ msgstr ""
msgid "Successfully approved"
msgstr ""
+msgid "Successfully banned"
+msgstr ""
+
msgid "Successfully blocked"
msgstr ""
@@ -31271,6 +31322,9 @@ msgstr ""
msgid "Successfully synced %{synced_timeago}."
msgstr ""
+msgid "Successfully unbanned"
+msgstr ""
+
msgid "Successfully unblocked"
msgstr ""
@@ -33377,6 +33431,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/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 1afd20f5021e9de3c1572c255c10bfbae75ebd70..722c9c322cca93d105900a16fecc8b24936621e8 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -365,6 +365,56 @@
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/factories/users.rb b/spec/factories/users.rb
index 7ac44d55687ecfc86bb9aa5a4236c36ab2dc7ea5..476c57f2d80d0aef5b9d3f9bdb23cf1d63944cd8 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -27,6 +27,10 @@
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 36907d4aa603d71220bb28882ca67f4b80441844..d3931373ee3ba00f91f641a454ffeda8185bfe28 100644
--- a/spec/features/admin/users/users_spec.rb
+++ b/spec/features/admin/users/users_spec.rb
@@ -85,6 +85,7 @@
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/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
index 46073b37a3b023125fec497de447be1220fb6979..862fd58df04e733739cf12f4accb53d0b4a4e626 100644
--- a/spec/helpers/users_helper_spec.rb
+++ b/spec/helpers/users_helper_spec.rb
@@ -136,6 +136,16 @@ def filter_ee_badges(badges)
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/models/user_spec.rb b/spec/models/user_spec.rb
index 0c35b1265ba4f212daa1cd5bcc0125abe55ca764..cb34917f07352d1c8c881be56f4946aad5a7b925 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -728,6 +728,7 @@
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 @@
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 @@
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 @@
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/services/users/ban_service_spec.rb b/spec/services/users/ban_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0e6ac615da5f4046487774376a2c4dc6d2a71cab
--- /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