Commit 308146dc authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 4b28d5ae
<script>
import _ from 'underscore';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import { GlModal, GlButton, GlFormInput } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
export default {
components: {
DeprecatedModal,
GlModal,
GlButton,
GlFormInput,
},
props: {
title: {
type: String,
required: true,
},
content: {
type: String,
required: true,
},
action: {
type: String,
required: true,
},
secondaryAction: {
type: String,
required: true,
},
deleteUserUrl: {
type: String,
required: false,
default: '',
required: true,
},
blockUserUrl: {
type: String,
required: false,
default: '',
},
deleteContributions: {
type: Boolean,
required: false,
default: false,
required: true,
},
username: {
type: String,
required: false,
default: '',
required: true,
},
csrfToken: {
type: String,
required: false,
default: '',
required: true,
},
},
data() {
......@@ -40,32 +49,12 @@ export default {
};
},
computed: {
title() {
const keepContributionsTitle = s__('AdminUsers|Delete User %{username}?');
const deleteContributionsTitle = s__('AdminUsers|Delete User %{username} and contributions?');
return sprintf(
this.deleteContributions ? deleteContributionsTitle : keepContributionsTitle,
{
username: `'${_.escape(this.username)}'`,
},
false,
);
modalTitle() {
return sprintf(this.title, { username: this.username });
},
text() {
const keepContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}.
Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user".
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
const deleteContributionsText = s__(`AdminArea|
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 %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
return sprintf(
this.deleteContributions ? deleteContributionsText : keepContributionsText,
this.content,
{
username: `<strong>${_.escape(this.username)}</strong>`,
strong_start: '<strong>',
......@@ -83,12 +72,7 @@ export default {
false,
);
},
primaryButtonLabel() {
const keepContributionsLabel = s__('AdminUsers|Delete user');
const deleteContributionsLabel = s__('AdminUsers|Delete user and contributions');
return this.deleteContributions ? deleteContributionsLabel : keepContributionsLabel;
},
secondaryButtonLabel() {
return s__('AdminUsers|Block user');
},
......@@ -97,8 +81,12 @@ export default {
},
},
methods: {
show() {
this.$refs.modal.show();
},
onCancel() {
this.enteredUsername = '';
this.$refs.modal.hide();
},
onSecondaryAction() {
const { form } = this.$refs;
......@@ -117,43 +105,28 @@ export default {
</script>
<template>
<deprecated-modal
id="delete-user-modal"
:title="title"
:text="text"
:primary-button-label="primaryButtonLabel"
:secondary-button-label="secondaryButtonLabel"
:submit-disabled="!canSubmit"
kind="danger"
@submit="onSubmit"
@cancel="onCancel"
>
<template slot="body" slot-scope="props">
<p v-html="props.text"></p>
<gl-modal ref="modal" modal-id="delete-user-modal" :title="modalTitle" kind="danger">
<template>
<p v-html="text"></p>
<p v-html="confirmationTextLabel"></p>
<form ref="form" :action="deleteUserUrl" method="post">
<input ref="method" type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
<input
<gl-form-input
v-model="enteredUsername"
autofocus
type="text"
name="username"
class="form-control"
aria-labelledby="input-label"
autocomplete="off"
/>
</form>
</template>
<template slot="secondary-button">
<button
:disabled="!canSubmit"
type="button"
class="btn js-secondary-button btn-warning"
data-dismiss="modal"
@click="onSecondaryAction"
>
{{ secondaryButtonLabel }}
</button>
<template slot="modal-footer">
<gl-button variant="secondary" @click="onCancel">{{ s__('Cancel') }}</gl-button>
<gl-button :disabled="!canSubmit" variant="warning" @click="onSecondaryAction">
{{ secondaryAction }}
</gl-button>
<gl-button :disabled="!canSubmit" variant="danger" @click="onSubmit">{{ action }}</gl-button>
</template>
</deprecated-modal>
</gl-modal>
</template>
<script>
export default {
props: {
modalConfiguration: {
required: true,
type: Object,
},
actionModals: {
required: true,
type: Object,
},
csrfToken: {
required: true,
type: String,
},
},
data() {
return {
currentModalData: null,
};
},
computed: {
activeModal() {
if (!this.currentModalData) return null;
const { glModalAction: action } = this.currentModalData;
return this.actionModals[action];
},
modalProps() {
const { glModalAction: requestedAction } = this.currentModalData;
return {
...this.modalConfiguration[requestedAction],
...this.currentModalData,
csrfToken: this.csrfToken,
};
},
},
mounted() {
document.addEventListener('click', this.handleClick);
},
beforeDestroy() {
document.removeEventListener('click', this.handleClick);
},
methods: {
handleClick(e) {
const { glModalAction: action } = e.target.dataset;
if (!action) return;
this.show(e.target.dataset);
e.preventDefault();
},
show(modalData) {
const { glModalAction: requestedAction } = modalData;
if (!this.actionModals[requestedAction]) {
throw new Error(`Requested non-existing modal action ${requestedAction}`);
}
if (!this.modalConfiguration[requestedAction]) {
throw new Error(`Modal action ${requestedAction} has no configuration in HTML`);
}
this.currentModalData = modalData;
return this.$nextTick().then(() => {
this.$refs.modal.show();
});
},
},
};
</script>
<template>
<div :is="activeModal" v-if="activeModal" ref="modal" v-bind="modalProps" />
</template>
<script>
import { GlModal } from '@gitlab/ui';
import { sprintf } from '~/locale';
export default {
components: {
GlModal,
},
props: {
title: {
type: String,
required: true,
},
content: {
type: String,
required: true,
},
action: {
type: String,
required: true,
},
url: {
type: String,
required: true,
},
username: {
type: String,
required: true,
},
csrfToken: {
type: String,
required: true,
},
method: {
type: String,
required: false,
default: 'put',
},
},
computed: {
modalTitle() {
return sprintf(this.title, { username: this.username });
},
},
methods: {
show() {
this.$refs.modal.show();
},
submit() {
this.$refs.form.submit();
},
},
};
</script>
<template>
<gl-modal
ref="modal"
modal-id="user-operation-modal"
:title="modalTitle"
ok-variant="warning"
:ok-title="action"
@ok="submit"
>
<form ref="form" :action="url" method="post">
<span v-html="content"></span>
<input ref="method" type="hidden" name="_method" :value="method" />
<input :value="csrfToken" type="hidden" name="authenticity_token" />
</form>
</gl-modal>
</template>
import $ from 'jquery';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import ModalManager from './components/user_modal_manager.vue';
import DeleteUserModal from './components/delete_user_modal.vue';
import UserOperationConfirmationModal from './components/user_operation_confirmation_modal.vue';
import csrf from '~/lib/utils/csrf';
import deleteUserModal from './components/delete_user_modal.vue';
const MODAL_TEXTS_CONTAINER_SELECTOR = '#modal-texts';
const MODAL_MANAGER_SELECTOR = '#user-modal';
const ACTION_MODALS = {
deactivate: UserOperationConfirmationModal,
block: UserOperationConfirmationModal,
delete: DeleteUserModal,
'delete-with-contributions': DeleteUserModal,
};
function loadModalsConfigurationFromHtml(modalsElement) {
const modalsConfiguration = {};
if (!modalsElement) {
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
throw new Error('Modals content element not found!');
}
Array.from(modalsElement.children).forEach(node => {
const { modal, ...config } = node.dataset;
modalsConfiguration[modal] = {
title: node.dataset.title,
...config,
content: node.innerHTML,
};
});
return modalsConfiguration;
}
document.addEventListener('DOMContentLoaded', () => {
Vue.use(Translate);
const deleteUserModalEl = document.getElementById('delete-user-modal');
const modalConfiguration = loadModalsConfigurationFromHtml(
document.querySelector(MODAL_TEXTS_CONTAINER_SELECTOR),
);
const deleteModal = new Vue({
el: deleteUserModalEl,
data: {
deleteUserUrl: '',
blockUserUrl: '',
deleteContributions: '',
username: '',
// eslint-disable-next-line no-new
new Vue({
el: MODAL_MANAGER_SELECTOR,
functional: true,
methods: {
show(...args) {
this.$refs.manager.show(...args);
},
},
render(createElement) {
return createElement(deleteUserModal, {
render(h) {
return h(ModalManager, {
ref: 'manager',
props: {
deleteUserUrl: this.deleteUserUrl,
blockUserUrl: this.blockUserUrl,
deleteContributions: this.deleteContributions,
username: this.username,
modalConfiguration,
actionModals: ACTION_MODALS,
csrfToken: csrf.token,
},
});
},
});
$(document).on('shown.bs.modal', event => {
if (event.relatedTarget.classList.contains('delete-user-button')) {
const buttonProps = event.relatedTarget.dataset;
deleteModal.deleteUserUrl = buttonProps.deleteUserUrl;
deleteModal.blockUserUrl = buttonProps.blockUserUrl;
deleteModal.deleteContributions = event.relatedTarget.hasAttribute(
'data-delete-contributions',
);
deleteModal.username = buttonProps.username;
}
});
});
......@@ -58,6 +58,22 @@ class Admin::UsersController < Admin::ApplicationController
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?
user.activate
redirect_back_or_admin_user(notice: _("Successfully activated"))
end
def deactivate
return redirect_back_or_admin_user(notice: _("Error occurred. A blocked user cannot be deactivated")) if user.blocked?
return redirect_back_or_admin_user(notice: _("Successfully deactivated")) if user.deactivated?
return redirect_back_or_admin_user(notice: _("The user you are trying to deactivate has been active in the past %{minimum_inactive_days} days and cannot be deactivated") % { minimum_inactive_days: ::User::MINIMUM_INACTIVE_DAYS }) unless user.can_be_deactivated?
user.deactivate
redirect_back_or_admin_user(notice: _("Successfully deactivated"))
end
def block
if update_user { |user| user.block }
redirect_back_or_admin_user(notice: _("Successfully blocked"))
......
......@@ -26,6 +26,7 @@ class ApplicationController < ActionController::Base
before_action :add_gon_variables, unless: [:peek_request?, :json_request?]
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :require_email, unless: :devise_controller?
before_action :active_user_check, unless: :devise_controller?
before_action :set_usage_stats_consent_flag
before_action :check_impersonation_availability
......@@ -294,6 +295,14 @@ class ApplicationController < ActionController::Base
end
end
def active_user_check
return unless current_user && current_user.deactivated?
sign_out current_user
flash[:alert] = _("Your account has been deactivated by your administrator. Please log back in to reactivate your account.")
redirect_to new_user_session_path
end
def ldap_security_check
if current_user && current_user.requires_ldap_check?
return unless current_user.try_obtain_ldap_lease
......
......@@ -148,6 +148,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
if user.two_factor_enabled? && !auth_user.bypass_two_factor?
prompt_for_two_factor(user)
else
if user.deactivated?
user.activate
flash[:notice] = _('Welcome back! Your account had been deactivated due to inactivity but is now reactivated.')
end
sign_in_and_redirect(user, event: :authentication)
end
else
......
......@@ -25,6 +25,9 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
return render_404 unless diffable
diffs = diffable.diffs_in_batch(params[:page], params[:per_page], diff_options: diff_options)
positions = @merge_request.note_positions_for_paths(diffs.diff_file_paths, current_user)
diffs.unfold_diff_files(positions.unfoldable)
options = {
merge_request: @merge_request,
......
......@@ -57,8 +57,14 @@ class SessionsController < Devise::SessionsController
reset_password_sent_at: nil)
end
# hide the signed-in notification
flash[:notice] = nil
if resource.deactivated?
resource.activate
flash[:notice] = _('Welcome back! Your account had been deactivated due to inactivity but is now reactivated.')
else
# hide the default signed-in notification
flash[:notice] = nil
end
log_audit_event(current_user, resource, with: authentication_method)
log_user_activity(current_user)
end
......
......@@ -52,6 +52,12 @@ class UserFinder
end
end
def find_by_ssh_key_id
return unless input_is_id?
User.find_by_ssh_key_id(@username_or_id)
end
def input_is_id?
@username_or_id.is_a?(Numeric) || @username_or_id =~ /^\d+$/
end
......
......@@ -15,7 +15,8 @@ module Ci
:merge_request_ref?,
:source_ref,
:source_ref_slug,
:legacy_detached_merge_request_pipeline?, to: :pipeline
:legacy_detached_merge_request_pipeline?,
:merge_train_pipeline?, to: :pipeline
end
end
end
......@@ -454,6 +454,15 @@ class MergeRequest < ApplicationRecord
merge_request_diffs.where.not(id: merge_request_diff.id)
end
# Overwritten in EE
def note_positions_for_paths(paths, _user = nil)
positions = notes.new_diff_notes.joins(:note_diff_file)
.where('note_diff_files.old_path IN (?) OR note_diff_files.new_path IN (?)', paths, paths)
.positions
Gitlab::Diff::PositionCollection.new(positions, diff_head_sha)
end
def preloads_discussion_diff_highlighting?
true
end
......
......@@ -193,6 +193,12 @@ class Note < ApplicationRecord
groups
end
def positions
where.not(position: nil)
.select(:id, :type, :position) # ActiveRecord needs id and type for typecasting.
.map(&:position)
end
def count_for_collection(ids, type)
user.select('noteable_id', 'COUNT(*) as count')
.group(:noteable_id)
......
......@@ -59,6 +59,8 @@ class User < ApplicationRecord
# Removed in GitLab 12.3. Keep until after 2019-09-22.
self.ignored_columns += %i[support_bot]
MINIMUM_INACTIVE_DAYS = 14
# Override Devise::Models::Trackable#update_tracked_fields!
# to limit database writes to at most once every hour
# rubocop: disable CodeReuse/ServiceClass
......@@ -242,18 +244,25 @@ class User < ApplicationRecord
state_machine :state, initial: :active do
event :block do
transition active: :blocked
transition deactivated: :blocked
transition ldap_blocked: :blocked
end
event :ldap_block do
transition active: :ldap_blocked
transition deactivated: :ldap_blocked
end
event :activate do
transition deactivated: :active
transition blocked: :active
transition ldap_blocked: :active
end
event :deactivate do
transition active: :deactivated
end
state :blocked, :ldap_blocked do
def blocked?
true
......@@ -284,6 +293,7 @@ class User < ApplicationRecord
scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active).non_internal }
scope :deactivated, -> { with_state(:deactivated).non_internal }
scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
......@@ -431,6 +441,8 @@ class User < ApplicationRecord
without_projects
when 'external'
external
when 'deactivated'
deactivated
else
active
end
......@@ -521,7 +533,7 @@ class User < ApplicationRecord
# Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id)
Key.find_by(id: key_id)&.user
find_by('EXISTS (?)', Key.select(1).where('keys.user_id = users.id').where(id: key_id))
end
def find_by_full_path(path, follow_redirects: false)
......@@ -1534,6 +1546,17 @@ class User < ApplicationRecord
!!(password_expires_at && password_expires_at < Time.now)
end
def can_be_deactivated?
active? && no_recent_activity?
end
def last_active_at
last_activity = last_activity_on&.to_time&.in_time_zone
last_sign_in = current_sign_in_at
[last_activity, last_sign_in].compact.max
end
# @deprecated
alias_method :owned_or_masters_groups, :owned_or_maintainers_groups
......@@ -1683,6 +1706,10 @@ class User < ApplicationRecord
::Group.where(id: developer_groups_hierarchy.select(:id),
project_creation_level: project_creation_levels)
end
def no_recent_activity?
last_active_at.to_i <= MINIMUM_INACTIVE_DAYS.days.ago.to_i
end
end
User.prepend_if_ee('EE::User')
......@@ -17,6 +17,10 @@ class BasePolicy < DeclarativePolicy::Base
with_options scope: :user, score: 0
condition(:blocked) { @user&.blocked? }
desc "User is deactivated"
with_options scope: :user, score: 0
condition(:deactivated) { @user&.deactivated? }
desc "User has access to all private groups & projects"
with_options scope: :user, score: 0
condition(:full_private_access) { @user&.full_private_access? }
......
......@@ -44,6 +44,12 @@ class GlobalPolicy < BasePolicy
prevent :use_slash_commands
end
rule { deactivated }.policy do
prevent :access_git
prevent :access_api
prevent :receive_notifications
end
rule { required_terms_not_accepted }.policy do
prevent :access_api
prevent :access_git
......
......@@ -6,6 +6,8 @@
%span.cred (Internal)
- if @user.admin
%span.cred (Admin)
- if @user.deactivated?
%span.cred (Deactivated)
= render_if_exists 'admin/users/audtior_user_badge'
.float-right
......
#user-modal
#modal-texts.hidden{ "hidden": true, "aria-hidden": true }
%div{ data: { modal: "deactivate",
title: s_("AdminUsers|Deactivate User %{username}?"),
action: s_("AdminUsers|Deactivate") } }
= render partial: 'admin/users/user_deactivation_effects'
%div{ data: { modal: "block",
title: s_("AdminUsers|Block user %{username}?"),
action: s_("AdminUsers|Block") } }
= render partial: 'admin/users/user_block_effects'
%div{ data: { modal: "delete",
title: s_("AdminUsers|Delete User %{username}?"),
action: s_('AdminUsers|Delete user'),
'secondary-action': s_('AdminUsers|Block user') } }
= s_('AdminUsers|You are about to permanently delete the user %{username}. Issues, merge requests,
and groups linked to them will be transferred to a system-wide "Ghost-user". To avoid data loss,
consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end},
it cannot be undone or recovered.')
%div{ data: { modal: "delete-with-contributions",
title: s_("AdminUsers|Delete User %{username} and contributions?"),
action: s_('AdminUsers|Delete user and contributions') ,
'secondary-action': s_('AdminUsers|Block user') } }
= s_('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 %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end},
it cannot be undone or recovered.')
......@@ -31,7 +31,19 @@
- elsif user.blocked?
= link_to _('Unblock'), unblock_admin_user_path(user), method: :put
- else
= link_to _('Block'), block_admin_user_path(user), data: { confirm: "#{s_('AdminUsers|User will be blocked').upcase}! #{_('Are you sure')}?" }, method: :put
%button.btn{ 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{ data: { 'gl-modal-action': 'deactivate',
url: deactivate_admin_user_path(user),
username: sanitize_name(user.name) } }
= s_('AdminUsers|Deactivate')
- elsif user.deactivated?
%li
= link_to _('Activate'), activate_admin_user_path(user), method: :put
- if user.access_locked?
%li
= link_to _('Unlock'), unlock_admin_user_path(user), method: :put, data: { confirm: _('Are you sure?') }
......@@ -39,19 +51,14 @@
%li.divider
- if user.can_be_removed?
%li
%button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
target: '#delete-user-modal',
%button.delete-user-button.btn.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),
delete_contributions: false }, type: 'button' }
username: sanitize_name(user.name) } }
= s_('AdminUsers|Delete user')
%li
%button.delete-user-button.btn.text-danger{ data: { toggle: 'modal',
target: '#delete-user-modal',
delete_user_url: admin_user_path(user, hard_delete: true),
block_user_url: block_admin_user_path(user),
username: sanitize_name(user.name),
delete_contributions: true }, type: 'button' }
= s_('AdminUsers|Delete user and contributions')
%li
%button.delete-user-button.btn.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) } }
= s_('AdminUsers|Delete user and contributions')
%p
= s_('AdminUsers|Reactivating a user will:')
%ul
%li
= s_('AdminUsers|Restore user access to the account, including web, Git and API.')
= render_if_exists 'admin/users/user_activation_effects_on_seats'
%p
= s_('AdminUsers|Blocking user has the following effects:')
%ul
%li
= s_('AdminUsers|User will not be able to login')
%li
= s_('AdminUsers|User will not be able to access git repositories')
%li
= s_('AdminUsers|Personal projects will be left')
%li
= s_('AdminUsers|Owned groups will be left')
%p
= s_('AdminUsers|Deactivating a user has the following effects:')
%ul
%li
= s_('AdminUsers|The user will be logged out')
%li
= s_('AdminUsers|The user will not be able to access git repositories')
%li
= s_('AdminUsers|The user will not be able to access the API')
%li
= s_('AdminUsers|The user will not receive any notifications')
%li
= s_('AdminUsers|When the user logs back in, their account will reactivate as a fully active account')
%li
= s_('AdminUsers|Personal projects, group and user history will be left intact')
= render_if_exists 'admin/users/user_deactivation_effects_on_seats'
......@@ -30,6 +30,10 @@
= link_to admin_users_path(filter: "blocked") do
= s_('AdminUsers|Blocked')
%small.badge.badge-pill= limited_counter_with_delimiter(User.blocked)
= nav_link(html_options: { class: active_when(params[:filter] == 'deactivated') }) do
= link_to admin_users_path(filter: "deactivated") do
= s_('AdminUsers|Deactivated')
%small.badge.badge-pill= limited_counter_with_delimiter(User.deactivated)
= nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do
= link_to admin_users_path(filter: "wop") do
= s_('AdminUsers|Without projects')
......@@ -50,6 +54,7 @@
= icon("search", class: "search-icon")
= button_tag s_('AdminUsers|Search users') if Rails.env.test?
.dropdown.user-sort-dropdown
= label_tag 'Sort by', nil, class: 'label-bold'
- toggle_text = @sort.present? ? users_sort_options_hash[@sort] : sort_title_name
= dropdown_toggle(toggle_text, { toggle: 'dropdown' })
%ul.dropdown-menu.dropdown-menu-right
......@@ -74,4 +79,4 @@
= paginate @users, theme: "gitlab"
#delete-user-modal
= render partial: 'admin/users/modals'
......@@ -156,6 +156,27 @@
= render_if_exists 'admin/users/user_detail_note'
- if @user.deactivated?
.card.border-info
.card-header.bg-info.text-white
Reactivate this user
.card-body
= render partial: 'admin/users/user_activation_effects'
%br
= link_to 'Activate user', activate_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' }
- elsif @user.can_be_deactivated?
.card.border-warning
.card-header.bg-warning.text-white
Deactivate this user
.card-body
= render partial: 'admin/users/user_deactivation_effects'
%br
%button.btn.btn-warning{ data: { 'gl-modal-action': 'deactivate',
content: 'You can always re-activate their account, their data will remain intact.',
url: deactivate_admin_user_path(@user),
username: sanitize_name(@user.name) } }
= s_('AdminUsers|Deactivate user')
- if @user.blocked?
.card.border-info
.card-header.bg-info.text-white
......@@ -172,14 +193,13 @@
.card-header.bg-warning.text-white
Block this user
.card-body
%p Blocking user has the following effects:
%ul
%li User will not be able to login
%li User will not be able to access git repositories
%li Personal projects will be left
%li Owned groups will be left
= render partial: 'admin/users/user_block_effects'
%br
= link_to 'Block user', block_admin_user_path(@user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put, class: "btn btn-warning"
%button.btn.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')
- if @user.access_locked?
.card.border-info
.card-header.bg-info.text-white
......@@ -197,12 +217,10 @@
%p Deleting a user has the following effects:
= render 'users/deletion_guidance', user: @user
%br
%button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal',
target: '#delete-user-modal',
%button.delete-user-button.btn.btn-danger{ data: { 'gl-modal-action': 'delete',
delete_user_url: admin_user_path(@user),
block_user_url: block_admin_user_path(@user),
username: @user.name,
delete_contributions: false }, type: 'button' }
username: sanitize_name(@user.name) } }
= s_('AdminUsers|Delete user')
- else
- if @user.solo_owned_groups.present?
......@@ -229,15 +247,13 @@
the user, and projects in them, will also be removed. Commits
to other projects are unaffected.
%br
%button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal',
target: '#delete-user-modal',
%button.delete-user-button.btn.btn-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: @user.name,
delete_contributions: true }, type: 'button' }
username: @user.name } }
= s_('AdminUsers|Delete user and contributions')
- else
%p
You don't have access to delete this user.
#delete-user-modal
= render partial: 'admin/users/modals'
---
title: Deactivate a user (with self-service reactivation)
merge_request: 17037
author:
type: added
......@@ -13,6 +13,8 @@ namespace :admin do
get :keys
put :block
put :unblock
put :deactivate
put :activate
put :unlock
put :confirm
post :impersonate
......
......@@ -58,7 +58,7 @@ this method only supports replies, and not the other features of [incoming email
## Set it up
If you want to use Gmail / Google Apps for incoming emails, make sure you have
[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
[IMAP access enabled](https://support.google.com/mail/answer/7126229)
and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255)
or [turn-on 2-step validation](https://support.google.com/accounts/answer/185839)
and use [an application password](https://support.google.com/mail/answer/185833).
......
......@@ -60,8 +60,8 @@ guides document the necessary steps for a selection of popular reverse proxies:
- [Apache](https://httpd.apache.org/docs/2.4/mod/mod_proxy_wstunnel.html)
- [NGINX](https://www.nginx.com/blog/websocket-nginx/)
- [HAProxy](http://blog.haproxy.com/2012/11/07/websockets-load-balancing-with-haproxy/)
- [Varnish](https://www.varnish-cache.org/docs/4.1/users-guide/vcl-example-websockets.html)
- [HAProxy](https://www.haproxy.com/blog/websockets-load-balancing-with-haproxy/)
- [Varnish](https://varnish-cache.org/docs/4.1/users-guide/vcl-example-websockets.html)
Workhorse won't let WebSocket requests through to non-WebSocket endpoints, so
it's safe to enable support for these headers globally. If you'd rather had a
......
......@@ -1152,6 +1152,48 @@ Parameters:
Will return `201 OK` on success, `404 User Not Found` is user cannot be found or
`403 Forbidden` when trying to unblock a user blocked by LDAP synchronization.
## Deactivate user
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/63921) in GitLab 12.4.
Deactivates the specified user. Available only for admin.
```
POST /users/:id/deactivate
```
Parameters:
- `id` (required) - id of specified user
Returns:
- `201 OK` on success.
- `404 User Not Found` if user cannot be found.
- `403 Forbidden` when trying to deactivate a user:
- Blocked by admin or by LDAP synchronization.
- That has any activity in past 14 days. These cannot be deactivated.
## Activate user
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/63921) in GitLab 12.4.
Activates the specified user. Available only for admin.
```
POST /users/:id/activate
```
Parameters:
- `id` (required) - id of specified user
Returns:
- `201 OK` on success.
- `404 User Not Found` if user cannot be found.
- `403 Forbidden` when trying to activate a user blocked by admin or by LDAP synchronization.
### Get user contribution events
Please refer to the [Events API documentation](events.md#get-user-contribution-events)
......
......@@ -122,6 +122,12 @@ is unavailable when
Follow [this issue](https://gitlab.com/gitlab-org/gitlab/issues/12267) to
track progress on this issue.
### Merge Train Pipeline cannot be retried
A Merge Train pipeline cannot be retried because the merge request is dropped from the merge train upon failure. For this reason, the retry button does not appear next to the pipeline icon.
In the case of pipeline failure, you should [re-enqueue](#how-to-add-a-merge-request-to-a-merge-train) the merge request to the merge train, which will then initiate a new pipeline.
### Merge Train disturbs your workflow
First of all, please check if [merge immediately](#immediately-merge-a-merge-request-with-a-merge-train)
......
......@@ -159,7 +159,6 @@ discovers the user associated with an SSH key.
|:----------|:-------|:---------|:------------|
| `key_id` | integer | no | The id of the SSH key used as found in the authorized-keys file or through the `/authorized_keys` check |
| `username` | string | no | Username of the user being looked up, used by GitLab-shell when authenticating using a certificate |
| `user_id` | integer | no | **Deprecated** User_id of the user being looked up |
```
GET /internal/discover
......
......@@ -148,7 +148,7 @@ For more information, please see our:
- [Subscription FAQ](https://about.gitlab.com/pricing/licensing-faq/).
- [Pricing page](https://about.gitlab.com/pricing/), which includes information
on our [true-up pricing policy](https://about.gitlab.com/handbook/product/pricing/#true-up-pricing)
on our [true-up pricing policy](https://about.gitlab.com/handbook/ceo/pricing/#true-up-pricing)
when adding more users other than at the time of purchase.
NOTE: **Note:**
......
......@@ -77,7 +77,7 @@ As Auto DevOps relies on many different components, it's good to have a basic
knowledge of the following:
- [Kubernetes](https://kubernetes.io/docs/home/)
- [Helm](https://docs.helm.sh/)
- [Helm](https://helm.sh/docs/)
- [Docker](https://docs.docker.com)
- [GitLab Runner](https://docs.gitlab.com/runner/)
- [Prometheus](https://prometheus.io/docs/introduction/overview/)
......@@ -124,7 +124,7 @@ To make full use of Auto DevOps, you will need:
- A [Kubernetes cluster][kubernetes-clusters] for the project.
- A load balancer. You can use NGINX Ingress by deploying it to your
Kubernetes cluster by either:
- Using the [`nginx-ingress`](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress) Helm chart.
- Using the [`nginx-ingress`](https://github.com/helm/charts/tree/master/stable/nginx-ingress) Helm chart.
- Installing the Ingress [GitLab Managed App](../../user/clusters/applications.md#ingress).
- **Prometheus** (for Auto Monitoring)
......@@ -172,7 +172,7 @@ and `1.2.3.4` is the IP address of your load balancer; generally NGINX
([see requirements](#requirements)). How to set up the DNS record is beyond
the scope of this document; you should check with your DNS provider.
Alternatively you can use free public services like [nip.io](http://nip.io)
Alternatively you can use free public services like [nip.io](https://nip.io)
which provide automatic wildcard DNS without any configuration. Just set the
Auto DevOps base domain to `1.2.3.4.nip.io`.
......
......@@ -25,7 +25,7 @@ Google account (for example, one that you use to access Gmail, Drive, etc.) or c
TIP: **Tip:**
Every new Google Cloud Platform (GCP) account receives [$300 in credit](https://console.cloud.google.com/freetrial),
and in partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's
Google Kubernetes Engine Integration. All you have to do is [follow this link](https://goo.gl/AaJzRW) and apply for credit.
Google Kubernetes Engine Integration. All you have to do is [follow this link](https://cloud.google.com/partners/partnercredit/?PCN=a0n60000006Vpz4AAC) and apply for credit.
## Creating a new project from a template
......
......@@ -105,8 +105,16 @@ You can administer all users in the GitLab instance from the Admin Area's Users
To access the Users page, go to **Admin Area > Overview > Users**.
Click the **Active**, **Admins**, **2FA Enabled**, or **2FA Disabled**, **External**, or
**Without projects** tab to list only users of that criteria.
To list users matching a specific criteria, click on one of the following tabs on the **Users** page:
- **Active**
- **Admins**
- **2FA Enabled**
- **2FA Disabled**
- **External**
- **Blocked**
- **Deactivated**
- **Without projects**
For each user, their username, email address, are listed, also the date their account was
created and the date of last activity. To edit a user, click the **Edit** button in that user's
......
......@@ -169,7 +169,7 @@ For Cycle Analytics functionality introduced in GitLab 12.3 and later:
Learn more about Cycle Analytics in the following resources:
- [Cycle Analytics feature page](https://about.gitlab.com/features/cycle-analytics/)
- [Cycle Analytics feature page](https://about.gitlab.com/product/cycle-analytics/)
- [Cycle Analytics feature preview](https://about.gitlab.com/2016/09/16/feature-preview-introducing-cycle-analytics/)
- [Cycle Analytics feature highlight](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/)
......
......@@ -62,7 +62,7 @@ can lead to confusion during deployments.
> - Introduced in GitLab 10.2 for project-level clusters.
> - Introduced in GitLab 11.6 for group-level clusters.
[Helm](https://docs.helm.sh/) is a package manager for Kubernetes and is
[Helm](https://helm.sh/docs/) is a package manager for Kubernetes and is
required to install all the other applications. It is installed in its
own pod inside the cluster which can run the `helm` CLI in a safe
environment.
......@@ -174,7 +174,7 @@ higher](../permissions.md) access to the associated project or group.
We use a [custom Jupyter
image](https://gitlab.com/gitlab-org/jupyterhub-user-image/blob/master/Dockerfile)
that installs additional useful packages on top of the base Jupyter. You
will also see ready-to-use DevOps Runbooks built with Nurtch's [Rubix library](https://github.com/amit1rrr/rubix).
will also see ready-to-use DevOps Runbooks built with Nurtch's [Rubix library](https://github.com/Nurtch/rubix).
More information on
creating executable runbooks can be found in [our Runbooks
......@@ -221,7 +221,7 @@ You can clone repositories from the files tab in Jupyter:
> - Introduced in GitLab 11.5 for project-level clusters.
> - Introduced in GitLab 12.3 for group- and instance-level clusters.
[Knative](https://cloud.google.com/knative) provides a platform to
[Knative](https://cloud.google.com/knative/) provides a platform to
create, deploy, and manage serverless workloads from a Kubernetes
cluster. It is used in conjunction with, and includes
[Istio](https://istio.io) to provide an external IP address for all
......
......@@ -41,7 +41,7 @@ Host gitlab.com
## GitLab Pages
Below are the settings for [GitLab Pages].
Below are the settings for [GitLab Pages](https://about.gitlab.com/product/pages/).
| Setting | GitLab.com | Default |
| --------------------------- | ---------------- | ------------- |
......@@ -103,13 +103,11 @@ Below are the shared Runners settings.
| Setting | GitLab.com | Default |
| ----------- | ----------------- | ---------- |
| [GitLab Runner] | [Runner versions dashboard][ci_version_dashboard] | - |
| [GitLab Runner] | [Runner versions dashboard](https://dashboards.gitlab.com/d/000000159/ci?from=now-1h&to=now&refresh=5m&orgId=1&panelId=12&fullscreen&theme=light) | - |
| Executor | `docker+machine` | - |
| Default Docker image | `ruby:2.5` | - |
| `privileged` (run [Docker in Docker]) | `true` | `false` |
[ci_version_dashboard]: https://dashboards.gitlab.com/dashboard/db/ci?from=now-1h&to=now&refresh=5m&orgId=1&panelId=12&fullscreen&theme=light
### `config.toml`
The full contents of our `config.toml` are:
......@@ -174,7 +172,7 @@ sentry_dsn = "X"
## Sidekiq
GitLab.com runs [Sidekiq][sidekiq] with arguments `--timeout=4 --concurrency=4`
GitLab.com runs [Sidekiq](https://sidekiq.org) with arguments `--timeout=4 --concurrency=4`
and the following environment variables:
| Setting | GitLab.com | Default |
......@@ -275,7 +273,7 @@ released depending on the type of block, as described below.
If you receive a `403 Forbidden` error for all requests to GitLab.com, please
check for any automated processes that may be triggering a block. For
assistance, contact [GitLab Support](https://support.gitlab.com)
assistance, contact [GitLab Support](https://support.gitlab.com/hc/en-us)
with details, such as the affected IP address.
### HAProxy API throttle
......@@ -390,10 +388,8 @@ High Performance TCP/HTTP Load Balancer:
[runners-post]: https://about.gitlab.com/2016/04/05/shared-runners/ "Shared Runners on GitLab.com"
[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-runner
[altssh]: https://about.gitlab.com/2016/02/18/gitlab-dot-com-now-supports-an-alternate-git-plus-ssh-port/ "GitLab.com now supports an alternate git+ssh port"
[GitLab Pages]: https://about.gitlab.com/features/pages "GitLab Pages"
[docker in docker]: https://hub.docker.com/_/docker/ "Docker in Docker at DockerHub"
[mailgun]: https://www.mailgun.com/ "Mailgun website"
[sidekiq]: http://sidekiq.org/ "Sidekiq website"
[unicorn-worker-killer]: https://rubygems.org/gems/unicorn-worker-killer "unicorn-worker-killer"
[4010]: https://gitlab.com/gitlab-com/infrastructure/issues/4010 "Find a good value for maximum timeout for Shared Runners"
[4070]: https://gitlab.com/gitlab-com/infrastructure/issues/4070 "Configure per-runner timeout for shared-runners-manager-X on GitLab.com"
......
......@@ -85,7 +85,7 @@ the re-calculation will happen immediately to set a new dynamic date.
## Roadmap in epics
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7327) in [GitLab Ultimate](https://about.gitlab.com/pricing) 11.10.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7327) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.10.
If your epic contains one or more [child epics](#multi-level-child-epics) which
have a [start or due date](#start-date-and-due-date), then you can see a
......
......@@ -105,13 +105,13 @@ NOTE: **Note:** GitLab is unable to provide support for IdPs that are not listed
| Provider | Documentation |
|----------|---------------|
| ADFS (Active Directory Federation Services) | [Create a Relying Party Trust](https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/operations/create-a-relying-party-trust) |
| Azure | [Configuring single sign-on to applications](https://docs.microsoft.com/en-us/azure/active-directory/active-directory-saas-custom-apps) |
| Azure | [Configuring single sign-on to applications](https://docs.microsoft.com/en-us/azure/active-directory/manage-apps/configure-single-sign-on-non-gallery-applications) |
| Auth0 | [Auth0 as Identity Provider](https://auth0.com/docs/protocols/saml/saml-idp-generic) |
| G Suite | [Set up your own custom SAML application](https://support.google.com/a/answer/6087519?hl=en) |
| JumpCloud | [Single Sign On (SSO) with GitLab](https://support.jumpcloud.com/customer/en/portal/articles/2810701-single-sign-on-sso-with-gitlab) |
| Okta | [Setting up a SAML application in Okta](https://developer.okta.com/standards/SAML/setting_up_a_saml_application_in_okta) |
| Okta | [Setting up a SAML application in Okta](https://developer.okta.com/docs/guides/saml-application-setup/overview/) |
| OneLogin | [Use the OneLogin SAML Test Connector](https://onelogin.service-now.com/support?id=kb_article&sys_id=93f95543db109700d5505eea4b96198f) |
| Ping Identity | [Add and configure a new SAML application](https://docs.pingidentity.com/bundle/p1_enterpriseConfigSsoSaml_cas/page/enableAppWithoutURL.html) |
| Ping Identity | [Add and configure a new SAML application](https://support.pingidentity.com/s/document-item?bundleId=pingone&topicId=xsh1564020480660-1.html) |
## Linking SAML to your existing GitLab.com account
......
......@@ -14,7 +14,7 @@ NOTE: **Note:** We encourage you to view this document as [rendered by GitLab it
GitLab uses "GitLab Flavored Markdown" (GFM). It extends the [CommonMark specification](https://spec.commonmark.org/current/)
(which is based on standard Markdown) in several ways to add additional useful functionality.
It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/).
It was inspired by [GitHub Flavored Markdown](https://help.github.com/en/articles/basic-writing-and-formatting-syntax).
You can use GFM in the following areas:
......@@ -352,7 +352,7 @@ However the wrapping tags cannot be mixed:
> If this is not rendered correctly, [view it in GitLab itself](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/markdown.md#math).
It is possible to have math written with LaTeX syntax rendered using [KaTeX](https://github.com/Khan/KaTeX).
It is possible to have math written with LaTeX syntax rendered using [KaTeX](https://github.com/KaTeX/KaTeX).
Math written between dollar signs `$` will be rendered inline with the text. Math written
inside a [code block](#code-spans-and-blocks) with the language declared as `math`, will be rendered
......@@ -379,7 +379,7 @@ a^2+b^2=c^2
_Be advised that KaTeX only supports a [subset](https://katex.org/docs/supported.html) of LaTeX._
NOTE: **Note:** This also works for the asciidoctor `:stem: latexmath`. For details see
the [asciidoctor user manual](http://asciidoctor.org/docs/user-manual/#activating-stem-support).
the [asciidoctor user manual](https://asciidoctor.org/docs/user-manual/#activating-stem-support).
### Special GitLab references
......@@ -641,7 +641,7 @@ Tildes are OK too.
GitLab uses the [Rouge Ruby library](http://rouge.jneen.net/) for more colorful syntax
highlighting in code blocks. For a list of supported languages visit the
[Rouge project wiki](https://github.com/jneen/rouge/wiki/List-of-supported-languages-and-lexers).
[Rouge project wiki](https://github.com/rouge-ruby/rouge/wiki/List-of-supported-languages-and-lexers).
Syntax highlighting is only supported in code blocks, it is not possible to highlight
code when it is inline.
......@@ -922,7 +922,7 @@ Here's a sample audio clip:
You can also use raw HTML in your Markdown, and it'll usually work pretty well.
See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/1.11.0/HTML/Pipeline/SanitizationFilter#WHITELIST-constant)
See the documentation for HTML::Pipeline's [SanitizationFilter](https://www.rubydoc.info/gems/html-pipeline/1.11.0/HTML/Pipeline/SanitizationFilter#WHITELIST-constant)
class for the list of allowed HTML tags and attributes. In addition to the default
`SanitizationFilter` whitelist, GitLab allows `span`, `abbr`, `details` and `summary` elements.
......@@ -1126,8 +1126,8 @@ Using references:
Some text to show that the reference links can follow later.
[arbitrary case-insensitive reference text]: https://www.mozilla.org
[1]: http://slashdot.org
[arbitrary case-insensitive reference text]: https://www.mozilla.org/en-US/
[1]: https://slashdot.org
[link text itself]: https://www.reddit.com
```
......@@ -1149,8 +1149,8 @@ Using references:
Some text to show that the reference links can follow later.
[arbitrary case-insensitive reference text]: https://www.mozilla.org
[1]: http://slashdot.org
[arbitrary case-insensitive reference text]: https://www.mozilla.org/en-US/
[1]: https://slashdot.org
[link text itself]: https://www.reddit.com
NOTE: **Note:** Relative links do not allow the referencing of project files in a wiki
......@@ -1164,7 +1164,7 @@ GFM will autolink almost any URL you put into your text:
```markdown
- https://www.google.com
- https://google.com/
- https://www.google.com
- ftp://ftp.us.debian.org/debian/
- smb://foo/bar/baz
- irc://irc.freenode.net/
......@@ -1172,7 +1172,7 @@ GFM will autolink almost any URL you put into your text:
```
- <https://www.google.com>
- <https://google.com/>
- <https://www.google.com>
- <ftp://ftp.us.debian.org/debian/>
- <smb://foo/bar/baz>
- <irc://irc.freenode.net/>
......
......@@ -24,7 +24,7 @@ For information on eligible approvers for Merge Requests, see
## Principles behind permissions
See our [product handbook on permissions](https://about.gitlab.com/handbook/product#permissions-in-gitlab)
See our [product handbook on permissions](https://about.gitlab.com/handbook/product/#permissions-in-gitlab)
## Instance-wide user permissions
......
......@@ -42,6 +42,52 @@ a user can be blocked directly from the Admin area. To do this:
1. Selecting a user.
1. Under the **Account** tab, click **Block user**.
### Deactivating a user
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/63921) in GitLab 12.4.
A user can be deactivated from the Admin area. Deactivating a user is functionally identical to blocking a user, 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.
Personal projects, group and user history of the deactivated user will be left intact.
NOTE: **Note:**
A deactivated user does not consume a [seat](../../../subscriptions/index.md#managing-subscriptions).
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.
- Should not have any activity in the last 14 days.
### Activating a user
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/63921) in GitLab 12.4.
A deactivated user can be activated from the Admin area. Activating a user sets their account to active state.
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**.
TIP: **Tip:**
A deactivated user can also activate their account by themselves by simply logging back via the UI.
## Associated Records
> - Introduced for issues in
......
......@@ -192,7 +192,7 @@ deployment of the other applications.
Next, if you would like the deployed app to be reachable on the internet, deploy
the Ingress. Note that this will also cause an
[Elastic Load Balancer](https://aws.amazon.com/documentation/elastic-load-balancing/)
[Elastic Load Balancer](https://docs.aws.amazon.com/elasticloadbalancing/)
to be created, which will incur additional AWS costs.
Once installed, you may see a `?` for "Ingress IP Address". This is because the
......
......@@ -112,7 +112,7 @@ There are two options when adding a new cluster to your project:
TIP: **Tip:**
Every new Google Cloud Platform (GCP) account receives [$300 in credit upon sign up](https://console.cloud.google.com/freetrial),
and in partnership with Google, GitLab is able to offer an additional $200 for new GCP accounts to get started with GitLab's
Google Kubernetes Engine Integration. All you have to do is [follow this link](https://goo.gl/AaJzRW) and apply for credit.
Google Kubernetes Engine Integration. All you have to do is [follow this link](https://cloud.google.com/partners/partnercredit/?PCN=a0n60000006Vpz4AAC) and apply for credit.
NOTE: **Note:**
The [Google authentication integration](../../../integration/google.md) must
......@@ -390,8 +390,8 @@ you can either:
When creating a cluster in GitLab, you will be asked if you would like to create either:
- An [Attribute-based access control (ABAC)](https://kubernetes.io/docs/admin/authorization/abac/) cluster.
- A [Role-based access control (RBAC)](https://kubernetes.io/docs/admin/authorization/rbac/) cluster.
- An [Attribute-based access control (ABAC)](https://kubernetes.io/docs/reference/access-authn-authz/abac/) cluster.
- A [Role-based access control (RBAC)](https://kubernetes.io/docs/reference/access-authn-authz/rbac/) cluster.
NOTE: **Note:**
[RBAC](#rbac-cluster-resources) is recommended and the GitLab default.
......
......@@ -50,7 +50,7 @@ To create an executable runbook, you will need:
Nurtch is the company behind the [Rubix library](https://github.com/Nurtch/rubix). Rubix is
an open-source Python library that makes it easy to perform common DevOps tasks inside Jupyter Notebooks.
Tasks such as plotting Cloudwatch metrics and rolling your ECS/Kubernetes app are simplified
down to a couple of lines of code. See the [Nurtch Documentation](http://docs.nurtch.com/en/latest)
down to a couple of lines of code. See the [Nurtch Documentation](http://docs.nurtch.com/en/latest/)
for more information.
## Configure an executable runbook with GitLab
......
......@@ -29,9 +29,9 @@ to the latest release.
Since Deploy Boards are tightly coupled with Kubernetes, there is some required
knowledge. In particular you should be familiar with:
- [Kubernetes pods](https://kubernetes.io/docs/user-guide/pods)
- [Kubernetes pods](https://kubernetes.io/docs/concepts/workloads/pods/pod/)
- [Kubernetes labels](https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/)
- [Kubernetes namespaces](https://kubernetes.io/docs/user-guide/namespaces/)
- [Kubernetes namespaces](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/)
- [Kubernetes canary deployments](https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments)
## Use cases
......
......@@ -50,7 +50,7 @@ users will only see the thread through email.
> **Note:**
Service Desk is enabled on GitLab.com. If you're a
[Silver subscriber](https://about.gitlab.com/gitlab-com/),
[Silver subscriber](https://about.gitlab.com/pricing/#gitlab-com),
you can skip the step 1 below; you only need to enable it per project.
1. [Set up incoming email](../../administration/incoming_email.md#set-it-up) for the GitLab instance. This must
......
......@@ -23,7 +23,7 @@ and go back to **Files**.
## How it works
The File finder feature is powered by the [Fuzzy filter] library.
The File finder feature is powered by the [Fuzzy filter](https://github.com/jeancroy/fuzz-aldrin-plus) library.
It implements a fuzzy search with highlight, and tries to provide intuitive
results by recognizing patterns that people use while searching.
......@@ -38,5 +38,4 @@ Using fuzzy search, we start by typing letters that get us closer to the file.
![Find file button](img/file_finder_find_file.png)
[gh-9889]: https://github.com/gitlabhq/gitlabhq/pull/9889 "File finder pull request"
[fuzzy filter]: https://github.com/jeancroy/fuzzaldrin-plus "fuzzaldrin-plus on GitHub"
[ce]: https://gitlab.com/gitlab-org/gitlab-foss/tree/master "GitLab CE repository"
......@@ -79,7 +79,7 @@ mirror.
To set up a mirror from GitLab to GitHub, you need to follow these steps:
1. Create a [GitHub personal access token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/) with the `public_repo` box checked.
1. Create a [GitHub personal access token](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line) with the `public_repo` box checked.
1. Fill in the **Git repository URL** field using this format: `https://<your_github_username>@github.com/<your_github_group>/<your_github_project>.git`.
1. Fill in **Password** field with your GitHub personal access token.
1. Click the **Mirror repository** button.
......@@ -197,10 +197,10 @@ Assuming you used the former, you now need to verify that the fingerprints are
those you expect. GitLab.com and other code hosting sites publish their
fingerprints in the open for you to check:
- [AWS CodeCommit](http://docs.aws.amazon.com/codecommit/latest/userguide/regions.html#regions-fingerprints)
- [Bitbucket](https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints)
- [GitHub](https://help.github.com/articles/github-s-ssh-key-fingerprints/)
- [GitLab.com](https://about.gitlab.com/gitlab-com/settings/#ssh-host-keys-fingerprints)
- [AWS CodeCommit](https://docs.aws.amazon.com/codecommit/latest/userguide/regions.html#regions-fingerprints)
- [Bitbucket](https://confluence.atlassian.com/bitbucket/ssh-keys-935365775.html)
- [GitHub](https://help.github.com/en/articles/githubs-ssh-key-fingerprints)
- [GitLab.com](../user/gitlab_com/index.md#ssh-host-keys-fingerprints)
- [Launchpad](https://help.launchpad.net/SSHFingerprints)
- [Savannah](http://savannah.gnu.org/maintenance/SshAccess/)
- [SourceForge](https://sourceforge.net/p/forge/documentation/SSH%20Key%20Fingerprints/)
......@@ -398,7 +398,7 @@ CAUTION: **Warning:**
Bidirectional mirroring should not be used as a permanent configuration. Refer to
[Migrating from Perforce Helix](../user/project/import/perforce.md) for alternative migration approaches.
[Git Fusion](https://www.perforce.com/video-tutorials/git-fusion-overview) provides a Git interface
[Git Fusion](https://www.perforce.com/manuals/git-fusion/#Git-Fusion/section_avy_hyc_gl.html) provides a Git interface
to [Perforce Helix](https://www.perforce.com/products) which can be used by GitLab to bidirectionally
mirror projects with GitLab. This may be useful in some situations when migrating from Perforce Helix
to GitLab where overlapping Perforce Helix workspaces cannot be migrated simultaneously to GitLab.
......@@ -415,7 +415,7 @@ settings are recommended:
- `unknown_git` user will be used as the commit author if the GitLab user does not exist in
Perforce Helix.
Read about [Git Fusion settings on Perforce.com](https://www.perforce.com/perforce/doc.current/manuals/git-fusion/Content/Git-Fusion/section_vss_bdw_w3.html#section_zdp_zz1_3l).
Read about [Git Fusion settings on Perforce.com](https://www.perforce.com/manuals/git-fusion/Content/Git-Fusion/section_vss_bdw_w3.html#section_zdp_zz1_3l).
## Troubleshooting
......@@ -423,4 +423,4 @@ Should an error occur during a push, GitLab will display an "Error" highlight fo
### 13:Received RST_STREAM with error code 2 with GitHub
If you receive an "13:Received RST_STREAM with error code 2" while mirroring to a GitHub repository, your GitHub settings might be set to block pushes that expose your email address used in commits. Either set your email address on GitHub to be public, or disable the [Block command line pushes that expose my email](http://github.com/settings/emails) setting.
If you receive an "13:Received RST_STREAM with error code 2" while mirroring to a GitHub repository, your GitHub settings might be set to block pushes that expose your email address used in commits. Either set your email address on GitHub to be public, or disable the [Block command line pushes that expose my email](https://github.com/settings/emails) setting.
......@@ -129,20 +129,15 @@ module API
#
# Discover user by ssh key, user id or username
#
# rubocop: disable CodeReuse/ActiveRecord
get "/discover" do
get '/discover' do
if params[:key_id]
key = Key.find(params[:key_id])
user = key.user
elsif params[:user_id]
user = User.find_by(id: params[:user_id])
user = UserFinder.new(params[:key_id]).find_by_ssh_key_id
elsif params[:username]
user = UserFinder.new(params[:username]).find_by_username
end
present user, with: Entities::UserSafe
end
# rubocop: enable CodeReuse/ActiveRecord
get "/check" do
{
......
......@@ -459,6 +459,42 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Activate a deactivated user. Available only for admins.'
params do
requires :id, type: Integer, desc: 'The ID of the user'
end
# rubocop: disable CodeReuse/ActiveRecord
post ':id/activate' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
forbidden!('A blocked user must be unblocked to be activated') if user.blocked?
user.activate
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Deactivate an active user. Available only for admins.'
params do
requires :id, type: Integer, desc: 'The ID of the user'
end
# rubocop: disable CodeReuse/ActiveRecord
post ':id/deactivate' do
authenticated_as_admin!
user = User.find_by(id: params[:id])
not_found!('User') unless user
break if user.deactivated?
unless user.can_be_deactivated?
forbidden!('A blocked user cannot be deactivated by the API') if user.blocked?
forbidden!("The user you are trying to deactivate has been active in the past #{::User::MINIMUM_INACTIVE_DAYS} days and cannot be deactivated")
end
user.deactivate
end
# rubocop: enable CodeReuse/ActiveRecord
desc 'Block a user. Available only for admins.'
params do
requires :id, type: Integer, desc: 'The ID of the user'
......@@ -489,6 +525,8 @@ module API
if user.ldap_blocked?
forbidden!('LDAP blocked users cannot be unblocked by the API')
elsif user.deactivated?
forbidden!('Deactivated users cannot be unblocked by the API')
else
user.activate
end
......
......@@ -69,7 +69,7 @@ module Gitlab
Gitlab::Auth::UniqueIpsLimiter.limit_user! do
user = User.by_login(login)
break if user && !user.active?
break if user && !user.can?(:log_in)
authenticators = []
......
......@@ -14,6 +14,9 @@ module Gitlab
when :terms_not_accepted
"You (#{@user.to_reference}) must accept the Terms of Service in order to perform this action. "\
"Please access GitLab from a web browser to accept these terms."
when :deactivated
"Your account has been deactivated by your administrator. "\
"Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}"
else
"Your account has been blocked."
end
......@@ -26,6 +29,8 @@ module Gitlab
:internal
elsif @user.required_terms_not_accepted?
:terms_not_accepted
elsif @user.deactivated?
:deactivated
else
:blocked
end
......
# Read more about this feature here: https://docs.gitlab.com/ee/user/application_security/container_scanning/
variables:
CS_MAJOR_VERSION: 1
container_scanning:
stage: test
image:
name: registry.gitlab.com/gitlab-org/security-products/analyzers/klar:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable
name: registry.gitlab.com/gitlab-org/security-products/analyzers/klar:$CS_MAJOR_VERSION
entrypoint: []
variables:
# By default, use the latest clair vulnerabilities database, however, allow it to be overridden here
......
......@@ -29,6 +29,10 @@ module Gitlab
}
end
def diff_file_paths
diff_files.map(&:file_path)
end
override :diffs
def diffs
strong_memoize(:diffs) do
......
......@@ -54,7 +54,7 @@ module Gitlab
def unfold_required?
strong_memoize(:unfold_required) do
next false unless @diff_file.text?
next false unless @position.on_text? && @position.unchanged?
next false unless @position.unfoldable?
next false if @diff_file.new_file? || @diff_file.deleted_file?
next false unless @position.old_line
# Invalid position (MR import scenario)
......
......@@ -79,6 +79,10 @@ module Gitlab
formatter.line_age
end
def unfoldable?
on_text? && unchanged?
end
def unchanged?
type.nil?
end
......
# frozen_string_literal: true
module Gitlab
module Diff
class PositionCollection
include Enumerable
# collection - An array of Gitlab::Diff::Position
def initialize(collection, diff_head_sha)
@collection = collection
@diff_head_sha = diff_head_sha
end
def each(&block)
@collection.each(&block)
end
def concat(positions)
tap { @collection.concat(positions) }
end
# Doing a lightweight filter in-memory given we're not prepared for querying
# positions (https://gitlab.com/gitlab-org/gitlab/issues/33271).
def unfoldable
select do |position|
position.unfoldable? && position.head_sha == @diff_head_sha
end
end
end
end
end
......@@ -809,6 +809,9 @@ msgstr ""
msgid "Action to take when receiving an alert."
msgstr ""
msgid "Activate"
msgstr ""
msgid "Activate Service Desk"
msgstr ""
......@@ -1051,12 +1054,6 @@ msgstr ""
msgid "Admin notes"
msgstr ""
msgid "AdminArea| You are about to permanently delete the user %{username}. Issues, merge requests, and groups linked to them will be transferred to a system-wide \"Ghost-user\". To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered."
msgstr ""
msgid "AdminArea| 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 %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered."
msgstr ""
msgid "AdminArea|Stop all jobs"
msgstr ""
......@@ -1162,15 +1159,39 @@ msgstr ""
msgid "AdminUsers|Admins"
msgstr ""
msgid "AdminUsers|Block"
msgstr ""
msgid "AdminUsers|Block user"
msgstr ""
msgid "AdminUsers|Block user %{username}?"
msgstr ""
msgid "AdminUsers|Blocked"
msgstr ""
msgid "AdminUsers|Blocking user has the following effects:"
msgstr ""
msgid "AdminUsers|Cannot unblock LDAP blocked users"
msgstr ""
msgid "AdminUsers|Deactivate"
msgstr ""
msgid "AdminUsers|Deactivate User %{username}?"
msgstr ""
msgid "AdminUsers|Deactivate user"
msgstr ""
msgid "AdminUsers|Deactivated"
msgstr ""
msgid "AdminUsers|Deactivating a user has the following effects:"
msgstr ""
msgid "AdminUsers|Delete User %{username} and contributions?"
msgstr ""
......@@ -1195,6 +1216,21 @@ msgstr ""
msgid "AdminUsers|No users found"
msgstr ""
msgid "AdminUsers|Owned groups will be left"
msgstr ""
msgid "AdminUsers|Personal projects will be left"
msgstr ""
msgid "AdminUsers|Personal projects, group and user history will be left intact"
msgstr ""
msgid "AdminUsers|Reactivating a user will:"
msgstr ""
msgid "AdminUsers|Restore user access to the account, including web, Git and API."
msgstr ""
msgid "AdminUsers|Search by name, email or username"
msgstr ""
......@@ -1207,18 +1243,42 @@ msgstr ""
msgid "AdminUsers|Sort by"
msgstr ""
msgid "AdminUsers|The user will be logged out"
msgstr ""
msgid "AdminUsers|The user will not be able to access git repositories"
msgstr ""
msgid "AdminUsers|The user will not be able to access the API"
msgstr ""
msgid "AdminUsers|The user will not receive any notifications"
msgstr ""
msgid "AdminUsers|To confirm, type %{projectName}"
msgstr ""
msgid "AdminUsers|To confirm, type %{username}"
msgstr ""
msgid "AdminUsers|User will be blocked"
msgid "AdminUsers|User will not be able to access git repositories"
msgstr ""
msgid "AdminUsers|User will not be able to login"
msgstr ""
msgid "AdminUsers|When the user logs back in, their account will reactivate as a fully active account"
msgstr ""
msgid "AdminUsers|Without projects"
msgstr ""
msgid "AdminUsers|You are about to permanently delete the user %{username}. Issues, merge requests, and groups linked to them will be transferred to a system-wide \"Ghost-user\". To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered."
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 %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered."
msgstr ""
msgid "Advanced"
msgstr ""
......@@ -1796,9 +1856,6 @@ msgstr ""
msgid "Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. <strong>The repository cannot be committed to, and no issues, comments or other entities can be created.</strong>"
msgstr ""
msgid "Are you sure"
msgstr ""
msgid "Are you sure that you want to archive this project?"
msgstr ""
......@@ -2367,9 +2424,6 @@ msgstr ""
msgid "Bitbucket import"
msgstr ""
msgid "Block"
msgstr ""
msgid "Blocked"
msgstr ""
......@@ -6273,6 +6327,12 @@ msgstr ""
msgid "Error occurred while updating the issue weight"
msgstr ""
msgid "Error occurred. A blocked user cannot be deactivated"
msgstr ""
msgid "Error occurred. A blocked user must be unblocked to be activated"
msgstr ""
msgid "Error occurred. User was not blocked"
msgstr ""
......@@ -15526,12 +15586,18 @@ msgstr ""
msgid "Subtracts"
msgstr ""
msgid "Successfully activated"
msgstr ""
msgid "Successfully blocked"
msgstr ""
msgid "Successfully confirmed"
msgstr ""
msgid "Successfully deactivated"
msgstr ""
msgid "Successfully deleted U2F device."
msgstr ""
......@@ -16128,6 +16194,9 @@ msgstr ""
msgid "The user map is a mapping of the FogBugz users that participated on your projects to the way their email address and usernames will be imported into GitLab. You can change this by populating the table below."
msgstr ""
msgid "The user you are trying to deactivate has been active in the past %{minimum_inactive_days} days and cannot be deactivated"
msgstr ""
msgid "The user-facing URL of the Geo node"
msgstr ""
......@@ -18175,6 +18244,9 @@ msgstr ""
msgid "Weight %{weight}"
msgstr ""
msgid "Welcome back! Your account had been deactivated due to inactivity but is now reactivated."
msgstr ""
msgid "Welcome to GitLab"
msgstr ""
......@@ -18792,6 +18864,9 @@ msgstr ""
msgid "Your access request to the %{source_type} has been withdrawn."
msgstr ""
msgid "Your account has been deactivated by your administrator. Please log back in to reactivate your account."
msgstr ""
msgid "Your account uses dedicated credentials for the \"%{group_name}\" group and can only be updated through SSO."
msgstr ""
......
......@@ -60,6 +60,96 @@ describe Admin::UsersController do
end
end
describe 'PUT #activate' do
shared_examples 'a request that activates the user' do
it 'activates the user' do
put :activate, params: { id: user.username }
user.reload
expect(user.active?).to be_truthy
expect(flash[:notice]).to eq('Successfully activated')
end
end
context 'for a deactivated user' do
before do
user.deactivate
end
it_behaves_like 'a request that activates the user'
end
context 'for an active user' do
it_behaves_like 'a request that activates the user'
end
context 'for a blocked user' do
before do
user.block
end
it 'does not activate the user' do
put :activate, params: { id: user.username }
user.reload
expect(user.active?).to be_falsey
expect(flash[:notice]).to eq('Error occurred. A blocked user must be unblocked to be activated')
end
end
end
describe 'PUT #deactivate' do
shared_examples 'a request that deactivates the user' do
it 'deactivates the user' do
put :deactivate, params: { id: user.username }
user.reload
expect(user.deactivated?).to be_truthy
expect(flash[:notice]).to eq('Successfully deactivated')
end
end
context 'for an active user' do
let(:activity) { {} }
let(:user) { create(:user, **activity) }
context 'with no recent activity' do
let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.next.days.ago } }
it_behaves_like 'a request that deactivates the user'
end
context 'with recent activity' do
let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.pred.days.ago } }
it 'does not deactivate the user' do
put :deactivate, params: { id: user.username }
user.reload
expect(user.deactivated?).to be_falsey
expect(flash[:notice]).to eq("The user you are trying to deactivate has been active in the past 14 days and cannot be deactivated")
end
end
end
context 'for a deactivated user' do
before do
user.deactivate
end
it_behaves_like 'a request that deactivates the user'
end
context 'for a blocked user' do
before do
user.block
end
it 'does not deactivate the user' do
put :deactivate, params: { id: user.username }
user.reload
expect(user.deactivated?).to be_falsey
expect(flash[:notice]).to eq('Error occurred. A blocked user cannot be deactivated')
end
end
end
describe 'PUT block/:id' do
it 'blocks user' do
put :block, params: { id: user.username }
......
......@@ -460,6 +460,25 @@ describe ApplicationController do
end
end
context 'deactivated user' do
controller(described_class) do
def index
render html: 'authenticated'
end
end
before do
sign_in user
user.deactivate
end
it 'signs out a deactivated user' do
get :index
expect(response).to redirect_to(new_user_session_path)
expect(flash[:alert]).to eq('Your account has been deactivated by your administrator. Please log back in to reactivate your account.')
end
end
context 'terms' do
controller(described_class) do
def index
......
......@@ -18,6 +18,28 @@ describe OmniauthCallbacksController, type: :controller do
Rails.application.env_config['omniauth.auth'] = @original_env_config_omniauth_auth
end
context 'a deactivated user' do
let(:provider) { :github }
let(:extern_uid) { 'my-uid' }
before do
user.deactivate!
post provider
end
it 'allows sign in' do
expect(request.env['warden']).to be_authenticated
end
it 'activates the user' do
expect(user.reload.active?).to be_truthy
end
it 'shows reactivation flash message after logging in' do
expect(flash[:notice]).to eq('Welcome back! Your account had been deactivated due to inactivity but is now reactivated.')
end
end
context 'when the user is on the last sign in attempt' do
let(:extern_uid) { 'my-uid' }
......
......@@ -258,5 +258,26 @@ describe Projects::MergeRequests::DiffsController do
it_behaves_like 'forked project with submodules'
it_behaves_like 'persisted preferred diff view cookie'
context 'diff unfolding' do
let!(:unfoldable_diff_note) do
create(:diff_note_on_merge_request, :folded_position, project: project, noteable: merge_request)
end
let!(:diff_note) do
create(:diff_note_on_merge_request, project: project, noteable: merge_request)
end
it 'unfolds correct diff file positions' do
expect_next_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiffBatch) do |instance|
expect(instance)
.to receive(:unfold_diff_files)
.with([unfoldable_diff_note.position])
.and_call_original
end
go
end
end
end
end
......@@ -61,6 +61,25 @@ describe SessionsController do
expect(subject.current_user).to eq user
end
context 'a deactivated user' do
before do
user.deactivate!
post(:create, params: { user: user_params })
end
it 'is allowed to login' do
expect(subject.current_user).to eq user
end
it 'activates the user' do
expect(subject.current_user.active?).to be_truthy
end
it 'shows reactivation flash message after logging in' do
expect(flash[:notice]).to eq('Welcome back! Your account had been deactivated due to inactivity but is now reactivated.')
end
end
context 'with password authentication disabled' do
before do
stub_application_setting(password_authentication_enabled_for_web: false)
......
......@@ -62,6 +62,18 @@ FactoryBot.define do
)
end
trait :folded_position do
position do
Gitlab::Diff::Position.new(
old_path: "files/ruby/popen.rb",
new_path: "files/ruby/popen.rb",
old_line: 1,
new_line: 1,
diff_refs: diff_refs
)
end
end
trait :resolved do
resolved_at { Time.now }
resolved_by { create(:user) }
......
......@@ -31,7 +31,8 @@ describe "Admin::Users" do
expect(page).to have_content(current_user.last_activity_on.strftime("%e %b, %Y"))
expect(page).to have_content(user.email)
expect(page).to have_content(user.name)
expect(page).to have_link('Block', href: block_admin_user_path(user))
expect(page).to have_button('Block')
expect(page).to have_button('Deactivate')
expect(page).to have_button('Delete user')
expect(page).to have_button('Delete user and contributions')
end
......@@ -277,7 +278,8 @@ describe "Admin::Users" do
expect(page).to have_content(user.email)
expect(page).to have_content(user.name)
expect(page).to have_content(user.id)
expect(page).to have_link('Block user', href: block_admin_user_path(user))
expect(page).to have_button('Deactivate user')
expect(page).to have_button('Block user')
expect(page).to have_button('Delete user')
expect(page).to have_button('Delete user and contributions')
end
......
......@@ -3,7 +3,7 @@
require 'spec_helper'
describe UserFinder do
set(:user) { create(:user) }
let_it_be(:user) { create(:user) }
describe '#find_by_id' do
context 'when the user exists' do
......@@ -24,7 +24,7 @@ describe UserFinder do
context 'when the user does not exist' do
it 'returns nil' do
found = described_class.new(1).find_by_id
found = described_class.new(-1).find_by_id
expect(found).to be_nil
end
......@@ -84,7 +84,7 @@ describe UserFinder do
context 'when the user does not exist' do
it 'returns nil' do
found = described_class.new(1).find_by_id_or_username
found = described_class.new(-1).find_by_id_or_username
expect(found).to be_nil
end
......@@ -110,7 +110,7 @@ describe UserFinder do
context 'when the user does not exist' do
it 'raises ActiveRecord::RecordNotFound' do
finder = described_class.new(1)
finder = described_class.new(-1)
expect { finder.find_by_id! }.to raise_error(ActiveRecord::RecordNotFound)
end
......@@ -170,10 +170,32 @@ describe UserFinder do
context 'when the user does not exist' do
it 'raises ActiveRecord::RecordNotFound' do
finder = described_class.new(1)
finder = described_class.new(-1)
expect { finder.find_by_id_or_username! }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
describe '#find_by_ssh_key_id' do
let_it_be(:ssh_key) { create(:key, user: user) }
it 'returns the user when passing the ssh key id' do
found = described_class.new(ssh_key.id).find_by_ssh_key_id
expect(found).to eq(user)
end
it 'returns the user when passing the ssh key id (string)' do
found = described_class.new(ssh_key.id.to_s).find_by_ssh_key_id
expect(found).to eq(user)
end
it 'returns nil when the id does not exist' do
found = described_class.new(-1).find_by_ssh_key_id
expect(found).to be_nil
end
end
end
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`User Operation confirmation modal renders modal with form included 1`] = `
<div>
<p>
content
</p>
<p>
To confirm, type
<code>
username
</code>
</p>
<form
action="delete-url"
method="post"
>
<input
name="_method"
type="hidden"
value="delete"
/>
<input
name="authenticity_token"
type="hidden"
value="csrf"
/>
<glforminput-stub
autocomplete="off"
autofocus=""
name="username"
type="text"
value=""
/>
</form>
<glbutton-stub
variant="secondary"
>
Cancel
</glbutton-stub>
<glbutton-stub
disabled="true"
variant="warning"
>
secondaryAction
</glbutton-stub>
<glbutton-stub
disabled="true"
variant="danger"
>
action
</glbutton-stub>
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`User Operation confirmation modal renders modal with form included 1`] = `
<glmodal-stub
modalclass=""
modalid="user-operation-modal"
ok-title="action"
ok-variant="warning"
title="title"
titletag="h4"
>
<form
action="/url"
method="post"
>
<span>
content
</span>
<input
name="_method"
type="hidden"
value="method"
/>
<input
name="authenticity_token"
type="hidden"
value="csrf"
/>
</form>
</glmodal-stub>
`;
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlFormInput } from '@gitlab/ui';
import DeleteUserModal from '~/pages/admin/users/components/delete_user_modal.vue';
import ModalStub from './stubs/modal_stub';
describe('User Operation confirmation modal', () => {
let wrapper;
const findButton = variant =>
wrapper
.findAll(GlButton)
.filter(w => w.attributes('variant') === variant)
.at(0);
const createComponent = (props = {}) => {
wrapper = shallowMount(DeleteUserModal, {
propsData: {
title: 'title',
content: 'content',
action: 'action',
secondaryAction: 'secondaryAction',
deleteUserUrl: 'delete-url',
blockUserUrl: 'block-url',
username: 'username',
csrfToken: 'csrf',
...props,
},
stubs: {
GlModal: ModalStub,
},
sync: false,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders modal with form included', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it.each`
variant | prop | action
${'danger'} | ${'deleteUserUrl'} | ${'delete'}
${'warning'} | ${'blockUserUrl'} | ${'block'}
`('closing modal with $variant button triggers $action', ({ variant, prop }) => {
createComponent();
const form = wrapper.find('form');
jest.spyOn(form.element, 'submit').mockReturnValue();
const modalButton = findButton(variant);
modalButton.vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(form.element.submit).toHaveBeenCalled();
expect(form.element.action).toContain(wrapper.props(prop));
expect(new FormData(form.element).get('authenticity_token')).toEqual(
wrapper.props('csrfToken'),
);
});
});
it('disables buttons by default', () => {
createComponent();
const blockButton = findButton('warning');
const deleteButton = findButton('danger');
expect(blockButton.attributes().disabled).toBeTruthy();
expect(deleteButton.attributes().disabled).toBeTruthy();
});
it('enables button when username is typed', () => {
createComponent({
username: 'some-username',
});
wrapper.find(GlFormInput).vm.$emit('input', 'some-username');
const blockButton = findButton('warning');
const deleteButton = findButton('danger');
return wrapper.vm.$nextTick().then(() => {
expect(blockButton.attributes().disabled).toBeFalsy();
expect(deleteButton.attributes().disabled).toBeFalsy();
});
});
});
const ModalStub = {
inheritAttrs: false,
name: 'glmodal-stub',
data() {
return {
showWasCalled: false,
};
},
methods: {
show() {
this.showWasCalled = true;
},
hide() {},
},
render(h) {
const children = [this.$slots.default, this.$slots['modal-footer']]
.filter(Boolean)
.reduce((acc, nodes) => acc.concat(nodes), []);
return h('div', children);
},
};
export default ModalStub;
import { shallowMount } from '@vue/test-utils';
import UserModalManager from '~/pages/admin/users/components/user_modal_manager.vue';
import ModalStub from './stubs/modal_stub';
describe('Users admin page Modal Manager', () => {
const modalConfiguration = {
action1: {
title: 'action1',
content: 'Action Modal 1',
},
action2: {
title: 'action2',
content: 'Action Modal 2',
},
};
const actionModals = {
action1: ModalStub,
action2: ModalStub,
};
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(UserModalManager, {
propsData: {
actionModals,
modalConfiguration,
csrfToken: 'dummyCSRF',
...props,
},
stubs: {
dummyComponent1: true,
dummyComponent2: true,
},
sync: false,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('render behavior', () => {
it('does not renders modal when initialized', () => {
createComponent();
expect(wrapper.find({ ref: 'modal' }).exists()).toBeFalsy();
});
it('throws if non-existing action is requested', () => {
createComponent();
expect(() => wrapper.vm.show({ glModalAction: 'non-existing' })).toThrow();
});
it('throws if action has no proper configuration', () => {
createComponent({
modalConfiguration: {},
});
expect(() => wrapper.vm.show({ glModalAction: 'action1' })).toThrow();
});
it('renders modal with expected props when valid configuration is passed', () => {
createComponent();
wrapper.vm.show({
glModalAction: 'action1',
extraProp: 'extraPropValue',
});
return wrapper.vm.$nextTick().then(() => {
const modal = wrapper.find({ ref: 'modal' });
expect(modal.exists()).toBeTruthy();
expect(modal.vm.$attrs.csrfToken).toEqual('dummyCSRF');
expect(modal.vm.$attrs.extraProp).toEqual('extraPropValue');
expect(modal.vm.showWasCalled).toBeTruthy();
});
});
});
describe('global listener', () => {
beforeEach(() => {
jest.spyOn(document, 'addEventListener');
jest.spyOn(document, 'removeEventListener');
});
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
jest.restoreAllMocks();
});
it('registers global listener on mount', () => {
createComponent();
expect(document.addEventListener).toHaveBeenCalledWith('click', expect.any(Function));
});
it('removes global listener on destroy', () => {
createComponent();
wrapper.destroy();
expect(document.removeEventListener).toHaveBeenCalledWith('click', expect.any(Function));
});
});
describe('click handling', () => {
let node;
beforeEach(() => {
node = document.createElement('div');
document.body.appendChild(node);
});
afterEach(() => {
node.remove();
node = null;
});
it('ignores wrong clicks', () => {
createComponent();
const event = new window.MouseEvent('click', {
bubbles: true,
cancellable: true,
});
jest.spyOn(event, 'preventDefault');
node.dispatchEvent(event);
expect(event.preventDefault).not.toHaveBeenCalled();
});
it('captures click with glModalAction', () => {
createComponent();
node.dataset.glModalAction = 'action1';
const event = new window.MouseEvent('click', {
bubbles: true,
cancellable: true,
});
jest.spyOn(event, 'preventDefault');
node.dispatchEvent(event);
expect(event.preventDefault).toHaveBeenCalled();
return wrapper.vm.$nextTick().then(() => {
const modal = wrapper.find({ ref: 'modal' });
expect(modal.exists()).toBeTruthy();
expect(modal.vm.showWasCalled).toBeTruthy();
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import UserOperationConfirmationModal from '~/pages/admin/users/components/user_operation_confirmation_modal.vue';
describe('User Operation confirmation modal', () => {
let wrapper;
const createComponent = (props = {}) => {
wrapper = shallowMount(UserOperationConfirmationModal, {
propsData: {
title: 'title',
content: 'content',
action: 'action',
url: '/url',
username: 'username',
csrfToken: 'csrf',
method: 'method',
...props,
},
sync: false,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('renders modal with form included', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('closing modal with ok button triggers form submit', () => {
createComponent();
const form = wrapper.find('form');
jest.spyOn(form.element, 'submit').mockReturnValue();
wrapper.find(GlModal).vm.$emit('ok');
return wrapper.vm.$nextTick().then(() => {
expect(form.element.submit).toHaveBeenCalled();
expect(form.element.action).toContain(wrapper.props('url'));
expect(new FormData(form.element).get('authenticity_token')).toEqual(
wrapper.props('csrfToken'),
);
});
});
});
......@@ -33,5 +33,13 @@ describe Gitlab::Auth::UserAccessDeniedReason do
it { is_expected.to match /This action cannot be performed by internal users/ }
end
context 'when the user is deactivated' do
before do
user.deactivate!
end
it { is_expected.to eq "Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}" }
end
end
end
......@@ -520,6 +520,12 @@ describe Gitlab::Auth do
end
end
it 'finds the user in deactivated state' do
user.deactivate!
expect( gl_auth.find_with_user_password(username, password) ).to eql user
end
it "does not find user in blocked state" do
user.block
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Diff::PositionCollection do
let(:merge_request) { build(:merge_request) }
def build_text_position(attrs = {})
attributes = {
old_path: "files/ruby/popen.rb",
new_path: "files/ruby/popen.rb",
old_line: nil,
new_line: 14,
diff_refs: merge_request.diff_refs
}.merge(attrs)
Gitlab::Diff::Position.new(attributes)
end
def build_image_position(attrs = {})
attributes = {
old_path: "files/images/any_image.png",
new_path: "files/images/any_image.png",
width: 10,
height: 10,
x: 1,
y: 1,
diff_refs: merge_request.diff_refs,
position_type: "image"
}.merge(attrs)
Gitlab::Diff::Position.new(attributes)
end
let(:text_position) { build_text_position }
let(:folded_text_position) { build_text_position(old_line: 1, new_line: 1) }
let(:image_position) { build_image_position }
let(:head_sha) { merge_request.diff_head_sha }
let(:collection) do
described_class.new([text_position, folded_text_position, image_position], head_sha)
end
describe '#to_a' do
it 'returns all positions' do
expect(collection.to_a).to eq([text_position, folded_text_position, image_position])
end
end
describe '#unfoldable' do
it 'returns unfoldable diff positions' do
expect(collection.unfoldable).to eq([folded_text_position])
end
context 'when given head_sha does not match with positions head_sha' do
let(:head_sha) { 'unknown' }
it 'returns no position' do
expect(collection.unfoldable).to be_empty
end
end
end
describe '#concat' do
let(:new_text_position) { build_text_position(old_line: 1, new_line: 1) }
it 'returns a Gitlab::Diff::Position' do
expect(collection.concat([new_text_position])).to be_a(described_class)
end
it 'concatenates the new position to the collection' do
collection.concat([new_text_position])
expect(collection.to_a).to eq([text_position, folded_text_position, image_position, new_text_position])
end
end
end
......@@ -541,6 +541,13 @@ describe Gitlab::GitAccess do
expect { pull_access_check }.to raise_unauthorized('Your account has been blocked.')
end
it 'disallows deactivated users to pull' do
project.add_maintainer(user)
user.deactivate!
expect { pull_access_check }.to raise_unauthorized("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}")
end
context 'when the project repository does not exist' do
it 'returns not found' do
project.add_guest(user)
......@@ -925,6 +932,12 @@ describe Gitlab::GitAccess do
project.add_developer(user)
end
it 'does not allow deactivated users to push' do
user.deactivate!
expect { push_access_check }.to raise_unauthorized("Your account has been deactivated by your administrator. Please log back in from a web browser to reactivate your account at #{Gitlab.config.gitlab.url}")
end
it 'cleans up the files' do
expect(project.repository).to receive(:clean_stale_repository_files).and_call_original
expect { push_access_check }.not_to raise_error
......
......@@ -650,6 +650,46 @@ describe MergeRequest do
end
end
describe '#note_positions_for_paths' do
let(:merge_request) { create(:merge_request, :with_diffs) }
let(:project) { merge_request.project }
let!(:diff_note) do
create(:diff_note_on_merge_request, project: project, noteable: merge_request)
end
let(:file_paths) { merge_request.diffs.diff_files.map(&:file_path) }
subject do
merge_request.note_positions_for_paths(file_paths)
end
it 'returns a Gitlab::Diff::PositionCollection' do
expect(subject).to be_a(Gitlab::Diff::PositionCollection)
end
context 'within all diff files' do
it 'returns correct positions' do
expect(subject).to match_array([diff_note.position])
end
end
context 'within specific diff file' do
let(:file_paths) { [diff_note.position.file_path] }
it 'returns correct positions' do
expect(subject).to match_array([diff_note.position])
end
end
context 'within no diff files' do
let(:file_paths) { [] }
it 'returns no positions' do
expect(subject.to_a).to be_empty
end
end
end
describe '#discussions_diffs' do
let(:merge_request) { create(:merge_request) }
......
......@@ -1120,6 +1120,30 @@ describe User do
end
end
describe 'deactivating a user' do
let(:user) { create(:user, name: 'John Smith') }
context "an active user" do
it "can be deactivated" do
user.deactivate
expect(user.deactivated?).to be_truthy
end
end
context "a user who is blocked" do
before do
user.block
end
it "cannot be deactivated" do
user.deactivate
expect(user.reload.deactivated?).to be_falsy
end
end
end
describe '.filter_items' do
let(:user) { double }
......@@ -1141,6 +1165,12 @@ describe User do
expect(described_class.filter_items('blocked')).to include user
end
it 'filters by deactivated' do
expect(described_class).to receive(:deactivated).and_return([user])
expect(described_class.filter_items('deactivated')).to include user
end
it 'filters by two_factor_disabled' do
expect(described_class).to receive(:without_two_factor).and_return([user])
......@@ -1524,15 +1554,22 @@ describe User do
end
describe '.find_by_ssh_key_id' do
context 'using an existing SSH key ID' do
let(:user) { create(:user) }
let(:key) { create(:key, user: user) }
let_it_be(:user) { create(:user) }
let_it_be(:key) { create(:key, user: user) }
context 'using an existing SSH key ID' do
it 'returns the corresponding User' do
expect(described_class.find_by_ssh_key_id(key.id)).to eq(user)
end
end
it 'only performs a single query' do
key # Don't count the queries for creating the key and user
expect { described_class.find_by_ssh_key_id(key.id) }
.not_to exceed_query_limit(1)
end
context 'using an invalid SSH key ID' do
it 'returns nil' do
expect(described_class.find_by_ssh_key_id(-1)).to be_nil
......@@ -2042,6 +2079,95 @@ describe User do
end
end
describe "#last_active_at" do
let(:last_activity_on) { 5.days.ago.to_date }
let(:current_sign_in_at) { 8.days.ago }
context 'for a user that has `last_activity_on` set' do
let(:user) { create(:user, last_activity_on: last_activity_on) }
it 'returns `last_activity_on` with current time zone' do
expect(user.last_active_at).to eq(last_activity_on.to_time.in_time_zone)
end
end
context 'for a user that has `current_sign_in_at` set' do
let(:user) { create(:user, current_sign_in_at: current_sign_in_at) }
it 'returns `current_sign_in_at`' do
expect(user.last_active_at).to eq(current_sign_in_at)
end
end
context 'for a user that has both `current_sign_in_at` & ``last_activity_on`` set' do
let(:user) { create(:user, current_sign_in_at: current_sign_in_at, last_activity_on: last_activity_on) }
it 'returns the latest among `current_sign_in_at` & `last_activity_on`' do
latest_event = [current_sign_in_at, last_activity_on.to_time.in_time_zone].max
expect(user.last_active_at).to eq(latest_event)
end
end
context 'for a user that does not have both `current_sign_in_at` & `last_activity_on` set' do
let(:user) { create(:user, current_sign_in_at: nil, last_activity_on: nil) }
it 'returns nil' do
expect(user.last_active_at).to eq(nil)
end
end
end
describe "#can_be_deactivated?" do
let(:activity) { {} }
let(:user) { create(:user, name: 'John Smith', **activity) }
let(:day_within_minium_inactive_days_threshold) { User::MINIMUM_INACTIVE_DAYS.pred.days.ago }
let(:day_outside_minium_inactive_days_threshold) { User::MINIMUM_INACTIVE_DAYS.next.days.ago }
shared_examples 'not eligible for deactivation' do
it 'returns false' do
expect(user.can_be_deactivated?).to be_falsey
end
end
shared_examples 'eligible for deactivation' do
it 'returns true' do
expect(user.can_be_deactivated?).to be_truthy
end
end
context "a user who is not active" do
before do
user.block
end
it_behaves_like 'not eligible for deactivation'
end
context 'a user who has activity within the specified minimum inactive days' do
let(:activity) { { last_activity_on: day_within_minium_inactive_days_threshold } }
it_behaves_like 'not eligible for deactivation'
end
context 'a user who has signed in within the specified minimum inactive days' do
let(:activity) { { current_sign_in_at: day_within_minium_inactive_days_threshold } }
it_behaves_like 'not eligible for deactivation'
end
context 'a user who has no activity within the specified minimum inactive days' do
let(:activity) { { last_activity_on: day_outside_minium_inactive_days_threshold } }
it_behaves_like 'eligible for deactivation'
end
context 'a user who has not signed in within the specified minimum inactive days' do
let(:activity) { { current_sign_in_at: day_outside_minium_inactive_days_threshold } }
it_behaves_like 'eligible for deactivation'
end
end
describe "#contributed_projects" do
subject { create(:user) }
let!(:project1) { create(:project) }
......
......@@ -141,6 +141,40 @@ describe GlobalPolicy do
end
end
describe 'receive notifications' do
describe 'regular user' do
it { is_expected.to be_allowed(:receive_notifications) }
end
describe 'admin' do
let(:current_user) { create(:admin) }
it { is_expected.to be_allowed(:receive_notifications) }
end
describe 'anonymous' do
let(:current_user) { nil }
it { is_expected.not_to be_allowed(:receive_notifications) }
end
describe 'blocked user' do
before do
current_user.block
end
it { is_expected.not_to be_allowed(:receive_notifications) }
end
describe 'deactivated user' do
before do
current_user.deactivate
end
it { is_expected.not_to be_allowed(:receive_notifications) }
end
end
describe 'git access' do
describe 'regular user' do
it { is_expected.to be_allowed(:access_git) }
......@@ -158,6 +192,14 @@ describe GlobalPolicy do
it { is_expected.to be_allowed(:access_git) }
end
describe 'deactivated user' do
before do
current_user.deactivate
end
it { is_expected.not_to be_allowed(:access_git) }
end
context 'when terms are enforced' do
before do
enforce_terms
......
......@@ -38,21 +38,35 @@ describe 'doorkeeper access' do
end
end
describe "when user is blocked" do
it "returns authorization error" do
user.block
shared_examples 'forbidden request' do
it 'returns 403 response' do
get api("/user"), params: { access_token: token.token }
expect(response).to have_gitlab_http_status(403)
end
end
describe "when user is ldap_blocked" do
it "returns authorization error" do
context "when user is blocked" do
before do
user.block
end
it_behaves_like 'forbidden request'
end
context "when user is ldap_blocked" do
before do
user.ldap_block
get api("/user"), params: { access_token: token.token }
end
expect(response).to have_gitlab_http_status(403)
it_behaves_like 'forbidden request'
end
context "when user is deactivated" do
before do
user.deactivate
end
it_behaves_like 'forbidden request'
end
end
......@@ -237,14 +237,6 @@ describe API::Internal::Base do
expect(json_response['name']).to eq(user.name)
end
it "finds a user by user id" do
get(api("/internal/discover"), params: { user_id: user.id, secret_token: secret_token })
expect(response).to have_gitlab_http_status(200)
expect(json_response['name']).to eq(user.name)
end
it "finds a user by username" do
get(api("/internal/discover"), params: { username: user.username, secret_token: secret_token })
......
......@@ -1846,6 +1846,182 @@ describe API::Users do
end
end
context 'activate and deactivate' do
shared_examples '404' do
it 'returns 404' do
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
end
describe 'POST /users/:id/activate' do
context 'performed by a non-admin user' do
it 'is not authorized to perform the action' do
post api("/users/#{user.id}/activate", user)
expect(response).to have_gitlab_http_status(403)
end
end
context 'performed by an admin user' do
context 'for a deactivated user' do
before do
user.deactivate
post api("/users/#{user.id}/activate", admin)
end
it 'activates a deactivated user' do
expect(response).to have_gitlab_http_status(201)
expect(user.reload.state).to eq('active')
end
end
context 'for an active user' do
before do
user.activate
post api("/users/#{user.id}/activate", admin)
end
it 'returns 201' do
expect(response).to have_gitlab_http_status(201)
expect(user.reload.state).to eq('active')
end
end
context 'for a blocked user' do
before do
user.block
post api("/users/#{user.id}/activate", admin)
end
it 'returns 403' do
expect(response).to have_gitlab_http_status(403)
expect(json_response['message']).to eq('403 Forbidden - A blocked user must be unblocked to be activated')
expect(user.reload.state).to eq('blocked')
end
end
context 'for a ldap blocked user' do
before do
user.ldap_block
post api("/users/#{user.id}/activate", admin)
end
it 'returns 403' do
expect(response).to have_gitlab_http_status(403)
expect(json_response['message']).to eq('403 Forbidden - A blocked user must be unblocked to be activated')
expect(user.reload.state).to eq('ldap_blocked')
end
end
context 'for a user that does not exist' do
before do
post api("/users/0/activate", admin)
end
it_behaves_like '404'
end
end
end
describe 'POST /users/:id/deactivate' do
context 'performed by a non-admin user' do
it 'is not authorized to perform the action' do
post api("/users/#{user.id}/deactivate", user)
expect(response).to have_gitlab_http_status(403)
end
end
context 'performed by an admin user' do
context 'for an active user' do
let(:activity) { {} }
let(:user) { create(:user, username: 'user.with.dot', **activity) }
context 'with no recent activity' do
let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.next.days.ago } }
before do
post api("/users/#{user.id}/deactivate", admin)
end
it 'deactivates an active user' do
expect(response).to have_gitlab_http_status(201)
expect(user.reload.state).to eq('deactivated')
end
end
context 'with recent activity' do
let(:activity) { { last_activity_on: ::User::MINIMUM_INACTIVE_DAYS.pred.days.ago } }
before do
post api("/users/#{user.id}/deactivate", admin)
end
it 'does not deactivate an active user' do
expect(response).to have_gitlab_http_status(403)
expect(json_response['message']).to eq("403 Forbidden - The user you are trying to deactivate has been active in the past #{::User::MINIMUM_INACTIVE_DAYS} days and cannot be deactivated")
expect(user.reload.state).to eq('active')
end
end
end
context 'for a deactivated user' do
before do
user.deactivate
post api("/users/#{user.id}/deactivate", admin)
end
it 'returns 201' do
expect(response).to have_gitlab_http_status(201)
expect(user.reload.state).to eq('deactivated')
end
end
context 'for a blocked user' do
before do
user.block
post api("/users/#{user.id}/deactivate", admin)
end
it 'returns 403' do
expect(response).to have_gitlab_http_status(403)
expect(json_response['message']).to eq('403 Forbidden - A blocked user cannot be deactivated by the API')
expect(user.reload.state).to eq('blocked')
end
end
context 'for a ldap blocked user' do
before do
user.ldap_block
post api("/users/#{user.id}/deactivate", admin)
end
it 'returns 403' do
expect(response).to have_gitlab_http_status(403)
expect(json_response['message']).to eq('403 Forbidden - A blocked user cannot be deactivated by the API')
expect(user.reload.state).to eq('ldap_blocked')
end
end
context 'for a user that does not exist' do
before do
post api("/users/0/deactivate", admin)
end
it_behaves_like '404'
end
end
end
end
describe 'POST /users/:id/block' do
before do
admin
......@@ -1878,6 +2054,7 @@ describe API::Users do
describe 'POST /users/:id/unblock' do
let(:blocked_user) { create(:user, state: 'blocked') }
let(:deactivated_user) { create(:user, state: 'deactivated') }
before do
admin
......@@ -1901,7 +2078,13 @@ describe API::Users do
expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
it 'does not be available for non admin users' do
it 'does not unblock deactivated users' do
post api("/users/#{deactivated_user.id}/unblock", admin)
expect(response).to have_gitlab_http_status(403)
expect(deactivated_user.reload.state).to eq('deactivated')
end
it 'is not available for non admin users' do
post api("/users/#{user.id}/unblock", user)
expect(response).to have_gitlab_http_status(403)
expect(user.reload.state).to eq('active')
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment