Skip to content
Snippets Groups Projects
Commit 308146dc authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 4b28d5ae
No related branches found
No related tags found
No related merge requests found
Showing
with 398 additions and 116 deletions
<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() {
Loading
Loading
@@ -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>',
Loading
Loading
@@ -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');
},
Loading
Loading
@@ -97,8 +81,12 @@ export default {
},
},
methods: {
show() {
this.$refs.modal.show();
},
onCancel() {
this.enteredUsername = '';
this.$refs.modal.hide();
},
onSecondaryAction() {
const { form } = this.$refs;
Loading
Loading
@@ -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;
}
});
});
Loading
Loading
@@ -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"))
Loading
Loading
Loading
Loading
@@ -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
 
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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,
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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)
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
@@ -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
Loading
Loading
@@ -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')) }
Loading
Loading
@@ -431,6 +441,8 @@ class User < ApplicationRecord
without_projects
when 'external'
external
when 'deactivated'
deactivated
else
active
end
Loading
Loading
@@ -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)
Loading
Loading
@@ -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
 
Loading
Loading
@@ -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')
Loading
Loading
@@ -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? }
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
#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.')
Loading
Loading
@@ -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?') }
Loading
Loading
@@ -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'
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment