Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-12 21:11:43 +00:00
parent 60eaf3d906
commit 054378fd4a
44 changed files with 292 additions and 116 deletions

View File

@ -10,6 +10,21 @@ const KEY_WEB_IDE = 'webide';
const KEY_GITPOD = 'gitpod';
const KEY_PIPELINE_EDITOR = 'pipeline_editor';
export const i18n = {
modal: {
title: __('Enable Gitpod?'),
content: s__(
'Gitpod|To use Gitpod you must first enable the feature in the integrations section of your %{linkStart}user preferences%{linkEnd}.',
),
actionCancelText: __('Cancel'),
actionPrimaryText: __('Enable Gitpod'),
},
webIdeText: s__('WebIDE|Quickly and easily edit multiple files in your project.'),
webIdeTooltip: s__(
'WebIDE|Quickly and easily edit multiple files in your project. Press . to open',
),
};
export default {
components: {
ActionsButton,
@ -19,16 +34,7 @@ export default {
GlLink,
ConfirmForkModal,
},
i18n: {
modal: {
title: __('Enable Gitpod?'),
content: s__(
'Gitpod|To use Gitpod you must first enable the feature in the integrations section of your %{linkStart}user preferences%{linkEnd}.',
),
actionCancelText: __('Cancel'),
actionPrimaryText: __('Enable Gitpod'),
},
},
i18n,
props: {
isFork: {
type: Boolean,
@ -207,8 +213,8 @@ export default {
return {
key: KEY_WEB_IDE,
text: this.webIdeActionText,
secondaryText: __('Quickly and easily edit multiple files in your project.'),
tooltip: '',
secondaryText: this.$options.i18n.webIdeText,
tooltip: this.$options.i18n.webIdeTooltip,
attrs: {
'data-qa-selector': 'web_ide_button',
'data-track-action': 'click_consolidated_edit_ide',

View File

@ -16,4 +16,14 @@ class OauthAccessToken < Doorkeeper::AccessToken
super
end
end
# this method overrides a shortcoming upstream, more context:
# https://gitlab.com/gitlab-org/gitlab/-/issues/367888
def self.find_by_fallback_token(attr, plain_secret)
return unless fallback_secret_strategy && fallback_secret_strategy == Doorkeeper::SecretStoring::Plain
# token is hashed, don't allow plaintext comparison
return if plain_secret.starts_with?("$")
super
end
end

View File

@ -0,0 +1,8 @@
---
name: hash_oauth_tokens
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91501
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/367570
milestone: '15.3'
type: development
group: group::authentication and authorization
default_enabled: false

View File

@ -90,6 +90,8 @@ Doorkeeper.configure do
# Check out the wiki for more information on customization
access_token_methods :from_access_token_param, :from_bearer_authorization, :from_bearer_param
hash_token_secrets using: '::Gitlab::DoorkeeperSecretStoring::Pbkdf2Sha512', fallback: :plain
# Specify what grant flows are enabled in array of Strings. The valid
# strings and the flows they enable are:
#

View File

@ -81,6 +81,36 @@ This requires you to either:
### Documentation link tests
Merge requests containing changes to Markdown (`.md`) files run a `docs-lint links`
job, which runs two types of link checks. In both cases, links with destinations
that begin with `http` or `https` are considered external links, and skipped:
- `bundle exec nanoc check internal_links`: Tests links to internal pages.
- `bundle exec nanoc check internal_anchors`: Tests links to subheadings (anchors) on internal pages.
Failures from these tests are displayed at the end of the test results in the **Issues found!** area.
For example, failures in the `internal_anchors` test follow this format:
```plaintext
[ ERROR ] internal_anchors - Broken anchor detected!
- source file `/tmp/gitlab-docs/public/ee/user/application_security/api_fuzzing/index.html`
- destination `/tmp/gitlab-docs/public/ee/development/code_review.html`
- link `../../../development/code_review.html#review-response-slo`
- anchor `#review-response-slo`
```
- **Source file**: The full path to the file containing the error. To find the
file in the `gitlab` repository, replace `/tmp/gitlab-docs/public/ee` with `doc`, and `.html` with `.md`.
- **Destination**: The full path to the file not found by the test. To find the
file in the `gitlab` repository, replace `/tmp/gitlab-docs/public/ee` with `doc`, and `.html` with `.md`.
- **Link**: The actual link the script attempted to find.
- **Anchor**: If present, the subheading (anchor) the script attempted to find.
Check for multiple instances of the same broken link on each page reporting an error.
Even if a specific broken link appears multiple times on a page, the test reports it only once.
#### Run document link tests locally
To execute documentation link tests locally:
1. Navigate to the [`gitlab-docs`](https://gitlab.com/gitlab-org/gitlab-docs) directory.

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
module Gitlab
module DoorkeeperSecretStoring
class Pbkdf2Sha512 < ::Doorkeeper::SecretStoring::Base
STRETCHES = 20_000
# An empty salt is used because we need to look tokens up solely by
# their hashed value. Additionally, tokens are always cryptographically
# pseudo-random and unique, therefore salting provides no
# additional security.
SALT = ''
def self.transform_secret(plain_secret)
return plain_secret unless Feature.enabled?(:hash_oauth_tokens)
Devise::Pbkdf2Encryptable::Encryptors::Pbkdf2Sha512.digest(plain_secret, STRETCHES, SALT)
end
##
# Determines whether this strategy supports restoring
# secrets from the database. This allows detecting users
# trying to use a non-restorable strategy with +reuse_access_tokens+.
def self.allows_restoring_secrets?
false
end
end
end
end

View File

@ -32068,9 +32068,6 @@ msgstr ""
msgid "Quick range"
msgstr ""
msgid "Quickly and easily edit multiple files in your project."
msgstr ""
msgid "Quota of CI/CD minutes"
msgstr ""
@ -43720,6 +43717,12 @@ msgstr ""
msgid "WebIDE|Merge request"
msgstr ""
msgid "WebIDE|Quickly and easily edit multiple files in your project."
msgstr ""
msgid "WebIDE|Quickly and easily edit multiple files in your project. Press . to open"
msgstr ""
msgid "WebIDE|This project does not accept unsigned commits."
msgstr ""

View File

@ -71,7 +71,7 @@ describe('Blob Header Default Actions', () => {
});
buttons = wrapper.findAllComponents(GlButton);
expect(buttons.at(0).attributes('disabled')).toBeTruthy();
expect(buttons.at(0).attributes('disabled')).toBe('true');
});
it('does not render the copy button if a rendering error is set', () => {

View File

@ -69,7 +69,7 @@ describe('Sketch viewer', () => {
const img = document.querySelector('#js-sketch-viewer img');
expect(img).not.toBeNull();
expect(img.classList.contains('img-fluid')).toBeTruthy();
expect(img.classList.contains('img-fluid')).toBe(true);
});
it('renders link to image', () => {

View File

@ -6,8 +6,8 @@ import BatchDeleteButton from '~/design_management/components/delete_button.vue'
describe('Batch delete button component', () => {
let wrapper;
const findButton = () => wrapper.find(GlButton);
const findModal = () => wrapper.find(GlModal);
const findButton = () => wrapper.findComponent(GlButton);
const findModal = () => wrapper.findComponent(GlModal);
function createComponent({ isDeleting = false } = {}, { slots = {} } = {}) {
wrapper = shallowMount(BatchDeleteButton, {

View File

@ -26,13 +26,13 @@ describe('Design discussions component', () => {
const originalGon = window.gon;
let wrapper;
const findDesignNotes = () => wrapper.findAll(DesignNote);
const findReplyPlaceholder = () => wrapper.find(ReplyPlaceholder);
const findReplyForm = () => wrapper.find(DesignReplyForm);
const findRepliesWidget = () => wrapper.find(ToggleRepliesWidget);
const findDesignNotes = () => wrapper.findAllComponents(DesignNote);
const findReplyPlaceholder = () => wrapper.findComponent(ReplyPlaceholder);
const findReplyForm = () => wrapper.findComponent(DesignReplyForm);
const findRepliesWidget = () => wrapper.findComponent(ToggleRepliesWidget);
const findResolveButton = () => wrapper.find('[data-testid="resolve-button"]');
const findResolvedMessage = () => wrapper.find('[data-testid="resolved-message"]');
const findResolveLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findResolveLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findResolveCheckbox = () => wrapper.find('[data-testid="resolve-checkbox"]');
const findApolloMutation = () => wrapper.findComponent(ApolloMutation);
@ -307,7 +307,7 @@ describe('Design discussions component', () => {
expect(
wrapper
.findAll(DesignNote)
.findAllComponents(DesignNote)
.wrappers.every((designNote) => designNote.classes('gl-bg-blue-50')),
).toBe(true);
},

View File

@ -100,7 +100,7 @@ describe('Design note component', () => {
note,
});
expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true);
expect(wrapper.findComponent(TimeAgoTooltip).exists()).toBe(true);
});
it('should not render edit icon when user does not have a permission', () => {

View File

@ -15,9 +15,9 @@ describe('Design reply form component', () => {
let wrapper;
const findTextarea = () => wrapper.find('textarea');
const findSubmitButton = () => wrapper.find({ ref: 'submitButton' });
const findCancelButton = () => wrapper.find({ ref: 'cancelButton' });
const findModal = () => wrapper.find({ ref: 'cancelCommentModal' });
const findSubmitButton = () => wrapper.findComponent({ ref: 'submitButton' });
const findCancelButton = () => wrapper.findComponent({ ref: 'cancelButton' });
const findModal = () => wrapper.findComponent({ ref: 'cancelCommentModal' });
function createComponent(props = {}, mountOptions = {}) {
wrapper = mount(DesignReplyForm, {

View File

@ -8,10 +8,10 @@ describe('Toggle replies widget component', () => {
let wrapper;
const findToggleWrapper = () => wrapper.find('[data-testid="toggle-comments-wrapper"]');
const findIcon = () => wrapper.find(GlIcon);
const findButton = () => wrapper.find(GlButton);
const findAuthorLink = () => wrapper.find(GlLink);
const findTimeAgo = () => wrapper.find(TimeAgoTooltip);
const findIcon = () => wrapper.findComponent(GlIcon);
const findButton = () => wrapper.findComponent(GlButton);
const findAuthorLink = () => wrapper.findComponent(GlLink);
const findTimeAgo = () => wrapper.findComponent(TimeAgoTooltip);
function createComponent(props = {}) {
wrapper = shallowMount(ToggleRepliesWidget, {

View File

@ -6,7 +6,7 @@ import DesignScaler from '~/design_management/components/design_scaler.vue';
describe('Design management design scaler component', () => {
let wrapper;
const getButtons = () => wrapper.findAll(GlButton);
const getButtons = () => wrapper.findAllComponents(GlButton);
const getDecreaseScaleButton = () => getButtons().at(0);
const getResetScaleButton = () => getButtons().at(1);
const getIncreaseScaleButton = () => getButtons().at(2);

View File

@ -32,12 +32,12 @@ describe('Design management design sidebar component', () => {
const originalGon = window.gon;
let wrapper;
const findDiscussions = () => wrapper.findAll(DesignDiscussion);
const findDiscussions = () => wrapper.findAllComponents(DesignDiscussion);
const findFirstDiscussion = () => findDiscussions().at(0);
const findUnresolvedDiscussions = () => wrapper.findAll('[data-testid="unresolved-discussion"]');
const findResolvedDiscussions = () => wrapper.findAll('[data-testid="resolved-discussion"]');
const findParticipants = () => wrapper.find(Participants);
const findResolvedCommentsToggle = () => wrapper.find(GlAccordionItem);
const findParticipants = () => wrapper.findComponent(Participants);
const findResolvedCommentsToggle = () => wrapper.findComponent(GlAccordionItem);
const findNewDiscussionDisclaimer = () =>
wrapper.find('[data-testid="new-discussion-disclaimer"]');
@ -87,7 +87,7 @@ describe('Design management design sidebar component', () => {
it('renders To-Do button', () => {
createComponent();
expect(wrapper.find(DesignTodoButton).exists()).toBe(true);
expect(wrapper.findComponent(DesignTodoButton).exists()).toBe(true);
});
describe('when has no discussions', () => {

View File

@ -57,7 +57,7 @@ describe('Design management design todo button', () => {
});
it('renders TodoButton component', () => {
expect(wrapper.find(TodoButton).exists()).toBe(true);
expect(wrapper.findComponent(TodoButton).exists()).toBe(true);
});
describe('when design has a pending todo', () => {

View File

@ -71,7 +71,7 @@ describe('Design management large image component', () => {
image.trigger('error');
await nextTick();
expect(image.isVisible()).toBe(false);
expect(wrapper.find(GlIcon).element).toMatchSnapshot();
expect(wrapper.findComponent(GlIcon).element).toMatchSnapshot();
});
describe('zoom', () => {

View File

@ -23,8 +23,8 @@ describe('Design management list item component', () => {
const findDesignEvent = () => wrapper.findByTestId('design-event');
const findImgFilename = (id = imgId) => wrapper.findByTestId(`design-img-filename-${id}`);
const findEventIcon = () => findDesignEvent().find(GlIcon);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findEventIcon = () => findDesignEvent().findComponent(GlIcon);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
function createComponent({
notesCount = 0,
@ -74,7 +74,7 @@ describe('Design management list item component', () => {
beforeEach(async () => {
createComponent();
image = wrapper.find('img');
glIntersectionObserver = wrapper.find(GlIntersectionObserver);
glIntersectionObserver = wrapper.findComponent(GlIntersectionObserver);
glIntersectionObserver.vm.$emit('appear');
await nextTick();
@ -86,7 +86,7 @@ describe('Design management list item component', () => {
describe('before image is loaded', () => {
it('renders loading spinner', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
});
@ -105,7 +105,7 @@ describe('Design management list item component', () => {
image.trigger('error');
await nextTick();
expect(image.isVisible()).toBe(false);
expect(wrapper.find(GlIcon).element).toMatchSnapshot();
expect(wrapper.findComponent(GlIcon).element).toMatchSnapshot();
});
describe('when imageV432x230 and image provided', () => {

View File

@ -85,35 +85,35 @@ describe('Design management toolbar component', () => {
createComponent();
await nextTick();
expect(wrapper.find(DeleteButton).exists()).toBe(true);
expect(wrapper.findComponent(DeleteButton).exists()).toBe(true);
});
it('does not render delete button on non-latest version', async () => {
createComponent(false, true, { isLatestVersion: false });
await nextTick();
expect(wrapper.find(DeleteButton).exists()).toBe(false);
expect(wrapper.findComponent(DeleteButton).exists()).toBe(false);
});
it('does not render delete button when user is not logged in', async () => {
createComponent(false, false);
await nextTick();
expect(wrapper.find(DeleteButton).exists()).toBe(false);
expect(wrapper.findComponent(DeleteButton).exists()).toBe(false);
});
it('emits `delete` event on deleteButton `delete-selected-designs` event', async () => {
createComponent();
await nextTick();
wrapper.find(DeleteButton).vm.$emit('delete-selected-designs');
wrapper.findComponent(DeleteButton).vm.$emit('delete-selected-designs');
expect(wrapper.emitted().delete).toBeTruthy();
});
it('renders download button with correct link', () => {
createComponent();
expect(wrapper.find(GlButton).attributes('href')).toBe(
expect(wrapper.findComponent(GlButton).attributes('href')).toBe(
'/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d',
);
});

View File

@ -34,7 +34,7 @@ describe('Design management upload button component', () => {
it('Button `loading` prop is `true`', () => {
createComponent({ isSaving: true });
const button = wrapper.find(GlButton);
const button = wrapper.findComponent(GlButton);
expect(button.exists()).toBe(true);
expect(button.props('loading')).toBe(true);
});

View File

@ -46,7 +46,7 @@ describe('Design management design version dropdown component', () => {
wrapper.destroy();
});
const findVersionLink = (index) => wrapper.findAll(GlDropdownItem).at(index);
const findVersionLink = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
it('renders design version dropdown button', async () => {
createComponent();
@ -76,35 +76,35 @@ describe('Design management design version dropdown component', () => {
createComponent();
await nextTick();
expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version');
expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe('Showing latest version');
});
it('displays latest version text when only 1 version is present', async () => {
createComponent({ maxVersions: 1 });
await nextTick();
expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version');
expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe('Showing latest version');
});
it('displays version text when the current version is not the latest', async () => {
createComponent({ $route: designRouteFactory(PREVIOUS_VERSION_ID) });
await nextTick();
expect(wrapper.find(GlDropdown).attributes('text')).toBe(`Showing version #1`);
expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe(`Showing version #1`);
});
it('displays latest version text when the current version is the latest', async () => {
createComponent({ $route: designRouteFactory(LATEST_VERSION_ID) });
await nextTick();
expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing latest version');
expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe('Showing latest version');
});
it('should have the same length as apollo query', async () => {
createComponent();
await nextTick();
expect(wrapper.findAll(GlDropdownItem)).toHaveLength(wrapper.vm.allVersions.length);
expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(wrapper.vm.allVersions.length);
});
it('should render TimeAgo', async () => {

View File

@ -85,9 +85,9 @@ describe('Design management design index page', () => {
let wrapper;
let router;
const findDiscussionForm = () => wrapper.find(DesignReplyForm);
const findSidebar = () => wrapper.find(DesignSidebar);
const findDesignPresentation = () => wrapper.find(DesignPresentation);
const findDiscussionForm = () => wrapper.findComponent(DesignReplyForm);
const findSidebar = () => wrapper.findComponent(DesignSidebar);
const findDesignPresentation = () => wrapper.findComponent(DesignPresentation);
function createComponent(
{ loading = false } = {},
@ -181,15 +181,15 @@ describe('Design management design index page', () => {
it('sets loading state', () => {
createComponent({ loading: true });
expect(wrapper.find(DesignPresentation).props('isLoading')).toBe(true);
expect(wrapper.find(DesignSidebar).props('isLoading')).toBe(true);
expect(wrapper.findComponent(DesignPresentation).props('isLoading')).toBe(true);
expect(wrapper.findComponent(DesignSidebar).props('isLoading')).toBe(true);
});
it('renders design index', () => {
createComponent({ loading: false }, { data: { design } });
expect(wrapper.element).toMatchSnapshot();
expect(wrapper.find(GlAlert).exists()).toBe(false);
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
it('passes correct props to sidebar component', () => {

View File

@ -111,8 +111,8 @@ describe('Design management index page', () => {
const findDropzoneWrapper = () => wrapper.findByTestId('design-dropzone-wrapper');
const findFirstDropzoneWithDesign = () => wrapper.findAllComponents(DesignDropzone).at(1);
const findDesignsWrapper = () => wrapper.findByTestId('designs-root');
const findDesigns = () => wrapper.findAll(Design);
const draggableAttributes = () => wrapper.find(VueDraggable).vm.$attrs;
const findDesigns = () => wrapper.findAllComponents(Design);
const draggableAttributes = () => wrapper.findComponent(VueDraggable).vm.$attrs;
const findDesignUploadButton = () => wrapper.findByTestId('design-upload-button');
const findDesignToolbarWrapper = () => wrapper.findByTestId('design-toolbar-wrapper');
const findDesignUpdateAlert = () => wrapper.findByTestId('design-update-alert');
@ -120,8 +120,8 @@ describe('Design management index page', () => {
async function moveDesigns(localWrapper) {
await waitForPromises();
localWrapper.find(VueDraggable).vm.$emit('input', reorderedDesigns);
localWrapper.find(VueDraggable).vm.$emit('change', {
localWrapper.findComponent(VueDraggable).vm.$emit('input', reorderedDesigns);
localWrapper.findComponent(VueDraggable).vm.$emit('change', {
moved: {
newIndex: 0,
element: designToMove,

View File

@ -44,7 +44,7 @@ describe('Design management router', () => {
it('pushes home component', () => {
const wrapper = factory(routeArg);
expect(wrapper.find(Designs).exists()).toBe(true);
expect(wrapper.findComponent(Designs).exists()).toBe(true);
});
});
@ -55,7 +55,7 @@ describe('Design management router', () => {
const wrapper = factory(routeArg);
return nextTick().then(() => {
const detail = wrapper.find(DesignDetail);
const detail = wrapper.findComponent(DesignDetail);
expect(detail.exists()).toBe(true);
expect(detail.props('id')).toEqual('1');
});

View File

@ -263,7 +263,7 @@ describe('DiffFileHeader component', () => {
},
},
});
expect(findModeChangedLine().exists()).toBeFalsy();
expect(findModeChangedLine().exists()).toBe(false);
},
);

View File

@ -79,7 +79,7 @@ describe('CommitMessageField', () => {
await fillText(text);
expect(findHighlightsText().text()).toEqual(text);
expect(findHighlightsMark().text()).toBeFalsy();
expect(findHighlightsMark().text()).toBe('');
});
it('highlights characters over 50 length', async () => {

View File

@ -60,8 +60,8 @@ describe('IDE store file actions', () => {
it('closes open files', () => {
return store.dispatch('closeFile', localFile).then(() => {
expect(localFile.opened).toBeFalsy();
expect(localFile.active).toBeFalsy();
expect(localFile.opened).toBe(false);
expect(localFile.active).toBe(false);
expect(store.state.openFiles.length).toBe(0);
});
});
@ -269,7 +269,7 @@ describe('IDE store file actions', () => {
it('sets the file as active', () => {
return store.dispatch('getFileData', { path: localFile.path }).then(() => {
expect(localFile.active).toBeTruthy();
expect(localFile.active).toBe(true);
});
});
@ -277,7 +277,7 @@ describe('IDE store file actions', () => {
return store
.dispatch('getFileData', { path: localFile.path, makeFileActive: false })
.then(() => {
expect(localFile.active).toBeFalsy();
expect(localFile.active).toBe(false);
});
});

View File

@ -71,7 +71,7 @@ describe('Author Select', () => {
wrapper.setData({ hasSearchParam: true });
await nextTick();
expect(findDropdownContainer().attributes('disabled')).toBe(undefined);
expect(findDropdownContainer().attributes('disabled')).toBeUndefined();
});
it('has correct tooltip message', async () => {
@ -91,7 +91,7 @@ describe('Author Select', () => {
wrapper.setData({ hasSearchParam: false });
await nextTick();
expect(findDropdown().attributes('disabled')).toBe(undefined);
expect(findDropdown().attributes('disabled')).toBeUndefined();
});
it('hasSearchParam if user types a truthy string', () => {

View File

@ -26,11 +26,11 @@ describe('Wip', () => {
it('should have props', () => {
const { mr, service } = WorkInProgress.props;
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
expect(mr.type instanceof Object).toBe(true);
expect(mr.required).toBe(true);
expect(service.type instanceof Object).toBeTruthy();
expect(service.required).toBeTruthy();
expect(service.type instanceof Object).toBe(true);
expect(service.required).toBe(true);
});
});
@ -64,7 +64,7 @@ describe('Wip', () => {
await waitForPromises();
expect(vm.isMakingRequest).toBeTruthy();
expect(vm.isMakingRequest).toBe(true);
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
expect(toast).toHaveBeenCalledWith('Marked as ready. Merging is now allowed.');
});
@ -81,7 +81,7 @@ describe('Wip', () => {
});
it('should have correct elements', () => {
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
expect(el.classList.contains('mr-widget-body')).toBe(true);
expect(el.innerText).toContain(
"Merge blocked: merge request must be marked as ready. It's still marked as draft.",
);
@ -95,7 +95,7 @@ describe('Wip', () => {
await nextTick();
expect(el.querySelector('.js-remove-draft')).toEqual(null);
expect(el.querySelector('.js-remove-draft')).toBeNull();
});
});
});

View File

@ -104,7 +104,7 @@ describe('vue_shared/components/confirm_modal', () => {
});
it('renders GlModal with data', () => {
expect(findModal().exists()).toBeTruthy();
expect(findModal().exists()).toBe(true);
expect(findModal().attributes()).toEqual(
expect.objectContaining({
oktitle: MOCK_MODAL_DATA.modalAttributes.okTitle,

View File

@ -234,14 +234,14 @@ describe('RunnerInstructionsModal component', () => {
MockResizeObserver.mockResize('xs');
await nextTick();
expect(findPlatformButtonGroup().attributes('vertical')).toBeTruthy();
expect(findPlatformButtonGroup().attributes('vertical')).toEqual('true');
});
it('to a non-xs viewport', async () => {
MockResizeObserver.mockResize('sm');
await nextTick();
expect(findPlatformButtonGroup().props('vertical')).toBeFalsy();
expect(findPlatformButtonGroup().props('vertical')).toBeUndefined();
});
});
});

View File

@ -3,7 +3,7 @@ import { nextTick } from 'vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
import WebIdeLink, { i18n } from '~/vue_shared/components/web_ide_link.vue';
import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue';
import { stubComponent } from 'helpers/stub_component';
@ -37,8 +37,8 @@ const ACTION_EDIT_CONFIRM_FORK = {
const ACTION_WEB_IDE = {
href: TEST_WEB_IDE_URL,
key: 'webide',
secondaryText: 'Quickly and easily edit multiple files in your project.',
tooltip: '',
secondaryText: i18n.webIdeText,
tooltip: i18n.webIdeTooltip,
text: 'Web IDE',
attrs: {
'data-qa-selector': 'web_ide_button',

View File

@ -127,7 +127,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
let(:doorkeeper_access_token) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') }
before do
set_bearer_token(doorkeeper_access_token.token)
set_bearer_token(doorkeeper_access_token.plaintext_token)
end
it { is_expected.to eq user }
@ -577,7 +577,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
context 'passed as header' do
before do
set_bearer_token(doorkeeper_access_token.token)
set_bearer_token(doorkeeper_access_token.plaintext_token)
end
it 'returns token if valid oauth_access_token' do
@ -587,7 +587,7 @@ RSpec.describe Gitlab::Auth::AuthFinders do
context 'passed as param' do
it 'returns user if valid oauth_access_token' do
set_param(:access_token, doorkeeper_access_token.token)
set_param(:access_token, doorkeeper_access_token.plaintext_token)
expect(find_oauth_access_token.token).to eq doorkeeper_access_token.token
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::DoorkeeperSecretStoring::Pbkdf2Sha512 do
describe '.transform_secret' do
let(:plaintext_token) { 'CzOBzBfU9F-HvsqfTaTXF4ivuuxYZuv3BoAK4pnvmyw' }
it 'generates a PBKDF2+SHA512 hashed value in the correct format' do
expect(described_class.transform_secret(plaintext_token))
.to eq("$pbkdf2-sha512$20000$$.c0G5XJVEew1TyeJk5TrkvB0VyOaTmDzPrsdNRED9vVeZlSyuG3G90F0ow23zUCiWKAVwmNnR/ceh.nJG3MdpQ") # rubocop:disable Layout/LineLength
end
context 'when hash_oauth_tokens is disabled' do
before do
stub_feature_flags(hash_oauth_tokens: false)
end
it 'returns a plaintext token' do
expect(described_class.transform_secret(plaintext_token)).to eq(plaintext_token)
end
end
end
describe 'STRETCHES' do
it 'is 20_000' do
expect(described_class::STRETCHES).to eq(20_000)
end
end
describe 'SALT' do
it 'is empty' do
expect(described_class::SALT).to be_empty
end
end
end

View File

@ -22,4 +22,51 @@ RSpec.describe OauthAccessToken do
end
end
end
describe 'Doorkeeper secret storing' do
it 'stores the token in hashed format' do
expect(token.token).not_to eq(token.plaintext_token)
end
it 'does not allow falling back to plaintext token comparison' do
expect(described_class.by_token(token.token)).to be_nil
end
it 'finds a token by plaintext token' do
expect(described_class.by_token(token.plaintext_token)).to be_a(OauthAccessToken)
end
context 'when the token is stored in plaintext' do
let(:plaintext_token) { Devise.friendly_token(20) }
before do
token.update_column(:token, plaintext_token)
end
it 'falls back to plaintext token comparison' do
expect(described_class.by_token(plaintext_token)).to be_a(OauthAccessToken)
end
end
context 'when hash_oauth_secrets is disabled' do
let(:hashed_token) { create(:oauth_access_token, application_id: app_one.id) }
before do
hashed_token
stub_feature_flags(hash_oauth_tokens: false)
end
it 'stores the token in plaintext' do
expect(token.token).to eq(token.plaintext_token)
end
it 'finds a token by plaintext token' do
expect(described_class.by_token(token.plaintext_token)).to be_a(OauthAccessToken)
end
it 'does not find a token that was previously stored as hashed' do
expect(described_class.by_token(hashed_token.plaintext_token)).to be_nil
end
end
end
end

View File

@ -9,13 +9,13 @@ RSpec.describe 'doorkeeper access' do
describe "unauthenticated" do
it "returns authentication success" do
get api("/user"), params: { access_token: token.token }
get api("/user"), params: { access_token: token.plaintext_token }
expect(response).to have_gitlab_http_status(:ok)
end
include_examples 'user login request with unique ip limit' do
def request
get api('/user'), params: { access_token: token.token }
get api('/user'), params: { access_token: token.plaintext_token }
end
end
end
@ -42,7 +42,7 @@ RSpec.describe 'doorkeeper access' do
shared_examples 'forbidden request' do
it 'returns 403 response' do
get api("/user"), params: { access_token: token.token }
get api("/user"), params: { access_token: token.plaintext_token }
expect(response).to have_gitlab_http_status(:forbidden)
end

View File

@ -376,7 +376,7 @@ RSpec.describe API::GoProxy do
end
it 'returns ok with a job token' do
get_resource(oauth_access_token: job)
get_resource(access_token: job)
expect(response).to have_gitlab_http_status(:ok)
end
@ -395,7 +395,7 @@ RSpec.describe API::GoProxy do
it 'returns unauthorized with a failed job token' do
job.update!(status: :failed)
get_resource(oauth_access_token: job)
get_resource(access_token: job)
expect(response).to have_gitlab_http_status(:unauthorized)
end
@ -445,7 +445,7 @@ RSpec.describe API::GoProxy do
end
it 'returns not found with a job token' do
get_resource(oauth_access_token: job)
get_resource(access_token: job)
expect(response).to have_gitlab_http_status(:not_found)
end

View File

@ -539,7 +539,7 @@ RSpec.describe API::Helpers do
let(:token) { create(:oauth_access_token) }
before do
env['HTTP_AUTHORIZATION'] = "Bearer #{token.token}"
env['HTTP_AUTHORIZATION'] = "Bearer #{token.plaintext_token}"
end
it_behaves_like 'sudo'

View File

@ -59,7 +59,7 @@ RSpec.describe API::NpmProjectPackages do
end
context 'with access token' do
let(:headers) { build_token_auth_header(token.token) }
let(:headers) { build_token_auth_header(token.plaintext_token) }
it_behaves_like 'successfully downloads the file'
it_behaves_like 'a package tracking event', 'API::NpmPackages', 'pull_package'
@ -95,7 +95,7 @@ RSpec.describe API::NpmProjectPackages do
it_behaves_like 'a package file that requires auth'
context 'with guest' do
let(:headers) { build_token_auth_header(token.token) }
let(:headers) { build_token_auth_header(token.plaintext_token) }
it 'denies download when not enough permissions' do
project.add_guest(user)
@ -356,7 +356,7 @@ RSpec.describe API::NpmProjectPackages do
end
def upload_with_token(package_name, params = {})
upload_package(package_name, params.merge(access_token: token.token))
upload_package(package_name, params.merge(access_token: token.plaintext_token))
end
def upload_with_job_token(package_name, params = {})

View File

@ -104,8 +104,8 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
end
context 'with the token in the OAuth headers' do
let(:request_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(token)) }
let(:other_user_request_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(other_user_token)) }
let(:request_args) { api_get_args_with_token_headers(api_partial_url, bearer_headers(token)) }
let(:other_user_request_args) { api_get_args_with_token_headers(api_partial_url, bearer_headers(other_user_token)) }
it_behaves_like 'rate-limited user based token-authenticated requests'
end
@ -131,8 +131,8 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
end
context 'with the token in the OAuth headers' do
let(:request_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(token)) }
let(:other_user_request_args) { api_get_args_with_token_headers(api_partial_url, oauth_token_headers(other_user_token)) }
let(:request_args) { api_get_args_with_token_headers(api_partial_url, bearer_headers(token)) }
let(:other_user_request_args) { api_get_args_with_token_headers(api_partial_url, bearer_headers(other_user_token)) }
it_behaves_like 'rate-limited user based token-authenticated requests'
end
@ -1189,7 +1189,7 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
it 'request is authenticated by token in the OAuth headers' do
expect_authenticated_request
get url, headers: oauth_token_headers(personal_access_token)
get url, headers: bearer_headers(personal_access_token)
end
it 'request is authenticated by token in basic auth' do
@ -1206,7 +1206,7 @@ RSpec.describe 'Rack Attack global throttles', :use_clean_rails_memory_store_cac
it 'request is authenticated by token in query string' do
expect_authenticated_request
get url, params: { access_token: oauth_token.token }
get url, params: { access_token: oauth_token.plaintext_token }
end
it 'request is authenticated by token in the headers' do

View File

@ -19,15 +19,17 @@ module ApiHelpers
# => "/api/v2/issues?foo=bar&private_token=..."
#
# Returns the relative path to the requested API resource
def api(path, user = nil, version: API::API.version, personal_access_token: nil, oauth_access_token: nil, job_token: nil)
def api(path, user = nil, version: API::API.version, personal_access_token: nil, oauth_access_token: nil, job_token: nil, access_token: nil)
full_path = "/api/#{version}#{path}"
if oauth_access_token
query_string = "access_token=#{oauth_access_token.token}"
query_string = "access_token=#{oauth_access_token.plaintext_token}"
elsif personal_access_token
query_string = "private_token=#{personal_access_token.token}"
elsif job_token
query_string = "job_token=#{job_token}"
elsif access_token
query_string = "access_token=#{access_token.token}"
elsif user
personal_access_token = create(:personal_access_token, user: user)
query_string = "private_token=#{personal_access_token.token}"

View File

@ -17,8 +17,12 @@ module RackAttackSpecHelpers
{ Gitlab::Auth::AuthFinders::PRIVATE_TOKEN_HEADER => personal_access_token.token }
end
def bearer_headers(token)
{ 'AUTHORIZATION' => "Bearer #{token.token}" }
end
def oauth_token_headers(oauth_access_token)
{ 'AUTHORIZATION' => "Bearer #{oauth_access_token.token}" }
{ 'AUTHORIZATION' => "Bearer #{oauth_access_token.plaintext_token}" }
end
def basic_auth_headers(user, personal_access_token)

View File

@ -244,7 +244,7 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
let(:headers) do
case auth
when :oauth
build_token_auth_header(token.token)
build_token_auth_header(token.plaintext_token)
when :personal_access_token
build_token_auth_header(personal_access_token.token)
when :job_token
@ -404,7 +404,7 @@ RSpec.shared_examples 'handling get dist tags requests' do |scope: :project|
shared_examples 'handling all conditions' do
context 'with oauth token' do
let(:headers) { build_token_auth_header(token.token) }
let(:headers) { build_token_auth_header(token.plaintext_token) }
it_behaves_like 'handling different package names, visibilities and user roles'
end
@ -514,7 +514,7 @@ RSpec.shared_examples 'handling create dist tag requests' do |scope: :project|
shared_examples 'handling all conditions' do
context 'with oauth token' do
let(:headers) { build_token_auth_header(token.token) }
let(:headers) { build_token_auth_header(token.plaintext_token) }
it_behaves_like 'handling different package names, visibilities and user roles'
end
@ -622,7 +622,7 @@ RSpec.shared_examples 'handling delete dist tag requests' do |scope: :project|
shared_examples 'handling all conditions' do
context 'with oauth token' do
let(:headers) { build_token_auth_header(token.token) }
let(:headers) { build_token_auth_header(token.plaintext_token) }
it_behaves_like 'handling different package names, visibilities and user roles'
end