Commit d3f6049e authored by Jiaan Louw's avatar Jiaan Louw Committed by Paul Slaughter

Prevent admin delete user form bypass

**How?**
The native form submission is now prevented

**Also:**
The corresponding spec file was refactored for readability and strength

https://gitlab.com/gitlab-org/gitlab/merge_requests/17343
parent a2f62b53
...@@ -109,7 +109,7 @@ export default { ...@@ -109,7 +109,7 @@ export default {
<template> <template>
<p v-html="text"></p> <p v-html="text"></p>
<p v-html="confirmationTextLabel"></p> <p v-html="confirmationTextLabel"></p>
<form ref="form" :action="deleteUserUrl" method="post"> <form ref="form" :action="deleteUserUrl" method="post" @submit.prevent>
<input ref="method" type="hidden" name="_method" value="delete" /> <input ref="method" type="hidden" name="_method" value="delete" />
<input :value="csrfToken" type="hidden" name="authenticity_token" /> <input :value="csrfToken" type="hidden" name="authenticity_token" />
<gl-form-input <gl-form-input
......
---
title: Fix delete user dialog bypass caused by hitting enter
merge_request: 17343
author:
type: fixed
...@@ -3,26 +3,46 @@ import { GlButton, GlFormInput } from '@gitlab/ui'; ...@@ -3,26 +3,46 @@ import { GlButton, GlFormInput } from '@gitlab/ui';
import DeleteUserModal from '~/pages/admin/users/components/delete_user_modal.vue'; import DeleteUserModal from '~/pages/admin/users/components/delete_user_modal.vue';
import ModalStub from './stubs/modal_stub'; import ModalStub from './stubs/modal_stub';
const TEST_DELETE_USER_URL = 'delete-url';
const TEST_BLOCK_USER_URL = 'block-url';
const TEST_CSRF = 'csrf';
describe('User Operation confirmation modal', () => { describe('User Operation confirmation modal', () => {
let wrapper; let wrapper;
let formSubmitSpy;
const findButton = variant => const findButton = variant =>
wrapper wrapper
.findAll(GlButton) .findAll(GlButton)
.filter(w => w.attributes('variant') === variant) .filter(w => w.attributes('variant') === variant)
.at(0); .at(0);
const findForm = () => wrapper.find('form');
const findUsernameInput = () => wrapper.find(GlFormInput);
const findPrimaryButton = () => findButton('danger');
const findSecondaryButton = () => findButton('warning');
const findAuthenticityToken = () => new FormData(findForm().element).get('authenticity_token');
const getUsername = () => findUsernameInput().attributes('value');
const getMethodParam = () => new FormData(findForm().element).get('_method');
const getFormAction = () => findForm().attributes('action');
const setUsername = username => {
findUsernameInput().vm.$emit('input', username);
};
const username = 'username';
const badUsername = 'bad_username';
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
wrapper = shallowMount(DeleteUserModal, { wrapper = shallowMount(DeleteUserModal, {
propsData: { propsData: {
username,
title: 'title', title: 'title',
content: 'content', content: 'content',
action: 'action', action: 'action',
secondaryAction: 'secondaryAction', secondaryAction: 'secondaryAction',
deleteUserUrl: 'delete-url', deleteUserUrl: TEST_DELETE_USER_URL,
blockUserUrl: 'block-url', blockUserUrl: TEST_BLOCK_USER_URL,
username: 'username', csrfToken: TEST_CSRF,
csrfToken: 'csrf',
...props, ...props,
}, },
stubs: { stubs: {
...@@ -32,6 +52,10 @@ describe('User Operation confirmation modal', () => { ...@@ -32,6 +52,10 @@ describe('User Operation confirmation modal', () => {
}); });
}; };
beforeEach(() => {
formSubmitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit').mockImplementation();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null; wrapper = null;
...@@ -42,44 +66,84 @@ describe('User Operation confirmation modal', () => { ...@@ -42,44 +66,84 @@ describe('User Operation confirmation modal', () => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
it.each` describe('on created', () => {
variant | prop | action beforeEach(() => {
${'danger'} | ${'deleteUserUrl'} | ${'delete'} createComponent();
${'warning'} | ${'blockUserUrl'} | ${'block'} });
`('closing modal with $variant button triggers $action', ({ variant, prop }) => {
createComponent(); it('has disabled buttons', () => {
const form = wrapper.find('form'); expect(findPrimaryButton().attributes('disabled')).toBeTruthy();
jest.spyOn(form.element, 'submit').mockReturnValue(); expect(findSecondaryButton().attributes('disabled')).toBeTruthy();
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', () => { describe('with incorrect username', () => {
createComponent(); beforeEach(() => {
const blockButton = findButton('warning'); createComponent();
const deleteButton = findButton('danger'); setUsername(badUsername);
expect(blockButton.attributes().disabled).toBeTruthy();
expect(deleteButton.attributes().disabled).toBeTruthy(); return wrapper.vm.$nextTick();
});
it('shows incorrect username', () => {
expect(getUsername()).toEqual(badUsername);
});
it('has disabled buttons', () => {
expect(findPrimaryButton().attributes('disabled')).toBeTruthy();
expect(findSecondaryButton().attributes('disabled')).toBeTruthy();
});
}); });
it('enables button when username is typed', () => { describe('with correct username', () => {
createComponent({ beforeEach(() => {
username: 'some-username', createComponent();
setUsername(username);
return wrapper.vm.$nextTick();
});
it('shows correct username', () => {
expect(getUsername()).toEqual(username);
});
it('has enabled buttons', () => {
expect(findPrimaryButton().attributes('disabled')).toBeFalsy();
expect(findSecondaryButton().attributes('disabled')).toBeFalsy();
}); });
wrapper.find(GlFormInput).vm.$emit('input', 'some-username');
const blockButton = findButton('warning');
const deleteButton = findButton('danger');
return wrapper.vm.$nextTick().then(() => { describe('when primary action is submitted', () => {
expect(blockButton.attributes().disabled).toBeFalsy(); beforeEach(() => {
expect(deleteButton.attributes().disabled).toBeFalsy(); findPrimaryButton().vm.$emit('click');
return wrapper.vm.$nextTick();
});
it('clears the input', () => {
expect(getUsername()).toEqual('');
});
it('has correct form attributes and calls submit', () => {
expect(getFormAction()).toBe(TEST_DELETE_USER_URL);
expect(getMethodParam()).toBe('delete');
expect(findAuthenticityToken()).toBe(TEST_CSRF);
expect(formSubmitSpy).toHaveBeenCalled();
});
});
describe('when secondary action is submitted', () => {
beforeEach(() => {
findSecondaryButton().vm.$emit('click');
return wrapper.vm.$nextTick();
});
it('has correct form attributes and calls submit', () => {
expect(getFormAction()).toBe(TEST_BLOCK_USER_URL);
expect(getMethodParam()).toBe('put');
expect(findAuthenticityToken()).toBe(TEST_CSRF);
expect(formSubmitSpy).toHaveBeenCalled();
});
}); });
}); });
}); });
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