Commit 71f0565c authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent da94c16ec383
<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 @@ def impersonate
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 @@ def check_password_expiration
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 @@ def sign_in_user_flow(auth_user_class)
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 @@ def diffs_batch
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 @@ def create
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 @@ def find_by_id_or_username!
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 PipelineDelegator
: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 @@ def non_latest_diffs
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 @@ def grouped_diff_discussions(diff_refs = nil)
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 @@ def update_tracked_fields!(request)
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