Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
88ccc935c0
commit
47579e24f3
|
@ -1,5 +1,14 @@
|
|||
Please view this file on the master branch, on stable branches it's out of date.
|
||||
|
||||
## 13.0.1 (2020-05-27)
|
||||
|
||||
### Security (3 changes)
|
||||
|
||||
- Change the mirror user along with pull mirror settings.
|
||||
- Allow only users with a verified email to be member of a group when the group has restricted membership based on email domain.
|
||||
- Do not auto-confirm email in Trial registration.
|
||||
|
||||
|
||||
## 13.0.0 (2020-05-22)
|
||||
|
||||
### Security (1 change)
|
||||
|
@ -326,6 +335,15 @@ Please view this file on the master branch, on stable branches it's out of date.
|
|||
- Translate unauthenticated user string for Audit Event. !31856 (Sashi Kumar)
|
||||
|
||||
|
||||
## 12.10.7 (2020-05-27)
|
||||
|
||||
### Security (3 changes)
|
||||
|
||||
- Change the mirror user along with pull mirror settings.
|
||||
- Allow only users with a verified email to be member of a group when the group has restricted membership based on email domain.
|
||||
- Do not auto-confirm email in Trial registration.
|
||||
|
||||
|
||||
## 12.10.6 (2020-05-15)
|
||||
|
||||
- No changes.
|
||||
|
@ -400,6 +418,15 @@ Please view this file on the master branch, on stable branches it's out of date.
|
|||
- Add health status counts to usage data. !28964
|
||||
|
||||
|
||||
## 12.9.8 (2020-05-27)
|
||||
|
||||
### Security (3 changes)
|
||||
|
||||
- Change the mirror user along with pull mirror settings.
|
||||
- Allow only users with a verified email to be member of a group when the group has restricted membership based on email domain.
|
||||
- Do not auto-confirm email in Trial registration.
|
||||
|
||||
|
||||
## 12.9.6 (2020-05-05)
|
||||
|
||||
- No changes.
|
||||
|
|
|
@ -1 +1 @@
|
|||
8.32.0
|
||||
8.32.1
|
||||
|
|
|
@ -108,7 +108,6 @@ export default class Clusters {
|
|||
});
|
||||
|
||||
this.installApplication = this.installApplication.bind(this);
|
||||
this.showToken = this.showToken.bind(this);
|
||||
|
||||
this.errorContainer = document.querySelector('.js-cluster-error');
|
||||
this.successContainer = document.querySelector('.js-cluster-success');
|
||||
|
@ -119,7 +118,6 @@ export default class Clusters {
|
|||
);
|
||||
this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
|
||||
this.successApplicationContainer = document.querySelector('.js-cluster-application-notice');
|
||||
this.showTokenButton = document.querySelector('.js-show-cluster-token');
|
||||
this.tokenField = document.querySelector('.js-cluster-token');
|
||||
this.ingressDomainHelpText = document.querySelector('.js-ingress-domain-help-text');
|
||||
this.ingressDomainSnippet =
|
||||
|
@ -258,7 +256,6 @@ export default class Clusters {
|
|||
}
|
||||
|
||||
addListeners() {
|
||||
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
|
||||
eventHub.$on('installApplication', this.installApplication);
|
||||
eventHub.$on('updateApplication', data => this.updateApplication(data));
|
||||
eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
|
||||
|
@ -275,7 +272,6 @@ export default class Clusters {
|
|||
}
|
||||
|
||||
removeListeners() {
|
||||
if (this.showTokenButton) this.showTokenButton.removeEventListener('click', this.showToken);
|
||||
eventHub.$off('installApplication', this.installApplication);
|
||||
eventHub.$off('updateApplication', this.updateApplication);
|
||||
eventHub.$off('saveKnativeDomain');
|
||||
|
@ -344,18 +340,6 @@ export default class Clusters {
|
|||
}
|
||||
}
|
||||
|
||||
showToken() {
|
||||
const type = this.tokenField.getAttribute('type');
|
||||
|
||||
if (type === 'password') {
|
||||
this.tokenField.setAttribute('type', 'text');
|
||||
this.showTokenButton.textContent = s__('ClusterIntegration|Hide');
|
||||
} else {
|
||||
this.tokenField.setAttribute('type', 'password');
|
||||
this.showTokenButton.textContent = s__('ClusterIntegration|Show');
|
||||
}
|
||||
}
|
||||
|
||||
hideAll() {
|
||||
this.errorContainer.classList.add('hidden');
|
||||
this.successContainer.classList.add('hidden');
|
||||
|
|
|
@ -119,7 +119,11 @@ export default class Issue {
|
|||
} else {
|
||||
this.disableCloseReopenButton($button);
|
||||
|
||||
const url = $button.attr('href');
|
||||
const url = $button.data('close-reopen-url');
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
return axios
|
||||
.put(url)
|
||||
.then(({ data }) => {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import { GlFormGroup, GlFormInput, GlFormRadioGroup, GlFormTextarea } from '@gitlab/ui';
|
||||
import { escape as esc } from 'lodash';
|
||||
|
||||
const defaultFileName = dashboard => dashboard.path.split('/').reverse()[0];
|
||||
|
||||
|
@ -42,7 +43,7 @@ export default {
|
|||
html: sprintf(
|
||||
__('Commit to %{branchName} branch'),
|
||||
{
|
||||
branchName: `<strong>${this.defaultBranch}</strong>`,
|
||||
branchName: `<strong>${esc(this.defaultBranch)}</strong>`,
|
||||
},
|
||||
false,
|
||||
),
|
||||
|
|
|
@ -146,11 +146,12 @@ const createTestDetails = detailsEndpoint => {
|
|||
};
|
||||
|
||||
const createDagApp = () => {
|
||||
const el = document.querySelector('#js-pipeline-dag-vue');
|
||||
if (!el) {
|
||||
if (!window.gon?.features?.dagPipelineTab) {
|
||||
return;
|
||||
}
|
||||
const graphUrl = el.dataset?.pipelineDataPath;
|
||||
|
||||
const el = document.querySelector('#js-pipeline-dag-vue');
|
||||
const graphUrl = el?.dataset?.pipelineDataPath;
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
|
|
|
@ -40,7 +40,9 @@ export default class Profile {
|
|||
bindEvents() {
|
||||
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
|
||||
$('.js-group-notification-email').on('change', this.submitForm);
|
||||
$('#user_notification_email').on('change', this.submitForm);
|
||||
$('#user_notification_email').on('select2-selecting', event => {
|
||||
setTimeout(this.submitForm.bind(event.currentTarget));
|
||||
});
|
||||
$('#user_notified_of_own_activity').on('change', this.submitForm);
|
||||
this.form.on('submit', this.onSubmitForm);
|
||||
}
|
||||
|
|
|
@ -172,13 +172,15 @@ export default {
|
|||
<div class="mb-5 mb-sm-3 mt-sm-4 col col-sm-auto">
|
||||
<gl-button
|
||||
v-gl-tooltip
|
||||
class="remove-button w-100"
|
||||
class="remove-button w-100 form-control"
|
||||
:aria-label="__('Remove asset link')"
|
||||
:title="__('Remove asset link')"
|
||||
@click="onRemoveClicked(link.id)"
|
||||
>
|
||||
<gl-icon class="mr-1 mr-sm-0 mb-1" :size="16" name="remove" />
|
||||
<span class="d-inline d-sm-none">{{ __('Remove asset link') }}</span>
|
||||
<div class="d-flex">
|
||||
<gl-icon class="mr-1 mr-sm-0" :size="16" name="remove" />
|
||||
<span class="d-inline d-sm-none">{{ __('Remove asset link') }}</span>
|
||||
</div>
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
// -------
|
||||
// Please see `app/assets/stylesheets/page_bundles/ide_themes/README.md` for a guide on contributing new themes
|
||||
// -------
|
||||
.ide.theme-dark {
|
||||
a:not(.btn) {
|
||||
color: var(--ide-link-color);
|
||||
.ide {
|
||||
$bs-input-focus-border: #80bdff;
|
||||
$bs-input-focus-box-shadow: rgba(0, 123, 255, 0.25);
|
||||
|
||||
a:not(.btn),
|
||||
.btn-link:hover,
|
||||
.btn-link:focus,
|
||||
.btn-link:active {
|
||||
color: var(--ide-link-color, $blue-600);
|
||||
}
|
||||
|
||||
h1,
|
||||
|
@ -19,156 +25,207 @@
|
|||
.context-header > a,
|
||||
input,
|
||||
textarea,
|
||||
.md-area.is-focused,
|
||||
.dropdown-menu li button,
|
||||
.dropdown-menu-selectable li a.is-active,
|
||||
.dropdown-menu-inner-title,
|
||||
.dropdown-menu-inner-content,
|
||||
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a,
|
||||
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover,
|
||||
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a.active .badge.badge-pill,
|
||||
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover .badge.badge-pill,
|
||||
.badge.badge-pill,
|
||||
.bs-callout,
|
||||
.ide-pipeline .top-bar,
|
||||
.ide-pipeline .top-bar .controllers .controllers-buttons {
|
||||
color: var(--ide-text-color);
|
||||
.ide-pipeline .top-bar .controllers .controllers-buttons,
|
||||
.controllers-buttons svg,
|
||||
.nav-links li a.active,
|
||||
.md-area.is-focused {
|
||||
color: var(--ide-text-color, $gl-text-color);
|
||||
}
|
||||
|
||||
.drag-handle:hover,
|
||||
.card-header .badge.badge-pill {
|
||||
background-color: var(--ide-dropdown-hover-background);
|
||||
.badge.badge-pill {
|
||||
color: var(--ide-text-color, $gray-800);
|
||||
background-color: var(--ide-background, $badge-bg);
|
||||
}
|
||||
|
||||
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a,
|
||||
.dropdown-menu-inner-content,
|
||||
.file-row .file-row-icon svg,
|
||||
.file-row:hover .file-row-icon svg,
|
||||
.controllers-buttons svg {
|
||||
color: var(--ide-text-color-secondary);
|
||||
.file-row:hover .file-row-icon svg {
|
||||
color: var(--ide-text-color-secondary, $gl-text-color-secondary);
|
||||
}
|
||||
|
||||
.nav-links:not(.quick-links) li:not(.md-header-toolbar) {
|
||||
&:hover a,
|
||||
&.active a,
|
||||
a:hover,
|
||||
a.active {
|
||||
&,
|
||||
.badge.badge-pill {
|
||||
color: var(--ide-text-color, $black);
|
||||
border-color: var(--ide-input-border, $gray-darkest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drag-handle:hover {
|
||||
background-color: var(--ide-dropdown-hover-background, $white-normal);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: var(--ide-background, $white);
|
||||
|
||||
.badge.badge-pill {
|
||||
background-color: var(--ide-dropdown-hover-background, $badge-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--ide-text-color-secondary) !important;
|
||||
color: var(--ide-text-color-secondary, $gl-text-color-secondary) !important;
|
||||
}
|
||||
|
||||
input[type='search']::placeholder,
|
||||
input[type='text']::placeholder,
|
||||
textarea::placeholder,
|
||||
textarea::placeholder {
|
||||
color: var(--ide-input-border, $gl-text-color-tertiary);
|
||||
}
|
||||
|
||||
.dropdown-input .fa {
|
||||
color: var(--ide-input-border);
|
||||
color: var(--ide-input-border, $dropdown-input-fa-color);
|
||||
}
|
||||
|
||||
.ide-nav-form .input-icon {
|
||||
color: var(--ide-input-border);
|
||||
color: var(--ide-input-border, $dropdown-input-fa-color);
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: var(--ide-background, $gray-100);
|
||||
}
|
||||
|
||||
code,
|
||||
.badge.badge-pill,
|
||||
.card-header,
|
||||
.bs-callout,
|
||||
.ide-pipeline .top-bar,
|
||||
.ide-terminal .top-bar {
|
||||
background-color: var(--ide-background);
|
||||
background-color: var(--ide-background, $gray-light);
|
||||
}
|
||||
|
||||
.bs-callout {
|
||||
border-color: var(--ide-dropdown-background);
|
||||
border-color: var(--ide-dropdown-background, $border-color);
|
||||
|
||||
code {
|
||||
background-color: var(--ide-dropdown-background);
|
||||
background-color: var(--ide-dropdown-background, $gray-100);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a:hover {
|
||||
border-color: var(--ide-dropdown-hover-background);
|
||||
.common-note-form .md-area {
|
||||
border-color: var(--ide-input-border, $border-color);
|
||||
}
|
||||
|
||||
.common-note-form .md-area {
|
||||
border-color: var(--ide-input-border);
|
||||
.md table:not(.code) tr th {
|
||||
background-color: var(--ide-highlight-background, $gray-100);
|
||||
}
|
||||
|
||||
&,
|
||||
.md table:not(.code) tr th,
|
||||
.common-note-form .md-area,
|
||||
.card {
|
||||
background-color: var(--ide-highlight-background);
|
||||
.card,
|
||||
.common-note-form .md-area {
|
||||
background-color: var(--ide-highlight-background, $white);
|
||||
}
|
||||
|
||||
.card,
|
||||
.card-header,
|
||||
.ide-terminal .top-bar,
|
||||
.ide-pipeline .top-bar {
|
||||
border-color: var(--ide-border-color);
|
||||
border-color: var(--ide-border-color, $border-color);
|
||||
}
|
||||
|
||||
hr {
|
||||
border-color: var(--ide-border-color, darken($gray-normal, 8%));
|
||||
}
|
||||
|
||||
hr,
|
||||
.md h1,
|
||||
.md h2,
|
||||
.md blockquote,
|
||||
pre,
|
||||
.md table:not(.code) tbody td,
|
||||
.md table:not(.code) tr th,
|
||||
.nav-links:not(.quick-links) {
|
||||
border-color: var(--ide-border-color-alt);
|
||||
.nav-links:not(.quick-links),
|
||||
.common-note-form .md-area.is-focused .nav-links {
|
||||
border-color: var(--ide-border-color-alt, $white-dark);
|
||||
}
|
||||
|
||||
.ide-sidebar-link.active {
|
||||
color: var(--ide-highlight-accent);
|
||||
box-shadow: inset 3px 0 var(--ide-highlight-accent);
|
||||
pre {
|
||||
border-color: var(--ide-border-color-alt, $gray-200);
|
||||
|
||||
&.is-right {
|
||||
box-shadow: inset -3px 0 var(--ide-highlight-accent);
|
||||
code {
|
||||
background-color: var(--ide-border-color, inherit);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-links li.active a,
|
||||
.nav-links li a.active {
|
||||
border-color: var(--ide-highlight-accent);
|
||||
color: var(--ide-text-color);
|
||||
}
|
||||
// highlight accents (based on navigation theme) should only apply
|
||||
// in the default white theme and "none" theme.
|
||||
&:not(.theme-white):not(.theme-none) {
|
||||
.ide-sidebar-link.active {
|
||||
color: var(--ide-highlight-accent, $gl-text-color);
|
||||
box-shadow: inset 3px 0 var(--ide-highlight-accent, $gl-text-color);
|
||||
|
||||
.avatar-container {
|
||||
&,
|
||||
.avatar {
|
||||
color: var(--ide-text-color);
|
||||
background-color: var(--ide-highlight-background);
|
||||
border-color: var(--ide-highlight-background);
|
||||
&.is-right {
|
||||
box-shadow: inset -3px 0 var(--ide-highlight-accent, $gl-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-links li.active a,
|
||||
.nav-links li a.active {
|
||||
border-color: var(--ide-highlight-accent, $gl-text-color);
|
||||
}
|
||||
|
||||
.dropdown-menu .nav-links li a.active {
|
||||
border-color: var(--ide-highlight-accent, $gl-text-color);
|
||||
}
|
||||
|
||||
// for other themes, suppress different avatar default colors for simplicity
|
||||
.avatar-container {
|
||||
&,
|
||||
.avatar {
|
||||
color: var(--ide-text-color, $gl-text-color);
|
||||
background-color: var(--ide-highlight-background, $white);
|
||||
border-color: var(--ide-highlight-background, rgba($black, $gl-avatar-border-opacity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type='text'],
|
||||
input[type='search'],
|
||||
.filtered-search-box {
|
||||
border-color: var(--ide-input-border);
|
||||
background: var(--ide-input-background) !important;
|
||||
border-color: var(--ide-input-border, $border-color);
|
||||
background: var(--ide-input-background, $white) !important;
|
||||
}
|
||||
|
||||
input[type='text']:not([disabled]):not([readonly]):focus,
|
||||
.md-area.is-focused {
|
||||
border-color: var(--ide-input-border, $bs-input-focus-border);
|
||||
box-shadow: 0 0 0 3px var(--ide-dropdown-background, $bs-input-focus-box-shadow);
|
||||
}
|
||||
|
||||
input[type='text'],
|
||||
input[type='search'],
|
||||
.filtered-search-box,
|
||||
textarea {
|
||||
color: var(--ide-input-color) !important;
|
||||
color: var(--ide-input-color, $gl-text-color) !important;
|
||||
}
|
||||
|
||||
.filtered-search-box input[type='search'] {
|
||||
border-color: transparent;
|
||||
border-color: transparent !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.filtered-search-token .value-container,
|
||||
.filtered-search-term .value-container {
|
||||
background-color: var(--ide-dropdown-hover-background);
|
||||
color: var(--ide-text-color);
|
||||
background-color: var(--ide-dropdown-hover-background, $white-normal);
|
||||
color: var(--ide-text-color, $gl-text-color);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--ide-input-border);
|
||||
background-color: var(--ide-input-border, $gray-200);
|
||||
}
|
||||
}
|
||||
|
||||
@function calc-btn-hover-padding($original-padding, $original-border: 1px) {
|
||||
@return calc(#{$original-padding + $original-border} - var(--ide-btn-hover-border-width));
|
||||
@return calc(#{$original-padding + $original-border} - var(--ide-btn-hover-border-width, $original-border));
|
||||
}
|
||||
|
||||
.btn:not(.btn-link):not([disabled]):hover {
|
||||
border-width: var(--ide-btn-hover-border-width);
|
||||
border-width: var(--ide-btn-hover-border-width, 1px);
|
||||
padding: calc-btn-hover-padding(6px) calc-btn-hover-padding(10px);
|
||||
}
|
||||
|
||||
|
@ -180,53 +237,71 @@
|
|||
padding: calc-btn-hover-padding(6px) 0;
|
||||
}
|
||||
|
||||
.btn-inverted,
|
||||
.btn-default,
|
||||
.dropdown,
|
||||
.dropdown-menu-toggle {
|
||||
background-color: var(--ide-input-background) !important;
|
||||
color: var(--ide-input-color) !important;
|
||||
border-color: var(--ide-btn-default-border);
|
||||
background-color: var(--ide-input-background, $white) !important;
|
||||
color: var(--ide-input-color, $gl-text-color) !important;
|
||||
border-color: var(--ide-btn-default-border, $border-color);
|
||||
}
|
||||
|
||||
.btn-inverted,
|
||||
.btn-default {
|
||||
.dropdown-menu-toggle {
|
||||
border-color: var(--ide-btn-default-border, $gray-darkest);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: var(--ide-btn-default-hover-border) !important;
|
||||
background-color: var(--ide-dropdown-btn-hover-background, $gray-darker) !important;
|
||||
border-color: var(--ide-dropdown-btn-hover-border, $gray-darkest) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown,
|
||||
.dropdown-menu-toggle {
|
||||
// In IDE, the only inverted buttons are `.btn-remove`
|
||||
.btn-inverted.btn-remove {
|
||||
color: var(--ide-input-color, $red-500) !important;
|
||||
background-color: var(--ide-input-background, $white) !important;
|
||||
border-color: var(--ide-btn-default-border, $red-500);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--ide-dropdown-btn-hover-background) !important;
|
||||
border-color: var(--ide-dropdown-btn-hover-border) !important;
|
||||
color: var(--ide-input-color, $red-700) !important;
|
||||
background-color: var(--ide-input-background, $red-100) !important;
|
||||
border-color: var(--ide-btn-default-hover-border, $red-500) !important;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: var(--ide-input-color, $red-800) !important;
|
||||
background-color: var(--ide-input-background, $red-200) !important;
|
||||
border-color: var(--ide-btn-default-hover-border, $red-600) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-default {
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: var(--ide-btn-default-hover-border, $border-white-normal) !important;
|
||||
background-color: var(--ide-input-background, $white-normal) !important;
|
||||
}
|
||||
|
||||
&:active,
|
||||
.active {
|
||||
border-color: var(--ide-btn-default-hover-border, $border-white-normal) !important;
|
||||
background-color: var(--ide-input-background, $white-dark) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
color: var(--ide-text-color);
|
||||
border-color: var(--ide-background);
|
||||
background-color: var(--ide-dropdown-background);
|
||||
color: var(--ide-text-color, $gl-text-color);
|
||||
border-color: var(--ide-background, $border-color);
|
||||
background-color: var(--ide-dropdown-background, $white);
|
||||
|
||||
.divider,
|
||||
.nav-links:not(.quick-links) {
|
||||
background-color: var(--ide-dropdown-hover-background);
|
||||
border-color: var(--ide-dropdown-hover-background);
|
||||
background-color: var(--ide-dropdown-hover-background, $white);
|
||||
border-color: var(--ide-dropdown-hover-background, $border-color);
|
||||
}
|
||||
|
||||
.nav-links li a.active {
|
||||
border-color: var(--ide-highlight-accent);
|
||||
}
|
||||
|
||||
.nav-links:not(.quick-links) li:not(.md-header-toolbar) a {
|
||||
color: var(--ide-text-color);
|
||||
|
||||
&.active {
|
||||
color: var(--ide-text-color);
|
||||
}
|
||||
.divider {
|
||||
background-color: var(--ide-dropdown-hover-background, $gray-200);
|
||||
border-color: var(--ide-dropdown-hover-background, $gray-200);
|
||||
}
|
||||
|
||||
li > a:not(.disable-hover):hover,
|
||||
|
@ -234,75 +309,88 @@
|
|||
li button:not(.disable-hover):hover,
|
||||
li button:not(.disable-hover):focus,
|
||||
li button.is-focused {
|
||||
background-color: var(--ide-dropdown-hover-background);
|
||||
color: var(--ide-text-color);
|
||||
background-color: var(--ide-dropdown-hover-background, $gray-darker);
|
||||
color: var(--ide-text-color, $gl-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-title,
|
||||
.dropdown-input {
|
||||
border-color: var(--ide-dropdown-hover-background) !important;
|
||||
border-color: var(--ide-dropdown-hover-background, $gray-200) !important;
|
||||
}
|
||||
|
||||
.btn-primary,
|
||||
.btn-info {
|
||||
background-color: var(--ide-btn-primary-background);
|
||||
border-color: var(--ide-btn-primary-border) !important;
|
||||
background-color: var(--ide-btn-primary-background, $blue-500);
|
||||
border-color: var(--ide-btn-primary-border, $blue-600) !important;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: var(--ide-btn-primary-hover-border) !important;
|
||||
background-color: var(--ide-btn-primary-background, $blue-600);
|
||||
border-color: var(--ide-btn-primary-hover-border, $blue-700) !important;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&.active {
|
||||
background-color: var(--ide-btn-primary-background, $blue-700);
|
||||
border-color: var(--ide-btn-primary-hover-border, $blue-800) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: var(--ide-btn-success-background);
|
||||
border-color: var(--ide-btn-success-border) !important;
|
||||
background-color: var(--ide-btn-success-background, $green-500);
|
||||
border-color: var(--ide-btn-success-border, $green-600) !important;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
border-color: var(--ide-btn-success-hover-border) !important;
|
||||
background-color: var(--ide-btn-success-background, $green-600);
|
||||
border-color: var(--ide-btn-success-hover-border, $green-700) !important;
|
||||
}
|
||||
|
||||
&:active,
|
||||
&.active {
|
||||
background-color: var(--ide-btn-success-background, $green-700);
|
||||
border-color: var(--ide-btn-success-hover-border, $green-800) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn[disabled] {
|
||||
background: var(--ide-btn-default-background) !important;
|
||||
border: 1px solid var(--ide-btn-disabled-border) !important;
|
||||
color: var(--ide-btn-disabled-color) !important;
|
||||
background-color: var(--ide-btn-default-background, $gray-light) !important;
|
||||
border: 1px solid var(--ide-btn-disabled-border, $gray-200) !important;
|
||||
color: var(--ide-btn-disabled-color, $gl-text-color-disabled) !important;
|
||||
}
|
||||
|
||||
pre code,
|
||||
.md table:not(.code) tbody {
|
||||
background-color: var(--ide-border-color);
|
||||
background-color: var(--ide-border-color, $white);
|
||||
}
|
||||
|
||||
.animation-container {
|
||||
[class^='skeleton-line-'] {
|
||||
background-color: var(--ide-animation-gradient-1);
|
||||
background-color: var(--ide-animation-gradient-1, $gray-100);
|
||||
|
||||
&::after {
|
||||
background-image: linear-gradient(to right,
|
||||
var(--ide-animation-gradient-1) 0%,
|
||||
var(--ide-animation-gradient-2) 20%,
|
||||
var(--ide-animation-gradient-1) 40%,
|
||||
var(--ide-animation-gradient-1) 100%);
|
||||
var(--ide-animation-gradient-1, $gray-100) 0%,
|
||||
var(--ide-animation-gradient-2, $gray-10) 20%,
|
||||
var(--ide-animation-gradient-1, $gray-100) 40%,
|
||||
var(--ide-animation-gradient-1, $gray-100) 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.idiff.addition {
|
||||
background-color: var(--ide-diff-insert);
|
||||
background-color: var(--ide-diff-insert, $line-added-dark);
|
||||
}
|
||||
|
||||
.idiff.deletion {
|
||||
background-color: var(--ide-diff-remove);
|
||||
background-color: var(--ide-diff-remove, $line-removed-dark);
|
||||
}
|
||||
|
||||
~ .popover {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar.theme-dark {
|
||||
.navbar:not(.theme-white):not(.theme-none) {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.theme-dark ~ .popover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
|
|
@ -32,19 +32,7 @@ To add a new theme, follow the following steps:
|
|||
3. Copy over all the CSS variables from `_dark.scss` to `_solarized_dark.scss` and assign them your own values.
|
||||
Put them under the selector `.ide.theme-solarized-dark`.
|
||||
4. Import this newly created SCSS file in `ide.scss` file in the parent directory.
|
||||
5. To make sure the variables apply to to your theme, add the selector `.ide.theme-solarized-dark` to the top
|
||||
of `_ide_theme_overrides.scss` file. The file should now look like this:
|
||||
|
||||
```scss
|
||||
.ide.theme-dark,
|
||||
.ide.theme-solarized-dark {
|
||||
/* file contents */
|
||||
}
|
||||
```
|
||||
|
||||
This step is temporary until all CSS variables in that file have their
|
||||
default values assigned.
|
||||
6. That's it! Raise a merge request with your newly added theme.
|
||||
5. That's it! Raise a merge request with your newly added theme.
|
||||
|
||||
## Modifying Monaco Themes
|
||||
|
||||
|
|
|
@ -191,8 +191,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
|
||||
params[:application_setting][:import_sources]&.delete("")
|
||||
params[:application_setting][:restricted_visibility_levels]&.delete("")
|
||||
params[:application_setting].delete(:elasticsearch_aws_secret_access_key) if params[:application_setting][:elasticsearch_aws_secret_access_key].blank?
|
||||
params[:application_setting][:required_instance_ci_template] = nil if params[:application_setting][:required_instance_ci_template].blank?
|
||||
|
||||
remove_blank_params_for!(:elasticsearch_aws_secret_access_key, :eks_secret_access_key)
|
||||
|
||||
# TODO Remove domain_blacklist_raw in APIv5 (See https://gitlab.com/gitlab-org/gitlab-foss/issues/67204)
|
||||
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
|
||||
params.delete(:domain_blacklist_raw) if params[:domain_blacklist]
|
||||
|
@ -262,6 +264,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
render action
|
||||
end
|
||||
|
||||
def remove_blank_params_for!(*keys)
|
||||
params[:application_setting].delete_if { |setting, value| setting.to_sym.in?(keys) && value.blank? }
|
||||
end
|
||||
|
||||
# overridden in EE
|
||||
def valid_setting_panels
|
||||
VALID_SETTING_PANELS
|
||||
|
|
|
@ -53,10 +53,16 @@ module MembershipActions
|
|||
end
|
||||
|
||||
def request_access
|
||||
membershipable.request_access(current_user)
|
||||
access_requester = membershipable.request_access(current_user)
|
||||
|
||||
redirect_to polymorphic_path(membershipable),
|
||||
notice: _('Your request for access has been queued for review.')
|
||||
if access_requester.persisted?
|
||||
redirect_to polymorphic_path(membershipable),
|
||||
notice: _('Your request for access has been queued for review.')
|
||||
else
|
||||
redirect_to polymorphic_path(membershipable),
|
||||
alert: _("Your request for access could not be processed: %{error_meesage}") %
|
||||
{ error_meesage: access_requester.errors.full_messages.to_sentence }
|
||||
end
|
||||
end
|
||||
|
||||
def approve_access_request
|
||||
|
|
|
@ -4,6 +4,8 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
|||
include Gitlab::Experimentation::ControllerConcern
|
||||
include InitializesCurrentUserMode
|
||||
|
||||
before_action :verify_confirmed_email!, only: [:new]
|
||||
|
||||
layout 'profile'
|
||||
|
||||
# Overridden from Doorkeeper::AuthorizationsController to
|
||||
|
@ -21,4 +23,13 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
|
|||
render "doorkeeper/authorizations/error"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def verify_confirmed_email!
|
||||
return if current_user&.confirmed?
|
||||
|
||||
pre_auth.error = :unconfirmed_email
|
||||
render "doorkeeper/authorizations/error"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -37,6 +37,8 @@ class Projects::DeployKeysController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
access_denied! unless deploy_key
|
||||
|
||||
if deploy_key.update(update_params)
|
||||
flash[:notice] = _('Deploy key was successfully updated.')
|
||||
redirect_to_repository
|
||||
|
@ -85,10 +87,12 @@ class Projects::DeployKeysController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def update_params
|
||||
permitted_params = [deploy_keys_projects_attributes: [:id, :can_push]]
|
||||
permitted_params = [deploy_keys_projects_attributes: [:can_push]]
|
||||
permitted_params << :title if can?(current_user, :update_deploy_key, deploy_key)
|
||||
|
||||
params.require(:deploy_key).permit(*permitted_params)
|
||||
key_update_params = params.require(:deploy_key).permit(*permitted_params)
|
||||
key_update_params.dig(:deploy_keys_projects_attributes, '0')&.merge!(id: deploy_keys_project.id)
|
||||
key_update_params
|
||||
end
|
||||
|
||||
def authorize_update_deploy_key!
|
||||
|
|
|
@ -14,6 +14,7 @@ class NotificationSetting < ApplicationRecord
|
|||
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
|
||||
message: "already exists in source",
|
||||
allow_nil: true }
|
||||
validate :owns_notification_email, if: :notification_email_changed?
|
||||
|
||||
scope :for_groups, -> { where(source_type: 'Namespace') }
|
||||
|
||||
|
@ -97,6 +98,13 @@ class NotificationSetting < ApplicationRecord
|
|||
def event_enabled?(event)
|
||||
respond_to?(event) && !!public_send(event) # rubocop:disable GitlabSecurity/PublicSend
|
||||
end
|
||||
|
||||
def owns_notification_email
|
||||
return if user.temp_oauth_email?
|
||||
return if notification_email.empty?
|
||||
|
||||
errors.add(:notification_email, _("is not an email you own")) unless user.verified_emails.include?(notification_email)
|
||||
end
|
||||
end
|
||||
|
||||
NotificationSetting.prepend_if_ee('EE::NotificationSetting')
|
||||
|
|
|
@ -238,9 +238,10 @@ class User < ApplicationRecord
|
|||
if previous_changes.key?('email')
|
||||
# Grab previous_email here since previous_changes changes after
|
||||
# #update_emails_with_primary_email and #update_notification_email are called
|
||||
previous_confirmed_at = previous_changes.key?('confirmed_at') ? previous_changes['confirmed_at'][0] : confirmed_at
|
||||
previous_email = previous_changes[:email][0]
|
||||
|
||||
update_emails_with_primary_email(previous_email)
|
||||
update_emails_with_primary_email(previous_confirmed_at, previous_email)
|
||||
update_invalid_gpg_signatures
|
||||
|
||||
if previous_email == notification_email
|
||||
|
@ -342,6 +343,7 @@ class User < ApplicationRecord
|
|||
where('EXISTS (?)',
|
||||
::PersonalAccessToken
|
||||
.where('personal_access_tokens.user_id = users.id')
|
||||
.without_impersonation
|
||||
.expiring_and_not_notified(at).select(1))
|
||||
end
|
||||
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
|
||||
|
@ -756,15 +758,15 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def owns_notification_email
|
||||
return if temp_oauth_email?
|
||||
return if new_record? || temp_oauth_email?
|
||||
|
||||
errors.add(:notification_email, _("is not an email you own")) unless all_emails.include?(notification_email)
|
||||
errors.add(:notification_email, _("is not an email you own")) unless verified_emails.include?(notification_email)
|
||||
end
|
||||
|
||||
def owns_public_email
|
||||
return if public_email.blank?
|
||||
|
||||
errors.add(:public_email, _("is not an email you own")) unless all_emails.include?(public_email)
|
||||
errors.add(:public_email, _("is not an email you own")) unless verified_emails.include?(public_email)
|
||||
end
|
||||
|
||||
def owns_commit_email
|
||||
|
@ -812,13 +814,15 @@ class User < ApplicationRecord
|
|||
# By using an `after_commit` instead of `after_update`, we avoid the recursive callback
|
||||
# scenario, though it then requires us to use the `previous_changes` hash
|
||||
# rubocop: disable CodeReuse/ServiceClass
|
||||
def update_emails_with_primary_email(previous_email)
|
||||
def update_emails_with_primary_email(previous_confirmed_at, previous_email)
|
||||
primary_email_record = emails.find_by(email: email)
|
||||
Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record
|
||||
|
||||
# the original primary email was confirmed, and we want that to carry over. We don't
|
||||
# have access to the original confirmation values at this point, so just set confirmed_at
|
||||
Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at)
|
||||
Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: previous_confirmed_at)
|
||||
|
||||
update_columns(confirmed_at: primary_email_record.confirmed_at) if primary_email_record&.confirmed_at
|
||||
end
|
||||
# rubocop: enable CodeReuse/ServiceClass
|
||||
|
||||
|
@ -1202,18 +1206,20 @@ class User < ApplicationRecord
|
|||
all_emails
|
||||
end
|
||||
|
||||
def all_public_emails
|
||||
all_emails(include_private_email: false)
|
||||
end
|
||||
|
||||
def verified_emails
|
||||
def verified_emails(include_private_email: true)
|
||||
verified_emails = []
|
||||
verified_emails << email if primary_email_verified?
|
||||
verified_emails << private_commit_email
|
||||
verified_emails << private_commit_email if include_private_email
|
||||
verified_emails.concat(emails.confirmed.pluck(:email))
|
||||
verified_emails
|
||||
end
|
||||
|
||||
def public_verified_emails
|
||||
emails = verified_emails(include_private_email: false)
|
||||
emails << email unless temp_oauth_email?
|
||||
emails.uniq
|
||||
end
|
||||
|
||||
def any_email?(check_email)
|
||||
downcased = check_email.downcase
|
||||
|
||||
|
|
|
@ -10,6 +10,12 @@ module Clusters
|
|||
|
||||
def execute(cluster)
|
||||
if validate_params(cluster)
|
||||
token = params.dig(:platform_kubernetes_attributes, :token)
|
||||
|
||||
if token.blank?
|
||||
params[:platform_kubernetes_attributes]&.delete(:token)
|
||||
end
|
||||
|
||||
cluster.update(params)
|
||||
else
|
||||
false
|
||||
|
|
|
@ -26,6 +26,6 @@
|
|||
= f.text_field :eks_access_key_id, class: 'form-control'
|
||||
.form-group
|
||||
= f.label :eks_secret_access_key, 'Secret access key', class: 'label-bold'
|
||||
= f.password_field :eks_secret_access_key, value: @application_setting.eks_secret_access_key, class: 'form-control'
|
||||
= f.password_field :eks_secret_access_key, autocomplete: 'off', class: 'form-control'
|
||||
|
||||
= f.submit 'Save changes', class: "btn btn-success"
|
||||
|
|
|
@ -25,16 +25,10 @@
|
|||
label: s_('ClusterIntegration|CA Certificate'), label_class: 'label-bold',
|
||||
input_group_class: 'gl-field-error-anchor', append: copy_ca_cert_btn
|
||||
|
||||
- show_token_btn = (platform_field.button s_('ClusterIntegration|Show'),
|
||||
type: 'button', class: 'js-show-cluster-token btn btn-default')
|
||||
- copy_token_btn = clipboard_button(text: platform.token, title: s_('ClusterIntegration|Copy Service Token'),
|
||||
class: 'input-group-text btn-default') if cluster.read_only_kubernetes_platform_fields?
|
||||
|
||||
= platform_field.text_field :token, type: 'password', class: 'js-select-on-focus js-cluster-token',
|
||||
required: true, title: s_('ClusterIntegration|Service token is required.'),
|
||||
readonly: cluster.read_only_kubernetes_platform_fields?,
|
||||
label: s_('ClusterIntegration|Service Token'), label_class: 'label-bold',
|
||||
input_group_class: 'gl-field-error-anchor', append: show_token_btn + copy_token_btn
|
||||
= platform_field.password_field :token, type: 'password', class: 'js-select-on-focus js-cluster-token',
|
||||
readonly: cluster.read_only_kubernetes_platform_fields?, autocomplete: 'new-password',
|
||||
label: s_('ClusterIntegration|Enter new Service Token'), label_class: 'label-bold',
|
||||
input_group_class: 'gl-field-error-anchor'
|
||||
|
||||
= platform_field.form_group :authorization_type do
|
||||
= platform_field.check_box :authorization_type, { disabled: true, label: s_('ClusterIntegration|RBAC-enabled cluster'),
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
- help_text = email_change_disabled ? s_("Your account uses dedicated credentials for the \"%{group_name}\" group and can only be updated through SSO.") % { group_name: @user.managing_group.name } : read_only_help_text
|
||||
|
||||
= form.text_field :email, required: true, class: 'input-lg', value: (@user.email unless @user.temp_oauth_email?), help: help_text.html_safe, readonly: readonly || email_change_disabled
|
||||
= form.select :public_email, options_for_select(@user.all_public_emails, selected: @user.public_email),
|
||||
= form.select :public_email, options_for_select(@user.public_verified_emails, selected: @user.public_email),
|
||||
{ help: s_("Profiles|This email will be displayed on your public profile"), include_blank: s_("Profiles|Do not show on profile") },
|
||||
control_class: 'select2 input-lg', disabled: email_change_disabled
|
||||
- commit_email_link_url = help_page_path('user/profile/index', anchor: 'commit-email', target: '_blank')
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- form = local_assigns.fetch(:form)
|
||||
.form-group
|
||||
= form.label :notification_email, class: "label-bold"
|
||||
= form.select :notification_email, @user.all_public_emails, { include_blank: false }, class: "select2", disabled: local_assigns.fetch(:email_change_disabled, nil)
|
||||
= form.select :notification_email, @user.public_verified_emails, { include_blank: false }, class: "select2", disabled: local_assigns.fetch(:email_change_disabled, nil)
|
||||
.help-block
|
||||
= local_assigns.fetch(:help_text, nil)
|
||||
|
|
|
@ -13,4 +13,4 @@
|
|||
|
||||
.table-section.section-30
|
||||
= form_for setting, url: profile_notifications_group_path(group), method: :put, html: { class: 'update-notifications' } do |f|
|
||||
= f.select :notification_email, @user.all_public_emails, { include_blank: 'Global notification email' }, class: 'select2 js-group-notification-email'
|
||||
= f.select :notification_email, @user.public_verified_emails, { include_blank: 'Global notification email' }, class: 'select2 js-group-notification-email'
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
- page_title 'Edit Deploy Key'
|
||||
%h3.page-title Edit Deploy Key
|
||||
%h3.page-title= _('Edit Deploy Key')
|
||||
%hr
|
||||
|
||||
%div
|
||||
= form_for [@project.namespace.becomes(Namespace), @project, @deploy_key], html: { class: 'js-requires-input' } do |f|
|
||||
= form_for [@project.namespace.becomes(Namespace), @project, @deploy_key], include_id: false, html: { class: 'js-requires-input' } do |f|
|
||||
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
|
||||
.form-actions
|
||||
= f.submit 'Save changes', class: 'btn-success btn'
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
.float-left.btn-group.prepend-left-10.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown
|
||||
= link_to "#{display_button_action} #{display_issuable_type}", close_reopen_issuable_path(issuable),
|
||||
method: button_method, class: "#{button_class} btn-#{button_action} #{(add_blocked_class ? 'btn-issue-blocked' : '')}", title: "#{display_button_action} #{display_issuable_type}", data: { qa_selector: 'close_issue_button' }
|
||||
method: button_method, class: "#{button_class} btn-#{button_action} #{(add_blocked_class ? 'btn-issue-blocked' : '')}", title: "#{display_button_action} #{display_issuable_type}", data: { qa_selector: 'close_issue_button', 'close-reopen-url': close_reopen_issuable_path(issuable) }
|
||||
|
||||
= button_tag type: 'button', class: "#{toggle_class} btn-#{button_action}-color",
|
||||
data: { 'dropdown-trigger' => '#issuable-close-menu' }, 'aria-label' => _('Toggle dropdown') do
|
||||
|
|
|
@ -15,9 +15,9 @@ module PersonalAccessTokens
|
|||
with_context(user: user) do
|
||||
notification_service.access_token_about_to_expire(user)
|
||||
|
||||
Rails.logger.info "#{self.class}: Notifying User #{user.id} about expiring tokens" # rubocop:disable Gitlab/RailsLogger
|
||||
Gitlab::AppLogger.info "#{self.class}: Notifying User #{user.id} about expiring tokens"
|
||||
|
||||
user.personal_access_tokens.expiring_and_not_notified(limit_date).update_all(expire_notification_delivered: true)
|
||||
user.personal_access_tokens.without_impersonation.expiring_and_not_notified(limit_date).update_all(expire_notification_delivered: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Prevent emails to user on expiry of impersonation token
|
||||
merge_request: 32140
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update U2F docs for Firefox 67+
|
||||
merge_request: 32289
|
||||
author: Takuya Noguchi
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix alignment of button text on the Edit Release page
|
||||
merge_request: 33104
|
||||
author:
|
||||
type: fixed
|
|
@ -36,6 +36,7 @@ en:
|
|||
access_denied: 'The resource owner or authorization server denied the request.'
|
||||
invalid_scope: 'The requested scope is invalid, unknown, or malformed.'
|
||||
server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.'
|
||||
unconfirmed_email: 'Verify the email address in your account profile before you sign in.'
|
||||
temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.'
|
||||
|
||||
#configuration error messages
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexToPersonalAccessTokenImpersonation < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
INDEX_NAME = 'index_expired_and_not_notified_personal_access_tokens'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index(
|
||||
:personal_access_tokens,
|
||||
[:id, :expires_at],
|
||||
where: "impersonation = FALSE AND revoked = FALSE AND expire_notification_delivered = FALSE",
|
||||
name: INDEX_NAME
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name(
|
||||
:personal_access_tokens,
|
||||
name: INDEX_NAME
|
||||
)
|
||||
end
|
||||
end
|
|
@ -9666,6 +9666,8 @@ CREATE INDEX index_events_on_target_type_and_target_id ON public.events USING bt
|
|||
|
||||
CREATE INDEX index_evidences_on_release_id ON public.evidences USING btree (release_id);
|
||||
|
||||
CREATE INDEX index_expired_and_not_notified_personal_access_tokens ON public.personal_access_tokens USING btree (id, expires_at) WHERE ((impersonation = false) AND (revoked = false) AND (expire_notification_delivered = false));
|
||||
|
||||
CREATE UNIQUE INDEX index_external_pull_requests_on_project_and_branches ON public.external_pull_requests USING btree (project_id, source_branch, target_branch);
|
||||
|
||||
CREATE UNIQUE INDEX index_feature_flag_scopes_on_flag_id_and_environment_scope ON public.operations_feature_flag_scopes USING btree (feature_flag_id, environment_scope);
|
||||
|
@ -13947,6 +13949,7 @@ COPY "schema_migrations" (version) FROM STDIN;
|
|||
20200514000132
|
||||
20200514000340
|
||||
20200515155620
|
||||
20200518091745
|
||||
20200519115908
|
||||
20200519171058
|
||||
20200519194042
|
||||
|
|
|
@ -125,7 +125,7 @@ the database. The following instructions can be used to build OpenSSH 7.5:
|
|||
```shell
|
||||
sudo su -
|
||||
cd /tmp
|
||||
curl --remote-name https://mirrors.evowise.com/pub/OpenBSD/OpenSSH/portable/openssh-7.5p1.tar.gz
|
||||
curl --remote-name https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-7.5p1.tar.gz
|
||||
tar xzvf openssh-7.5p1.tar.gz
|
||||
yum install rpm-build gcc make wget openssl-devel krb5-devel pam-devel libX11-devel xmkmf libXt-devel
|
||||
```
|
||||
|
|
|
@ -1204,7 +1204,7 @@ PUT /projects/:id
|
|||
| `approvals_before_merge` | integer | no | **(STARTER)** How many approvers should approve merge request by default |
|
||||
| `external_authorization_classification_label` | string | no | **(PREMIUM)** The classification label for the project |
|
||||
| `mirror` | boolean | no | **(STARTER)** Enables pull mirroring in a project |
|
||||
| `mirror_user_id` | integer | no | **(STARTER)** User responsible for all the activity surrounding a pull mirror event |
|
||||
| `mirror_user_id` | integer | no | **(STARTER)** User responsible for all the activity surrounding a pull mirror event. Can only be set by admins. |
|
||||
| `mirror_trigger_builds` | boolean | no | **(STARTER)** Pull mirroring triggers builds |
|
||||
| `only_mirror_protected_branches` | boolean | no | **(STARTER)** Only mirror protected branches |
|
||||
| `mirror_overwrites_diverged_branches` | boolean | no | **(STARTER)** Pull mirror overwrites diverged branches |
|
||||
|
|
|
@ -333,53 +333,53 @@ tenses, words, and phrases:
|
|||
|
||||
- Use common contractions when it helps create a friendly and informal tone, especially in tutorials, instructional documentation, and [UIs](https://design.gitlab.com/content/punctuation/#contractions).
|
||||
|
||||
| Do | Don't |
|
||||
|----------|-----------|
|
||||
| it's | it is |
|
||||
| can't | cannot |
|
||||
| wouldn't | would not |
|
||||
| you're | you are |
|
||||
| you've | you have |
|
||||
| haven't | have not |
|
||||
| don't | do not |
|
||||
| we're | we are |
|
||||
| that's | that is |
|
||||
| won't | will not |
|
||||
| Do | Don't |
|
||||
|----------|-----------|
|
||||
| it's | it is |
|
||||
| can't | cannot |
|
||||
| wouldn't | would not |
|
||||
| you're | you are |
|
||||
| you've | you have |
|
||||
| haven't | have not |
|
||||
| don't | do not |
|
||||
| we're | we are |
|
||||
| that's | that is |
|
||||
| won't | will not |
|
||||
|
||||
- Avoid less common contractions:
|
||||
|
||||
| Do | Don't |
|
||||
|--------------|-------------|
|
||||
| he would | he'd |
|
||||
| it will | it'll |
|
||||
| should have | should've |
|
||||
| there would | there'd |
|
||||
| Do | Don't |
|
||||
|--------------|-------------|
|
||||
| he would | he'd |
|
||||
| it will | it'll |
|
||||
| should have | should've |
|
||||
| there would | there'd |
|
||||
|
||||
- Do not use contractions with a proper noun and a verb. For example:
|
||||
|
||||
| Do | Don't |
|
||||
|----------------------|---------------------|
|
||||
| GitLab is creating X | GitLab's creating X |
|
||||
| Do | Don't |
|
||||
|----------------------|---------------------|
|
||||
| GitLab is creating X | GitLab's creating X |
|
||||
|
||||
- Do not use contractions when you need to emphasize a negative. For example:
|
||||
|
||||
| Do | Don't |
|
||||
|-----------------------------|----------------------------|
|
||||
| Do **not** install X with Y | **Don't** install X with Y |
|
||||
| Do | Don't |
|
||||
|-----------------------------|----------------------------|
|
||||
| Do **not** install X with Y | **Don't** install X with Y |
|
||||
|
||||
- Do not use contractions in reference documentation. For example:
|
||||
|
||||
| Do | Don't |
|
||||
|------------------------------------------|----------------------------|
|
||||
| Do **not** set a limit greater than 1000 | **Don't** set a limit greater than 1000 |
|
||||
| For `parameter1`, the default is 10 | For `parameter1`, the default's 10 |
|
||||
| Do | Don't |
|
||||
|------------------------------------------|----------------------------|
|
||||
| Do **not** set a limit greater than 1000 | **Don't** set a limit greater than 1000 |
|
||||
| For `parameter1`, the default is 10 | For `parameter1`, the default's 10 |
|
||||
|
||||
- Avoid contractions in error messages. Examples:
|
||||
|
||||
| Do | Don't |
|
||||
|------------------------------------------|----------------------------|
|
||||
| Requests to localhost are not allowed | Requests to localhost aren't allowed |
|
||||
| Specified URL cannot be used | Specified URL can't be used |
|
||||
| Do | Don't |
|
||||
|------------------------------------------|----------------------------|
|
||||
| Requests to localhost are not allowed | Requests to localhost aren't allowed |
|
||||
| Specified URL cannot be used | Specified URL can't be used |
|
||||
|
||||
<!-- vale on -->
|
||||
|
||||
|
|
|
@ -59,6 +59,8 @@ it. The restriction for visibility levels on the application setting level also
|
|||
applies to groups, so if that's set to internal, the explore page will be empty
|
||||
for anonymous users. The group page now has a visibility level icon.
|
||||
|
||||
Admin users cannot create subgroups or projects with higher visibility level than that of the parent group.
|
||||
|
||||
## Visibility of users
|
||||
|
||||
The public page of a user, located at `/username`, is always visible whether
|
||||
|
|
|
@ -502,9 +502,10 @@ This will disable the option for all users who previously had permissions to
|
|||
operate project memberships, so no new users can be added. Furthermore, any
|
||||
request to add a new user to a project through API will not be possible.
|
||||
|
||||
#### IP access restriction **(ULTIMATE)**
|
||||
#### IP access restriction **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1985) in [GitLab Ultimate and Gold](https://about.gitlab.com/pricing/) 12.0.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1985) in [GitLab Ultimate and Gold](https://about.gitlab.com/pricing/) 12.0.
|
||||
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/215410) to [GitLab Premium and Silver](https://about.gitlab.com/pricing/) in 13.1.
|
||||
|
||||
To make sure only people from within your organization can access particular
|
||||
resources, you have the option to restrict access to groups and their
|
||||
|
|
|
@ -73,11 +73,11 @@ following desktop browsers:
|
|||
|
||||
- Chrome
|
||||
- Edge
|
||||
- Firefox (disabled by default)
|
||||
- Firefox 67+
|
||||
- Opera
|
||||
|
||||
NOTE: **Note:**
|
||||
For Firefox, you can enable the FIDO U2F API in
|
||||
For Firefox 47-66, you can enable the FIDO U2F API in
|
||||
[about:config](https://support.mozilla.org/en-US/kb/about-config-editor-firefox).
|
||||
Search for `security.webauth.u2f` and double click on it to toggle to `true`.
|
||||
|
||||
|
|
|
@ -311,3 +311,9 @@ Attaching a [Zoom](https://zoom.us) call an issue
|
|||
results in a **Join Zoom meeting** button at the top of the issue, just under the header.
|
||||
|
||||
Read more how to [add or remove a zoom meeting](associate_zoom_meeting.md).
|
||||
|
||||
### Publish an issue **(ULTIMATE)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30906) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.1.
|
||||
|
||||
If a status page application is associated with the project, you can use the `/publish` [quick action](../quick_actions.md) to publish the issue. Refer to [GitLab Status Page](../status_page/index.md) for more information.
|
||||
|
|
|
@ -51,6 +51,7 @@ The following quick actions are applicable to descriptions, discussions and thre
|
|||
| `/move <path/to/project>` | ✓ | | | Move this issue to another project. |
|
||||
| `/parent_epic <epic>` | | | ✓ | Set parent epic to `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic ([introduced in GitLab 12.1](https://gitlab.com/gitlab-org/gitlab/-/issues/10556)). **(ULTIMATE)** |
|
||||
| `/promote` | ✓ | | | Promote issue to epic. **(PREMIUM)** |
|
||||
| `/publish` | ✓ | | | Publish issue to an associated [Status Page](status_page/index.md) ([Introduced in GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30906)) **(ULTIMATE)** |
|
||||
| `/reassign @user1 @user2` | ✓ | ✓ | | Change assignee. **(STARTER)** |
|
||||
| `/relabel ~label1 ~label2` | ✓ | ✓ | ✓ | Replace existing labels with those specified. |
|
||||
| `/relate #issue1 #issue2` | ✓ | | | Mark issues as related. **(STARTER)** |
|
||||
|
|
|
@ -83,12 +83,15 @@ The incident detail page shows detailed information about a particular incident.
|
|||
|
||||
To publish an Incident, you first need to create an issue in the Project you enabled the Status Page settings in.
|
||||
|
||||
Once this issue is created, a background worker will publish the issue onto the Status Page using the credentials you provided during setup.
|
||||
Issues are not published to the Status Page by default. Use the `/publish` [quick action](../quick_actions.md) in an issue to publish the issue. Only [project or group owners](../../permissions.md) are permitted to publish issues.
|
||||
|
||||
After the quick action is used, a background worker publishes the issue onto the Status Page using the credentials you provided during setup.
|
||||
|
||||
Since all incidents are published publicly, user and group mentions are anonymized with `Incident Responder`,
|
||||
and titles of non-public [GitLab references](../../markdown.md#special-gitlab-references) are removed.
|
||||
|
||||
NOTE: **Note:**
|
||||
Confidential issues are not published. If a published issue is made confidential it will be unpublished.
|
||||
Confidential issues can't be published. If you make a published issue confidential, it will be unpublished.
|
||||
|
||||
### Publishing updates
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
module API
|
||||
class GroupImport < Grape::API
|
||||
helpers Helpers::FileUploadHelpers
|
||||
|
||||
helpers do
|
||||
def parent_group
|
||||
find_group!(params[:parent_id]) if params[:parent_id].present?
|
||||
|
@ -49,29 +51,20 @@ module API
|
|||
params do
|
||||
requires :path, type: String, desc: 'Group path'
|
||||
requires :name, type: String, desc: 'Group name'
|
||||
requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The group export file to be imported'
|
||||
optional :parent_id, type: Integer, desc: "The ID of the parent group that the group will be imported into. Defaults to the current user's namespace."
|
||||
optional 'file.path', type: String, desc: 'Path to locally stored body (generated by Workhorse)'
|
||||
optional 'file.name', type: String, desc: 'Real filename as send in Content-Disposition (generated by Workhorse)'
|
||||
optional 'file.type', type: String, desc: 'Real content type as send in Content-Type (generated by Workhorse)'
|
||||
optional 'file.size', type: Integer, desc: 'Real size of file (generated by Workhorse)'
|
||||
optional 'file.md5', type: String, desc: 'MD5 checksum of the file (generated by Workhorse)'
|
||||
optional 'file.sha1', type: String, desc: 'SHA1 checksum of the file (generated by Workhorse)'
|
||||
optional 'file.sha256', type: String, desc: 'SHA256 checksum of the file (generated by Workhorse)'
|
||||
end
|
||||
post 'import' do
|
||||
authorize_create_group!
|
||||
require_gitlab_workhorse!
|
||||
|
||||
uploaded_file = UploadedFile.from_params(params, :file, ImportExportUploader.workhorse_local_upload_path)
|
||||
|
||||
bad_request!('Unable to process group import file') unless uploaded_file
|
||||
validate_file!
|
||||
|
||||
group_params = {
|
||||
path: params[:path],
|
||||
name: params[:name],
|
||||
parent_id: params[:parent_id],
|
||||
visibility_level: closest_allowed_visibility_level,
|
||||
import_export_upload: ImportExportUpload.new(import_file: uploaded_file)
|
||||
import_export_upload: ImportExportUpload.new(import_file: params[:file])
|
||||
}
|
||||
|
||||
group = ::Groups::CreateService.new(current_user, group_params).execute
|
||||
|
|
|
@ -444,6 +444,8 @@ module API
|
|||
|
||||
not_found!("Source Project") unless fork_from_project
|
||||
|
||||
authorize! :fork_project, fork_from_project
|
||||
|
||||
result = ::Projects::ForkService.new(fork_from_project, current_user).execute(user_project)
|
||||
|
||||
if result
|
||||
|
|
|
@ -6,6 +6,8 @@ module API
|
|||
class Repositories < Grape::API
|
||||
include PaginationParams
|
||||
|
||||
helpers ::API::Helpers::HeadersHelpers
|
||||
|
||||
before { authorize! :download_code, user_project }
|
||||
|
||||
params do
|
||||
|
@ -67,6 +69,8 @@ module API
|
|||
get ':id/repository/blobs/:sha/raw' do
|
||||
assign_blob_vars!
|
||||
|
||||
no_cache_headers
|
||||
|
||||
send_git_blob @repo, @blob
|
||||
end
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ module Gitlab
|
|||
|
||||
def store_pull_request_error(pull_request, ex)
|
||||
backtrace = Gitlab::BacktraceCleaner.clean_backtrace(ex.backtrace)
|
||||
error = { type: :pull_request, iid: pull_request.iid, errors: ex.message, trace: backtrace, raw_response: pull_request.raw }
|
||||
error = { type: :pull_request, iid: pull_request.iid, errors: ex.message, trace: backtrace, raw_response: pull_request.raw&.to_json }
|
||||
|
||||
Gitlab::ErrorTracking.log_exception(ex, error)
|
||||
|
||||
|
|
|
@ -12,14 +12,15 @@ module Gitlab
|
|||
WIKI = RepoType.new(
|
||||
name: :wiki,
|
||||
access_checker_class: Gitlab::GitAccessWiki,
|
||||
repository_resolver: -> (project) { project&.wiki&.repository },
|
||||
repository_resolver: -> (container) { container&.wiki&.repository },
|
||||
project_resolver: -> (container) { container.is_a?(Project) ? container : nil },
|
||||
suffix: :wiki
|
||||
).freeze
|
||||
SNIPPET = RepoType.new(
|
||||
name: :snippet,
|
||||
access_checker_class: Gitlab::GitAccessSnippet,
|
||||
repository_resolver: -> (snippet) { snippet&.repository },
|
||||
container_resolver: -> (id) { Snippet.find_by_id(id) },
|
||||
container_class: Snippet,
|
||||
project_resolver: -> (snippet) { snippet&.project },
|
||||
guest_read_ability: :read_snippet
|
||||
).freeze
|
||||
|
@ -42,16 +43,12 @@ module Gitlab
|
|||
end
|
||||
|
||||
def self.parse(gl_repository)
|
||||
type_name, _id = gl_repository.split('-').first
|
||||
type = types[type_name]
|
||||
result = ::Gitlab::GlRepository::Identifier.new(gl_repository)
|
||||
|
||||
unless type
|
||||
raise ArgumentError, "Invalid GL Repository \"#{gl_repository}\""
|
||||
end
|
||||
repo_type = result.repo_type
|
||||
container = result.fetch_container!
|
||||
|
||||
container = type.fetch_container!(gl_repository)
|
||||
|
||||
[container, type.project_for(container), type]
|
||||
[container, repo_type.project_for(container), repo_type]
|
||||
end
|
||||
|
||||
def self.default_type
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
class GlRepository
|
||||
class Identifier
|
||||
attr_reader :gl_repository, :repo_type
|
||||
|
||||
def initialize(gl_repository)
|
||||
@gl_repository = gl_repository
|
||||
@segments = gl_repository.split('-')
|
||||
|
||||
raise_error if segments.size > 3
|
||||
|
||||
@repo_type = find_repo_type
|
||||
@container_id = find_container_id
|
||||
@container_class = find_container_class
|
||||
end
|
||||
|
||||
def fetch_container!
|
||||
container_class.find_by_id(container_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :segments, :container_class, :container_id
|
||||
|
||||
def find_repo_type
|
||||
type_name = three_segments_format? ? segments.last : segments.first
|
||||
type = Gitlab::GlRepository.types[type_name]
|
||||
|
||||
raise_error unless type
|
||||
|
||||
type
|
||||
end
|
||||
|
||||
def find_container_class
|
||||
if three_segments_format?
|
||||
case segments[0]
|
||||
when 'project'
|
||||
Project
|
||||
when 'group'
|
||||
Group
|
||||
else
|
||||
raise_error
|
||||
end
|
||||
else
|
||||
repo_type.container_class
|
||||
end
|
||||
end
|
||||
|
||||
def find_container_id
|
||||
id = Integer(segments[1], 10, exception: false)
|
||||
|
||||
raise_error unless id
|
||||
|
||||
id
|
||||
end
|
||||
|
||||
# gl_repository can either have 2 or 3 segments:
|
||||
# "wiki-1" is the older 2-segment format, where container is implied.
|
||||
# "group-1-wiki" is the newer 3-segment format, including container information.
|
||||
#
|
||||
# TODO: convert all 2-segment format to 3-segment:
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/219192
|
||||
def three_segments_format?
|
||||
segments.size == 3
|
||||
end
|
||||
|
||||
def raise_error
|
||||
raise ArgumentError, "Invalid GL Repository \"#{gl_repository}\""
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,7 +6,7 @@ module Gitlab
|
|||
attr_reader :name,
|
||||
:access_checker_class,
|
||||
:repository_resolver,
|
||||
:container_resolver,
|
||||
:container_class,
|
||||
:project_resolver,
|
||||
:guest_read_ability,
|
||||
:suffix
|
||||
|
@ -15,36 +15,27 @@ module Gitlab
|
|||
name:,
|
||||
access_checker_class:,
|
||||
repository_resolver:,
|
||||
container_resolver: default_container_resolver,
|
||||
container_class: default_container_class,
|
||||
project_resolver: nil,
|
||||
guest_read_ability: :download_code,
|
||||
suffix: nil)
|
||||
@name = name
|
||||
@access_checker_class = access_checker_class
|
||||
@repository_resolver = repository_resolver
|
||||
@container_resolver = container_resolver
|
||||
@container_class = container_class
|
||||
@project_resolver = project_resolver
|
||||
@guest_read_ability = guest_read_ability
|
||||
@suffix = suffix
|
||||
end
|
||||
|
||||
def identifier_for_container(container)
|
||||
if container.is_a?(Group)
|
||||
return "#{container.class.name.underscore}-#{container.id}-#{name}"
|
||||
end
|
||||
|
||||
"#{name}-#{container.id}"
|
||||
end
|
||||
|
||||
def fetch_id(identifier)
|
||||
match = /\A#{name}-(?<id>\d+)\z/.match(identifier)
|
||||
match[:id] if match
|
||||
end
|
||||
|
||||
def fetch_container!(identifier)
|
||||
id = fetch_id(identifier)
|
||||
|
||||
raise ArgumentError, "Invalid GL Repository \"#{identifier}\"" unless id
|
||||
|
||||
container_resolver.call(id)
|
||||
end
|
||||
|
||||
def wiki?
|
||||
self == WIKI
|
||||
end
|
||||
|
@ -85,8 +76,8 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def default_container_resolver
|
||||
-> (id) { Project.find_by_id(id) }
|
||||
def default_container_class
|
||||
Project
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -21,7 +21,7 @@ module Gitlab
|
|||
project_id: project.id,
|
||||
project: project.path,
|
||||
namespace: project.namespace.path,
|
||||
return_url: return_url,
|
||||
return_url: sanitize_url(return_url),
|
||||
is_supported_content: supported_content?.to_s,
|
||||
base_url: Gitlab::Routing.url_helpers.project_show_sse_path(project, full_path)
|
||||
}
|
||||
|
@ -52,6 +52,10 @@ module Gitlab
|
|||
def full_path
|
||||
"#{ref}/#{file_path}"
|
||||
end
|
||||
|
||||
def sanitize_url(url)
|
||||
url if Gitlab::UrlSanitizer.valid_web?(url)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
module Gitlab
|
||||
class UrlSanitizer
|
||||
ALLOWED_SCHEMES = %w[http https ssh git].freeze
|
||||
ALLOWED_WEB_SCHEMES = %w[http https].freeze
|
||||
|
||||
def self.sanitize(content)
|
||||
regexp = URI::DEFAULT_PARSER.make_regexp(ALLOWED_SCHEMES)
|
||||
|
@ -12,17 +13,21 @@ module Gitlab
|
|||
content.gsub(regexp, '')
|
||||
end
|
||||
|
||||
def self.valid?(url)
|
||||
def self.valid?(url, allowed_schemes: ALLOWED_SCHEMES)
|
||||
return false unless url.present?
|
||||
return false unless url.is_a?(String)
|
||||
|
||||
uri = Addressable::URI.parse(url.strip)
|
||||
|
||||
ALLOWED_SCHEMES.include?(uri.scheme)
|
||||
allowed_schemes.include?(uri.scheme)
|
||||
rescue Addressable::URI::InvalidURIError
|
||||
false
|
||||
end
|
||||
|
||||
def self.valid_web?(url)
|
||||
valid?(url, allowed_schemes: ALLOWED_WEB_SCHEMES)
|
||||
end
|
||||
|
||||
def initialize(url, credentials: nil)
|
||||
%i[user password].each do |symbol|
|
||||
credentials[symbol] = credentials[symbol].presence if credentials&.key?(symbol)
|
||||
|
|
|
@ -4673,9 +4673,6 @@ msgstr ""
|
|||
msgid "ClusterIntegration|Copy Kubernetes cluster name"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Copy Service Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Could not load IAM roles"
|
||||
msgstr ""
|
||||
|
||||
|
@ -4754,6 +4751,9 @@ msgstr ""
|
|||
msgid "ClusterIntegration|Enabled stack"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Enter new Service Token"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Enter the details for your Amazon EKS Kubernetes cluster"
|
||||
msgstr ""
|
||||
|
||||
|
@ -4838,9 +4838,6 @@ msgstr ""
|
|||
msgid "ClusterIntegration|Helm streamlines installing and managing Kubernetes applications. Tiller runs inside of your Kubernetes Cluster, and manages releases of your charts."
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Hide"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|If you are setting up multiple clusters and are using Auto DevOps, %{help_link_start}read this first%{help_link_end}."
|
||||
msgstr ""
|
||||
|
||||
|
@ -5237,9 +5234,6 @@ msgstr ""
|
|||
msgid "ClusterIntegration|Set the global mode for the WAF in this cluster. This can be overridden at the environmental level."
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Show"
|
||||
msgstr ""
|
||||
|
||||
msgid "ClusterIntegration|Something went wrong on our end."
|
||||
msgstr ""
|
||||
|
||||
|
@ -22495,9 +22489,6 @@ msgstr ""
|
|||
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches."
|
||||
msgstr ""
|
||||
|
||||
msgid "This user will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches. Upon creation or when reassigning you can only assign yourself to be the mirror user."
|
||||
msgstr ""
|
||||
|
||||
msgid "This variable can not be masked."
|
||||
msgstr ""
|
||||
|
||||
|
@ -25322,6 +25313,9 @@ msgstr ""
|
|||
msgid "You will be removed from existing projects/groups"
|
||||
msgstr ""
|
||||
|
||||
msgid "You will be the author of all events in the activity feed that are the result of an update, like new branches being created or new commits being pushed to existing branches."
|
||||
msgstr ""
|
||||
|
||||
msgid "You will first need to set up Jira Integration to use this feature."
|
||||
msgstr ""
|
||||
|
||||
|
@ -25586,6 +25580,9 @@ msgstr ""
|
|||
msgid "Your projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your request for access could not be processed: %{error_meesage}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your request for access has been queued for review."
|
||||
msgstr ""
|
||||
|
||||
|
@ -26024,6 +26021,9 @@ msgstr ""
|
|||
msgid "email '%{email}' does not match the allowed domain of '%{email_domain}'"
|
||||
msgstr ""
|
||||
|
||||
msgid "email '%{email}' is not a verified email."
|
||||
msgstr ""
|
||||
|
||||
msgid "enabled"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -162,6 +162,46 @@ describe Admin::ApplicationSettingsController do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'PATCH #integrations' do
|
||||
before do
|
||||
stub_feature_flags(instance_level_integrations: false)
|
||||
sign_in(admin)
|
||||
end
|
||||
|
||||
describe 'EKS integration' do
|
||||
let(:application_setting) { ApplicationSetting.current }
|
||||
let(:settings_params) do
|
||||
{
|
||||
eks_integration_enabled: '1',
|
||||
eks_account_id: '123456789012',
|
||||
eks_access_key_id: 'dummy access key',
|
||||
eks_secret_access_key: 'dummy secret key'
|
||||
}
|
||||
end
|
||||
|
||||
it 'updates EKS settings' do
|
||||
patch :integrations, params: { application_setting: settings_params }
|
||||
|
||||
expect(application_setting.eks_integration_enabled).to be_truthy
|
||||
expect(application_setting.eks_account_id).to eq '123456789012'
|
||||
expect(application_setting.eks_access_key_id).to eq 'dummy access key'
|
||||
expect(application_setting.eks_secret_access_key).to eq 'dummy secret key'
|
||||
end
|
||||
|
||||
context 'secret access key is blank' do
|
||||
let(:settings_params) { { eks_secret_access_key: '' } }
|
||||
|
||||
it 'does not update the secret key' do
|
||||
application_setting.update!(eks_secret_access_key: 'dummy secret key')
|
||||
|
||||
patch :integrations, params: { application_setting: settings_params }
|
||||
|
||||
expect(application_setting.reload.eks_secret_access_key).to eq 'dummy secret key'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT #reset_registration_token' do
|
||||
before do
|
||||
sign_in(admin)
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Oauth::AuthorizationsController do
|
||||
let(:user) { create(:user) }
|
||||
let!(:application) { create(:oauth_application, scopes: 'api read_user', redirect_uri: 'http://example.com') }
|
||||
let(:params) do
|
||||
{
|
||||
|
@ -19,53 +18,68 @@ describe Oauth::AuthorizationsController do
|
|||
end
|
||||
|
||||
describe 'GET #new' do
|
||||
context 'without valid params' do
|
||||
it 'returns 200 code and renders error view' do
|
||||
get :new
|
||||
context 'when the user is confirmed' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
context 'without valid params' do
|
||||
it 'returns 200 code and renders error view' do
|
||||
get :new
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template('doorkeeper/authorizations/error')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid params' do
|
||||
render_views
|
||||
|
||||
it 'returns 200 code and renders view' do
|
||||
get :new, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template('doorkeeper/authorizations/new')
|
||||
end
|
||||
|
||||
it 'deletes session.user_return_to and redirects when skip authorization' do
|
||||
application.update(trusted: true)
|
||||
request.session['user_return_to'] = 'http://example.com'
|
||||
|
||||
get :new, params: params
|
||||
|
||||
expect(request.session['user_return_to']).to be_nil
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
end
|
||||
|
||||
context 'when there is already an access token for the application' do
|
||||
context 'when the request scope matches any of the created token scopes' do
|
||||
before do
|
||||
scopes = Doorkeeper::OAuth::Scopes.from_string('api')
|
||||
|
||||
allow(Doorkeeper.configuration).to receive(:scopes).and_return(scopes)
|
||||
|
||||
create :oauth_access_token, application: application, resource_owner_id: user.id, scopes: scopes
|
||||
end
|
||||
|
||||
it 'authorizes the request and redirects' do
|
||||
get :new, params: params
|
||||
|
||||
expect(request.session['user_return_to']).to be_nil
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user is unconfirmed' do
|
||||
let(:user) { create(:user, confirmed_at: nil) }
|
||||
|
||||
it 'returns 200 and renders error view' do
|
||||
get :new, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template('doorkeeper/authorizations/error')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid params' do
|
||||
render_views
|
||||
|
||||
it 'returns 200 code and renders view' do
|
||||
get :new, params: params
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template('doorkeeper/authorizations/new')
|
||||
end
|
||||
|
||||
it 'deletes session.user_return_to and redirects when skip authorization' do
|
||||
application.update(trusted: true)
|
||||
request.session['user_return_to'] = 'http://example.com'
|
||||
|
||||
get :new, params: params
|
||||
|
||||
expect(request.session['user_return_to']).to be_nil
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
end
|
||||
|
||||
context 'when there is already an access token for the application' do
|
||||
context 'when the request scope matches any of the created token scopes' do
|
||||
before do
|
||||
scopes = Doorkeeper::OAuth::Scopes.from_string('api')
|
||||
|
||||
allow(Doorkeeper.configuration).to receive(:scopes).and_return(scopes)
|
||||
|
||||
create :oauth_access_token, application: application, resource_owner_id: user.id, scopes: scopes
|
||||
end
|
||||
|
||||
it 'authorizes the request and redirects' do
|
||||
get :new, params: params
|
||||
|
||||
expect(request.session['user_return_to']).to be_nil
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,8 +5,8 @@ require 'spec_helper'
|
|||
describe Profiles::NotificationsController do
|
||||
let(:user) do
|
||||
create(:user) do |user|
|
||||
user.emails.create(email: 'original@example.com')
|
||||
user.emails.create(email: 'new@example.com')
|
||||
user.emails.create(email: 'original@example.com', confirmed_at: Time.current)
|
||||
user.emails.create(email: 'new@example.com', confirmed_at: Time.current)
|
||||
user.notification_email = 'original@example.com'
|
||||
user.save!
|
||||
end
|
||||
|
|
|
@ -256,7 +256,7 @@ describe Projects::DeployKeysController do
|
|||
end
|
||||
|
||||
def deploy_key_params(title, can_push)
|
||||
deploy_keys_projects_attributes = { '0' => { id: deploy_keys_project, can_push: can_push } }
|
||||
deploy_keys_projects_attributes = { '0' => { can_push: can_push } }
|
||||
{ deploy_key: { title: title, deploy_keys_projects_attributes: deploy_keys_projects_attributes } }
|
||||
end
|
||||
|
||||
|
@ -300,6 +300,42 @@ describe Projects::DeployKeysController do
|
|||
expect { subject }.to change { deploy_keys_project.reload.can_push }.from(false).to(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a different deploy key id param is injected' do
|
||||
let(:extra_params) { deploy_key_params('updated title', '1') }
|
||||
let(:hacked_params) do
|
||||
extra_params.reverse_merge(id: other_deploy_key_id,
|
||||
namespace_id: project.namespace,
|
||||
project_id: project)
|
||||
end
|
||||
|
||||
subject { put :update, params: hacked_params }
|
||||
|
||||
context 'and that deploy key id exists' do
|
||||
let(:other_project) { create(:project) }
|
||||
let(:other_deploy_key) do
|
||||
key = create(:deploy_key)
|
||||
project.deploy_keys << key
|
||||
key
|
||||
end
|
||||
|
||||
let(:other_deploy_key_id) { other_deploy_key.id }
|
||||
|
||||
it 'does not update the can_push attribute' do
|
||||
expect { subject }.not_to change { deploy_key.deploy_keys_project_for(project).can_push }
|
||||
end
|
||||
end
|
||||
|
||||
context 'and that deploy key id does not exist' do
|
||||
let(:other_deploy_key_id) { 9999 }
|
||||
|
||||
it 'returns 404' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with admin as project maintainer' do
|
||||
|
|
|
@ -48,6 +48,10 @@ FactoryBot.define do
|
|||
after(:build) { |user, _| user.block! }
|
||||
end
|
||||
|
||||
trait :unconfirmed do
|
||||
confirmed_at { nil }
|
||||
end
|
||||
|
||||
trait :with_avatar do
|
||||
avatar { fixture_file_upload('spec/fixtures/dk.png') }
|
||||
end
|
||||
|
|
|
@ -39,7 +39,7 @@ describe 'User Cluster', :js do
|
|||
expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value)
|
||||
.to have_content('http://example.com')
|
||||
expect(page.find_field('cluster[platform_kubernetes_attributes][token]').value)
|
||||
.to have_content('my-token')
|
||||
.to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'OAuth Provider' do
|
||||
describe 'Standard OAuth Authorization' do
|
||||
let(:application) { create(:oauth_application, scopes: 'read_user') }
|
||||
|
||||
before do
|
||||
sign_in(user)
|
||||
|
||||
visit oauth_authorization_path(client_id: application.uid,
|
||||
redirect_uri: application.redirect_uri.split.first,
|
||||
response_type: 'code',
|
||||
state: 'my_state',
|
||||
scope: 'read_user')
|
||||
end
|
||||
|
||||
it_behaves_like 'Secure OAuth Authorizations'
|
||||
end
|
||||
end
|
|
@ -46,7 +46,7 @@ describe 'User Cluster', :js do
|
|||
expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value)
|
||||
.to have_content('http://example.com')
|
||||
expect(page.find_field('cluster[platform_kubernetes_attributes][token]').value)
|
||||
.to have_content('my-token')
|
||||
.to be_empty
|
||||
end
|
||||
|
||||
it 'user sees RBAC is enabled by default' do
|
||||
|
|
|
@ -82,28 +82,6 @@ describe('Clusters', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('showToken', () => {
|
||||
it('should update token field type', () => {
|
||||
cluster.showTokenButton.click();
|
||||
|
||||
expect(cluster.tokenField.getAttribute('type')).toEqual('text');
|
||||
|
||||
cluster.showTokenButton.click();
|
||||
|
||||
expect(cluster.tokenField.getAttribute('type')).toEqual('password');
|
||||
});
|
||||
|
||||
it('should update show token button text', () => {
|
||||
cluster.showTokenButton.click();
|
||||
|
||||
expect(cluster.showTokenButton.textContent).toEqual('Hide');
|
||||
|
||||
cluster.showTokenButton.click();
|
||||
|
||||
expect(cluster.showTokenButton.textContent).toEqual('Show');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkForNewInstalls', () => {
|
||||
const INITIAL_APP_MAP = {
|
||||
helm: { status: null, title: 'Helm Tiller' },
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
<div class="description" updated-at="">
|
||||
<div class="md issue-realtime-trigger-pulse">
|
||||
<svg
|
||||
id="mermaid-1587752414912"
|
||||
width="100%"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="max-width: 185.35000610351562px;"
|
||||
viewBox="0 0 185.35000610351562 50.5"
|
||||
class="mermaid"
|
||||
>
|
||||
<g transform="translate(0, 0)">
|
||||
<g class="output">
|
||||
<g class="clusters"></g>
|
||||
<g class="edgePaths"></g>
|
||||
<g class="edgeLabels"></g>
|
||||
<g class="nodes">
|
||||
<g
|
||||
class="node js-issuable-actions btn-close clickable"
|
||||
style="opacity: 1;"
|
||||
id="A"
|
||||
transform="translate(92.67500305175781,25.25)"
|
||||
title="click to PUT"
|
||||
>
|
||||
<a
|
||||
class="js-issuable-actions btn-close clickable"
|
||||
href="https://invalid"
|
||||
rel="noopener"
|
||||
>
|
||||
<rect
|
||||
rx="0"
|
||||
ry="0"
|
||||
x="-84.67500305175781"
|
||||
y="-17.25"
|
||||
width="169.35000610351562"
|
||||
height="34.5"
|
||||
class="label-container"
|
||||
></rect>
|
||||
<g class="label" transform="translate(0,0)">
|
||||
<g transform="translate(-74.67500305175781,-7.25)">
|
||||
<text style="">
|
||||
<tspan xml:space="preserve" dy="1em" x="1">Click to send a PUT request</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</a>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<text class="source" display="none">
|
||||
Click to send a PUT request
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
<textarea
|
||||
data-update-url="/h5bp/html5-boilerplate/-/issues/35.json"
|
||||
dir="auto"
|
||||
class="hidden js-task-list-field"
|
||||
></textarea>
|
||||
<div class="modal-open recaptcha-modal js-recaptcha-modal" style="display: none;">
|
||||
<div role="dialog" tabindex="-1" class="modal d-block">
|
||||
<div role="document" class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title float-left">Please solve the reCAPTCHA</h4>
|
||||
<button type="button" data-dismiss="modal" aria-label="Close" class="close float-right">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div>
|
||||
<p>We want to be sure it is you, please confirm you are not a robot.</p>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<!---->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop fade show"></div>
|
||||
</div>
|
||||
</div>
|
|
@ -18,6 +18,7 @@ describe('Issue', () => {
|
|||
preloadFixtures('issues/closed-issue.html');
|
||||
preloadFixtures('issues/issue-with-task-list.html');
|
||||
preloadFixtures('issues/open-issue.html');
|
||||
preloadFixtures('static/issue_with_mermaid_graph.html');
|
||||
|
||||
function expectErrorMessage() {
|
||||
const $flashMessage = $('div.flash-alert');
|
||||
|
@ -228,4 +229,30 @@ describe('Issue', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when not displaying blocked warning', () => {
|
||||
describe('when clicking a mermaid graph inside an issue description', () => {
|
||||
let mock;
|
||||
let spy;
|
||||
|
||||
beforeEach(() => {
|
||||
loadFixtures('static/issue_with_mermaid_graph.html');
|
||||
mock = new MockAdapter(axios);
|
||||
spy = jest.spyOn(axios, 'put');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('does not make a PUT request', () => {
|
||||
Issue.prototype.initIssueBtnEventListeners();
|
||||
|
||||
$('svg a.js-issuable-actions').trigger('click');
|
||||
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,9 +3,17 @@ import DuplicateDashboardForm from '~/monitoring/components/duplicate_dashboard_
|
|||
|
||||
import { dashboardGitResponse } from '../mock_data';
|
||||
|
||||
describe('DuplicateDashboardForm', () => {
|
||||
let wrapper;
|
||||
let wrapper;
|
||||
|
||||
const createMountedWrapper = (props = {}) => {
|
||||
// Use `mount` to render native input elements
|
||||
wrapper = mount(DuplicateDashboardForm, {
|
||||
propsData: { ...props },
|
||||
sync: false,
|
||||
});
|
||||
};
|
||||
|
||||
describe('DuplicateDashboardForm', () => {
|
||||
const defaultBranch = 'master';
|
||||
|
||||
const findByRef = ref => wrapper.find({ ref });
|
||||
|
@ -20,14 +28,7 @@ describe('DuplicateDashboardForm', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Use `mount` to render native input elements
|
||||
wrapper = mount(DuplicateDashboardForm, {
|
||||
propsData: {
|
||||
dashboard: dashboardGitResponse[0],
|
||||
defaultBranch,
|
||||
},
|
||||
sync: false,
|
||||
});
|
||||
createMountedWrapper({ dashboard: dashboardGitResponse[0], defaultBranch });
|
||||
});
|
||||
|
||||
it('renders correctly', () => {
|
||||
|
@ -146,3 +147,18 @@ describe('DuplicateDashboardForm', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DuplicateDashboardForm escapes elements', () => {
|
||||
const branchToEscape = "<img/src='x'onerror=alert(document.domain)>";
|
||||
|
||||
beforeEach(() => {
|
||||
createMountedWrapper({ dashboard: dashboardGitResponse[0], defaultBranch: branchToEscape });
|
||||
});
|
||||
|
||||
it('should escape branch name data', () => {
|
||||
const branchOptionHtml = wrapper.vm.branchOptions[0].html;
|
||||
const escapedBranch = '<img/src='x'onerror=alert(document.domain)>';
|
||||
|
||||
expect(branchOptionHtml).toEqual(expect.stringContaining(escapedBranch));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -190,11 +190,14 @@ describe Gitlab::BitbucketImport::Importer do
|
|||
|
||||
context 'when importing a pull request throws an exception' do
|
||||
before do
|
||||
allow(pull_request).to receive(:raw).and_return('hello world')
|
||||
allow(pull_request).to receive(:raw).and_return({ error: "broken" })
|
||||
allow(subject.client).to receive(:pull_request_comments).and_raise(Gitlab::HTTP::Error)
|
||||
end
|
||||
|
||||
it 'logs an error without the backtrace' do
|
||||
expect(Gitlab::ErrorTracking).to receive(:log_exception)
|
||||
.with(instance_of(Gitlab::HTTP::Error), hash_including(raw_response: '{"error":"broken"}'))
|
||||
|
||||
subject.execute
|
||||
|
||||
expect(subject.errors.count).to eq(1)
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::GlRepository::Identifier do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:personal_snippet) { create(:personal_snippet, author: project.owner) }
|
||||
let_it_be(:project_snippet) { create(:project_snippet, project: project, author: project.owner) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
shared_examples 'parsing gl_repository identifier' do
|
||||
subject { described_class.new(identifier) }
|
||||
|
||||
it 'returns correct information' do
|
||||
aggregate_failures do
|
||||
expect(subject.repo_type).to eq(expected_type)
|
||||
expect(subject.fetch_container!).to eq(expected_container)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'project repository' do
|
||||
it_behaves_like 'parsing gl_repository identifier' do
|
||||
let(:record_id) { project.id }
|
||||
let(:identifier) { "project-#{record_id}" }
|
||||
let(:expected_container) { project }
|
||||
let(:expected_type) { Gitlab::GlRepository::PROJECT }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'wiki' do
|
||||
it_behaves_like 'parsing gl_repository identifier' do
|
||||
let(:record_id) { project.id }
|
||||
let(:identifier) { "wiki-#{record_id}" }
|
||||
let(:expected_container) { project }
|
||||
let(:expected_type) { Gitlab::GlRepository::WIKI }
|
||||
end
|
||||
|
||||
context 'group wiki' do
|
||||
it_behaves_like 'parsing gl_repository identifier' do
|
||||
let(:record_id) { group.id }
|
||||
let(:identifier) { "group-#{record_id}-wiki" }
|
||||
let(:expected_container) { group }
|
||||
let(:expected_type) { Gitlab::GlRepository::WIKI }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'snippet' do
|
||||
context 'when PersonalSnippet' do
|
||||
it_behaves_like 'parsing gl_repository identifier' do
|
||||
let(:record_id) { personal_snippet.id }
|
||||
let(:identifier) { "snippet-#{record_id}" }
|
||||
let(:expected_container) { personal_snippet }
|
||||
let(:expected_type) { Gitlab::GlRepository::SNIPPET }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when ProjectSnippet' do
|
||||
it_behaves_like 'parsing gl_repository identifier' do
|
||||
let(:record_id) { project_snippet.id }
|
||||
let(:identifier) { "snippet-#{record_id}" }
|
||||
let(:expected_container) { project_snippet }
|
||||
let(:expected_type) { Gitlab::GlRepository::SNIPPET }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'design' do
|
||||
it_behaves_like 'parsing gl_repository identifier' do
|
||||
let(:record_id) { project.id }
|
||||
let(:identifier) { "design-#{project.id}" }
|
||||
let(:expected_container) { project }
|
||||
let(:expected_type) { Gitlab::GlRepository::DESIGN }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'incorrect format' do
|
||||
def expect_error_raised_for(identifier)
|
||||
expect { described_class.new(identifier) }.to raise_error(ArgumentError)
|
||||
end
|
||||
|
||||
it 'raises error for incorrect id' do
|
||||
expect_error_raised_for('wiki-noid')
|
||||
end
|
||||
|
||||
it 'raises error for incorrect type' do
|
||||
expect_error_raised_for('foo-2')
|
||||
end
|
||||
|
||||
it 'raises error for incorrect three-segments container' do
|
||||
expect_error_raised_for('snippet-2-wiki')
|
||||
end
|
||||
|
||||
it 'raises error for one segment' do
|
||||
expect_error_raised_for('snippet')
|
||||
end
|
||||
|
||||
it 'raises error for for than three segments' do
|
||||
expect_error_raised_for('project-1-wiki-bar')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -13,7 +13,7 @@ describe Gitlab::GlRepository::RepoType do
|
|||
|
||||
describe Gitlab::GlRepository::PROJECT do
|
||||
it_behaves_like 'a repo type' do
|
||||
let(:expected_id) { project.id.to_s }
|
||||
let(:expected_id) { project.id }
|
||||
let(:expected_identifier) { "project-#{expected_id}" }
|
||||
let(:expected_suffix) { '' }
|
||||
let(:expected_container) { project }
|
||||
|
@ -42,7 +42,7 @@ describe Gitlab::GlRepository::RepoType do
|
|||
|
||||
describe Gitlab::GlRepository::WIKI do
|
||||
it_behaves_like 'a repo type' do
|
||||
let(:expected_id) { project.id.to_s }
|
||||
let(:expected_id) { project.id }
|
||||
let(:expected_identifier) { "wiki-#{expected_id}" }
|
||||
let(:expected_suffix) { '.wiki' }
|
||||
let(:expected_container) { project }
|
||||
|
@ -67,12 +67,24 @@ describe Gitlab::GlRepository::RepoType do
|
|||
expect(described_class.valid?(design_path)).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'group wiki' do
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
it_behaves_like 'a repo type' do
|
||||
let(:expected_id) { group.id }
|
||||
let(:expected_identifier) { "group-#{expected_id}-wiki" }
|
||||
let(:expected_suffix) { '.wiki' }
|
||||
let(:expected_container) { group }
|
||||
let(:expected_repository) { expected_container.wiki.repository }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe Gitlab::GlRepository::SNIPPET do
|
||||
context 'when PersonalSnippet' do
|
||||
it_behaves_like 'a repo type' do
|
||||
let(:expected_id) { personal_snippet.id.to_s }
|
||||
let(:expected_id) { personal_snippet.id }
|
||||
let(:expected_identifier) { "snippet-#{expected_id}" }
|
||||
let(:expected_suffix) { '' }
|
||||
let(:expected_repository) { personal_snippet.repository }
|
||||
|
@ -101,7 +113,7 @@ describe Gitlab::GlRepository::RepoType do
|
|||
|
||||
context 'when ProjectSnippet' do
|
||||
it_behaves_like 'a repo type' do
|
||||
let(:expected_id) { project_snippet.id.to_s }
|
||||
let(:expected_id) { project_snippet.id }
|
||||
let(:expected_identifier) { "snippet-#{expected_id}" }
|
||||
let(:expected_suffix) { '' }
|
||||
let(:expected_repository) { project_snippet.repository }
|
||||
|
@ -131,7 +143,7 @@ describe Gitlab::GlRepository::RepoType do
|
|||
describe Gitlab::GlRepository::DESIGN do
|
||||
it_behaves_like 'a repo type' do
|
||||
let(:expected_identifier) { "design-#{project.id}" }
|
||||
let(:expected_id) { project.id.to_s }
|
||||
let(:expected_id) { project.id }
|
||||
let(:expected_suffix) { '.design' }
|
||||
let(:expected_repository) { project.design_repository }
|
||||
let(:expected_container) { project }
|
||||
|
|
|
@ -5,16 +5,21 @@ require 'spec_helper'
|
|||
describe ::Gitlab::GlRepository do
|
||||
describe '.parse' do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:snippet) { create(:personal_snippet) }
|
||||
|
||||
it 'parses a project gl_repository' do
|
||||
expect(described_class.parse("project-#{project.id}")).to eq([project, project, Gitlab::GlRepository::PROJECT])
|
||||
end
|
||||
|
||||
it 'parses a wiki gl_repository' do
|
||||
it 'parses a project wiki gl_repository' do
|
||||
expect(described_class.parse("wiki-#{project.id}")).to eq([project, project, Gitlab::GlRepository::WIKI])
|
||||
end
|
||||
|
||||
it 'parses a group wiki gl_repository' do
|
||||
expect(described_class.parse("group-#{group.id}-wiki")).to eq([group, nil, Gitlab::GlRepository::WIKI])
|
||||
end
|
||||
|
||||
it 'parses a snippet gl_repository' do
|
||||
expect(described_class.parse("snippet-#{snippet.id}")).to eq([snippet, nil, Gitlab::GlRepository::SNIPPET])
|
||||
end
|
||||
|
|
|
@ -65,5 +65,23 @@ describe Gitlab::StaticSiteEditor::Config do
|
|||
|
||||
it { is_expected.to include(is_supported_content: 'false') }
|
||||
end
|
||||
|
||||
context 'when return_url is not a valid URL' do
|
||||
let(:return_url) { 'example.com' }
|
||||
|
||||
it { is_expected.to include(return_url: nil) }
|
||||
end
|
||||
|
||||
context 'when return_url has a javascript scheme' do
|
||||
let(:return_url) { 'javascript:alert(document.domain)' }
|
||||
|
||||
it { is_expected.to include(return_url: nil) }
|
||||
end
|
||||
|
||||
context 'when return_url is missing' do
|
||||
let(:return_url) { nil }
|
||||
|
||||
it { is_expected.to include(return_url: nil) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -60,6 +60,30 @@ describe Gitlab::UrlSanitizer do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.valid_web?' do
|
||||
where(:value, :url) do
|
||||
false | nil
|
||||
false | ''
|
||||
false | '123://invalid:url'
|
||||
false | 'valid@project:url.git'
|
||||
false | 'valid:pass@project:url.git'
|
||||
false | %w(test array)
|
||||
false | 'ssh://example.com'
|
||||
false | 'ssh://:@example.com'
|
||||
false | 'ssh://foo@example.com'
|
||||
false | 'ssh://foo:bar@example.com'
|
||||
false | 'ssh://foo:bar@example.com/group/group/project.git'
|
||||
false | 'git://example.com/group/group/project.git'
|
||||
false | 'git://foo:bar@example.com/group/group/project.git'
|
||||
true | 'http://foo:bar@example.com/group/group/project.git'
|
||||
true | 'https://foo:bar@example.com/group/group/project.git'
|
||||
end
|
||||
|
||||
with_them do
|
||||
it { expect(described_class.valid_web?(url)).to eq(value) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#sanitized_url' do
|
||||
context 'credentials in hash' do
|
||||
where(username: ['foo', '', nil], password: ['bar', '', nil])
|
||||
|
|
|
@ -110,6 +110,11 @@ describe Group do
|
|||
let(:group_notification_email) { 'user+group@example.com' }
|
||||
let(:subgroup_notification_email) { 'user+subgroup@example.com' }
|
||||
|
||||
before do
|
||||
create(:email, :confirmed, user: user, email: group_notification_email)
|
||||
create(:email, :confirmed, user: user, email: subgroup_notification_email)
|
||||
end
|
||||
|
||||
subject { subgroup.notification_email_for(user) }
|
||||
|
||||
context 'when both group notification emails are set' do
|
||||
|
|
|
@ -48,6 +48,33 @@ RSpec.describe NotificationSetting do
|
|||
expect(notification_setting.reopen_merge_request).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'notification_email' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
subject { described_class.new(source_id: 1, source_type: 'Project', user_id: user.id) }
|
||||
|
||||
it 'allows to change email to verified one' do
|
||||
email = create(:email, :confirmed, user: user)
|
||||
|
||||
subject.update(notification_email: email.email)
|
||||
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
|
||||
it 'does not allow to change email to not verified one' do
|
||||
email = create(:email, user: user)
|
||||
|
||||
subject.update(notification_email: email.email)
|
||||
|
||||
expect(subject).to be_invalid
|
||||
end
|
||||
|
||||
it 'allows to change email to empty one' do
|
||||
subject.update(notification_email: '')
|
||||
|
||||
expect(subject).to be_valid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#for_projects' do
|
||||
|
|
|
@ -178,6 +178,15 @@ describe PersonalAccessToken do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.without_impersonation' do
|
||||
let_it_be(:impersonation_token) { create(:personal_access_token, :impersonation) }
|
||||
let_it_be(:personal_access_token) { create(:personal_access_token) }
|
||||
|
||||
it 'returns only non-impersonation tokens' do
|
||||
expect(described_class.without_impersonation).to contain_exactly(personal_access_token)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.simple_sorts' do
|
||||
|
|
|
@ -311,7 +311,7 @@ describe User do
|
|||
end
|
||||
|
||||
it_behaves_like 'an object with RFC3696 compliant email-formated attributes', :public_email, :notification_email do
|
||||
subject { build(:user).tap { |user| user.emails << build(:email, email: email_value) } }
|
||||
subject { create(:user).tap { |user| user.emails << build(:email, email: email_value, confirmed_at: Time.current) } }
|
||||
end
|
||||
|
||||
describe '#commit_email' do
|
||||
|
@ -568,6 +568,32 @@ describe User do
|
|||
user = build(:user, email: "temp-email-for-oauth@example.com")
|
||||
expect(user).to be_valid
|
||||
end
|
||||
|
||||
it 'does not accept not verified emails' do
|
||||
email = create(:email)
|
||||
user = email.user
|
||||
user.update(notification_email: email.email)
|
||||
|
||||
expect(user).to be_invalid
|
||||
end
|
||||
end
|
||||
|
||||
context 'owns_public_email' do
|
||||
it 'accepts verified emails' do
|
||||
email = create(:email, :confirmed, email: 'test@test.com')
|
||||
user = email.user
|
||||
user.update(public_email: email.email)
|
||||
|
||||
expect(user).to be_valid
|
||||
end
|
||||
|
||||
it 'does not accept not verified emails' do
|
||||
email = create(:email)
|
||||
user = email.user
|
||||
user.update(public_email: email.email)
|
||||
|
||||
expect(user).to be_invalid
|
||||
end
|
||||
end
|
||||
|
||||
context 'set_commit_email' do
|
||||
|
@ -791,6 +817,7 @@ describe User do
|
|||
|
||||
let_it_be(:expired_token) { create(:personal_access_token, user: user1, expires_at: 2.days.ago) }
|
||||
let_it_be(:revoked_token) { create(:personal_access_token, user: user1, revoked: true) }
|
||||
let_it_be(:impersonation_token) { create(:personal_access_token, :impersonation, user: user1, expires_at: 2.days.from_now) }
|
||||
let_it_be(:valid_token_and_notified) { create(:personal_access_token, user: user2, expires_at: 2.days.from_now, expire_notification_delivered: true) }
|
||||
let_it_be(:valid_token1) { create(:personal_access_token, user: user2, expires_at: 2.days.from_now) }
|
||||
let_it_be(:valid_token2) { create(:personal_access_token, user: user2, expires_at: 2.days.from_now) }
|
||||
|
@ -917,6 +944,108 @@ describe User do
|
|||
expect(@user.emails.count).to eq 1
|
||||
expect(@user.emails.first.confirmed_at).not_to eq nil
|
||||
end
|
||||
|
||||
context 'when the first email was unconfirmed and the second email gets confirmed' do
|
||||
let(:user) { create(:user, :unconfirmed, email: 'should-be-unconfirmed@test.com') }
|
||||
|
||||
before do
|
||||
user.update!(email: 'should-be-confirmed@test.com')
|
||||
user.confirm
|
||||
end
|
||||
|
||||
it 'updates user.email' do
|
||||
expect(user.email).to eq('should-be-confirmed@test.com')
|
||||
end
|
||||
|
||||
it 'confirms user.email' do
|
||||
expect(user).to be_confirmed
|
||||
end
|
||||
|
||||
it 'keeps the unconfirmed email unconfirmed' do
|
||||
email = user.emails.first
|
||||
|
||||
expect(email.email).to eq('should-be-unconfirmed@test.com')
|
||||
expect(email).not_to be_confirmed
|
||||
end
|
||||
|
||||
it 'has only one email association' do
|
||||
expect(user.emails.size).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when an existing email record is set as primary' do
|
||||
let(:user) { create(:user, email: 'confirmed@test.com') }
|
||||
|
||||
context 'when it is unconfirmed' do
|
||||
let(:originally_unconfirmed_email) { 'should-stay-unconfirmed@test.com' }
|
||||
|
||||
before do
|
||||
user.emails << create(:email, email: originally_unconfirmed_email, confirmed_at: nil)
|
||||
|
||||
user.update!(email: originally_unconfirmed_email)
|
||||
end
|
||||
|
||||
it 'keeps the user confirmed' do
|
||||
expect(user).to be_confirmed
|
||||
end
|
||||
|
||||
it 'keeps the original email' do
|
||||
expect(user.email).to eq('confirmed@test.com')
|
||||
end
|
||||
|
||||
context 'when the email gets confirmed' do
|
||||
before do
|
||||
user.confirm
|
||||
end
|
||||
|
||||
it 'keeps the user confirmed' do
|
||||
expect(user).to be_confirmed
|
||||
end
|
||||
|
||||
it 'updates the email' do
|
||||
expect(user.email).to eq(originally_unconfirmed_email)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is confirmed' do
|
||||
let!(:old_confirmed_email) { user.email }
|
||||
let(:confirmed_email) { 'already-confirmed@test.com' }
|
||||
|
||||
before do
|
||||
user.emails << create(:email, :confirmed, email: confirmed_email)
|
||||
|
||||
user.update!(email: confirmed_email)
|
||||
end
|
||||
|
||||
it 'keeps the user confirmed' do
|
||||
expect(user).to be_confirmed
|
||||
end
|
||||
|
||||
it 'updates the email' do
|
||||
expect(user.email).to eq(confirmed_email)
|
||||
end
|
||||
|
||||
it 'moves the old email' do
|
||||
email = user.reload.emails.first
|
||||
|
||||
expect(email.email).to eq(old_confirmed_email)
|
||||
expect(email).to be_confirmed
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when unconfirmed user deletes a confirmed additional email' do
|
||||
let(:user) { create(:user, :unconfirmed) }
|
||||
|
||||
before do
|
||||
user.emails << create(:email, :confirmed)
|
||||
end
|
||||
|
||||
it 'does not affect the confirmed status' do
|
||||
expect { user.emails.confirmed.destroy_all }.not_to change { user.confirmed? } # rubocop: disable Cop/DestroyAll
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update_notification_email' do
|
||||
|
@ -2070,6 +2199,31 @@ describe User do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#public_verified_emails' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it 'returns only confirmed public emails' do
|
||||
email_confirmed = create :email, user: user, confirmed_at: Time.current
|
||||
create :email, user: user
|
||||
|
||||
expect(user.public_verified_emails).to contain_exactly(
|
||||
user.email,
|
||||
email_confirmed.email
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns confirmed public emails plus main user email when user is not confirmed' do
|
||||
user = create(:user, confirmed_at: nil)
|
||||
email_confirmed = create :email, user: user, confirmed_at: Time.current
|
||||
create :email, user: user
|
||||
|
||||
expect(user.public_verified_emails).to contain_exactly(
|
||||
user.email,
|
||||
email_confirmed.email
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#verified_email?' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
|
@ -4200,9 +4354,10 @@ describe User do
|
|||
context 'when an ancestor has a level other than Global' do
|
||||
let(:ancestor) { create(:group) }
|
||||
let(:group) { create(:group, parent: ancestor) }
|
||||
let(:email) { create(:email, :confirmed, email: 'ancestor@example.com', user: user) }
|
||||
|
||||
before do
|
||||
create(:notification_setting, user: user, source: ancestor, level: 'participating', notification_email: 'ancestor@example.com')
|
||||
create(:notification_setting, user: user, source: ancestor, level: 'participating', notification_email: email.email)
|
||||
end
|
||||
|
||||
it 'has the same level set' do
|
||||
|
@ -4227,10 +4382,12 @@ describe User do
|
|||
let(:grand_ancestor) { create(:group) }
|
||||
let(:ancestor) { create(:group, parent: grand_ancestor) }
|
||||
let(:group) { create(:group, parent: ancestor) }
|
||||
let(:ancestor_email) { create(:email, :confirmed, email: 'ancestor@example.com', user: user) }
|
||||
let(:grand_email) { create(:email, :confirmed, email: 'grand@example.com', user: user) }
|
||||
|
||||
before do
|
||||
create(:notification_setting, user: user, source: grand_ancestor, level: 'participating', notification_email: 'grand@example.com')
|
||||
create(:notification_setting, user: user, source: ancestor, level: 'global', notification_email: 'ancestor@example.com')
|
||||
create(:notification_setting, user: user, source: grand_ancestor, level: 'participating', notification_email: grand_email.email)
|
||||
create(:notification_setting, user: user, source: ancestor, level: 'global', notification_email: ancestor_email.email)
|
||||
end
|
||||
|
||||
it 'has the same email set' do
|
||||
|
@ -4268,7 +4425,7 @@ describe User do
|
|||
context 'when group has notification email set' do
|
||||
it 'returns group notification email' do
|
||||
group_notification_email = 'user+group@example.com'
|
||||
|
||||
create(:email, :confirmed, user: user, email: group_notification_email)
|
||||
create(:notification_setting, user: user, source: group, notification_email: group_notification_email)
|
||||
|
||||
is_expected.to eq(group_notification_email)
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'Create an alert issue from an alert' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:alert) { create(:alert_management_alert, project: project) }
|
||||
|
||||
let(:mutation) do
|
||||
variables = {
|
||||
project_path: project.full_path,
|
||||
iid: alert.iid.to_s
|
||||
}
|
||||
graphql_mutation(:create_alert_issue, variables,
|
||||
<<~QL
|
||||
clientMutationId
|
||||
errors
|
||||
alert {
|
||||
iid
|
||||
issueIid
|
||||
}
|
||||
issue {
|
||||
iid
|
||||
title
|
||||
}
|
||||
QL
|
||||
)
|
||||
end
|
||||
|
||||
let(:mutation_response) { graphql_mutation_response(:create_alert_issue) }
|
||||
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
context 'when there is no issue associated with the alert' do
|
||||
it 'creates an alert issue' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
new_issue = Issue.last!
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response.slice('alert', 'issue')).to eq(
|
||||
'alert' => {
|
||||
'iid' => alert.iid.to_s,
|
||||
'issueIid' => new_issue.iid.to_s
|
||||
},
|
||||
'issue' => {
|
||||
'iid' => new_issue.iid.to_s,
|
||||
'title' => new_issue.title
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is an issue already associated with the alert' do
|
||||
before do
|
||||
AlertManagement::CreateAlertIssueService.new(alert, user).execute
|
||||
end
|
||||
|
||||
it 'responds with an error' do
|
||||
post_graphql_mutation(mutation, current_user: user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:success)
|
||||
expect(mutation_response.slice('errors', 'issue')).to eq(
|
||||
'errors' => ['An issue already exists'],
|
||||
'issue' => nil
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11,7 +11,7 @@ describe API::GroupImport do
|
|||
let(:file) { File.join('spec', 'fixtures', 'group_export.tar.gz') }
|
||||
let(:export_path) { "#{Dir.tmpdir}/group_export_spec" }
|
||||
let(:workhorse_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
|
||||
let(:workhorse_header) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
|
||||
let(:workhorse_headers) { { 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token } }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(Gitlab::ImportExport) do |import_export|
|
||||
|
@ -35,7 +35,7 @@ describe API::GroupImport do
|
|||
}
|
||||
end
|
||||
|
||||
subject { post api('/groups/import', user), params: params, headers: workhorse_header }
|
||||
subject { upload_archive(file_upload, workhorse_headers, params) }
|
||||
|
||||
shared_examples 'when all params are correct' do
|
||||
context 'when user is authorized to create new group' do
|
||||
|
@ -151,7 +151,7 @@ describe API::GroupImport do
|
|||
params[:file] = file_upload
|
||||
|
||||
expect do
|
||||
post api('/groups/import', user), params: params, headers: workhorse_header
|
||||
upload_archive(file_upload, workhorse_headers, params)
|
||||
end.not_to change { Group.count }.from(1)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
|
@ -171,7 +171,7 @@ describe API::GroupImport do
|
|||
|
||||
context 'without a file from workhorse' do
|
||||
it 'rejects the request' do
|
||||
subject
|
||||
upload_archive(nil, workhorse_headers, params)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
@ -179,7 +179,7 @@ describe API::GroupImport do
|
|||
|
||||
context 'without a workhorse header' do
|
||||
it 'rejects request without a workhorse header' do
|
||||
post api('/groups/import', user), params: params
|
||||
upload_archive(file_upload, {}, params)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
|
@ -189,9 +189,7 @@ describe API::GroupImport do
|
|||
let(:params) do
|
||||
{
|
||||
path: 'test-import-group',
|
||||
name: 'test-import-group',
|
||||
'file.path' => file_upload.path,
|
||||
'file.name' => file_upload.original_filename
|
||||
name: 'test-import-group'
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -229,9 +227,7 @@ describe API::GroupImport do
|
|||
{
|
||||
path: 'test-import-group',
|
||||
name: 'test-import-group',
|
||||
file: fog_file,
|
||||
'file.remote_id' => file_name,
|
||||
'file.size' => fog_file.size
|
||||
file: fog_file
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -245,10 +241,21 @@ describe API::GroupImport do
|
|||
include_examples 'when some params are missing'
|
||||
end
|
||||
end
|
||||
|
||||
def upload_archive(file, headers = {}, params = {})
|
||||
workhorse_finalize(
|
||||
api('/groups/import', user),
|
||||
method: :post,
|
||||
file_key: :file,
|
||||
params: params.merge(file: file),
|
||||
headers: headers,
|
||||
send_rewritten_field: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /groups/import/authorize' do
|
||||
subject { post api('/groups/import/authorize', user), headers: workhorse_header }
|
||||
subject { post api('/groups/import/authorize', user), headers: workhorse_headers }
|
||||
|
||||
it 'authorizes importing group with workhorse header' do
|
||||
subject
|
||||
|
@ -258,7 +265,7 @@ describe API::GroupImport do
|
|||
end
|
||||
|
||||
it 'rejects requests that bypassed gitlab-workhorse' do
|
||||
workhorse_header.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
|
||||
workhorse_headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
|
||||
|
||||
subject
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ describe API::NotificationSettings do
|
|||
end
|
||||
|
||||
describe "PUT /notification_settings" do
|
||||
let(:email) { create(:email, user: user) }
|
||||
let(:email) { create(:email, :confirmed, user: user) }
|
||||
|
||||
it "updates global notification settings for the current user" do
|
||||
put api("/notification_settings", user), params: { level: 'watch', notification_email: email.email }
|
||||
|
|
|
@ -1891,6 +1891,17 @@ describe API::Projects do
|
|||
expect(project_fork_target).to be_forked
|
||||
end
|
||||
|
||||
it 'fails without permission from forked_from project' do
|
||||
project_fork_source.project_feature.update_attribute(:forking_access_level, ProjectFeature::PRIVATE)
|
||||
|
||||
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
expect(project_fork_target.forked_from_project).to be_nil
|
||||
expect(project_fork_target.fork_network_member).not_to be_present
|
||||
expect(project_fork_target).not_to be_forked
|
||||
end
|
||||
|
||||
it 'denies project to be forked from a private project' do
|
||||
post api("/projects/#{project_fork_target.id}/fork/#{private_project_fork_source.id}", user)
|
||||
|
||||
|
|
|
@ -177,6 +177,12 @@ describe API::Repositories do
|
|||
expect(headers['Content-Disposition']).to eq 'inline'
|
||||
end
|
||||
|
||||
it_behaves_like 'uncached response' do
|
||||
before do
|
||||
get api(route, current_user)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sha does not exist' do
|
||||
it_behaves_like '404 response' do
|
||||
let(:request) { get api(route.sub(sample_blob.oid, 'abcd9876'), current_user) }
|
||||
|
|
|
@ -9,15 +9,11 @@ describe 'OpenID Connect requests' do
|
|||
name: 'Alice',
|
||||
username: 'alice',
|
||||
email: 'private@example.com',
|
||||
emails: [public_email],
|
||||
public_email: public_email.email,
|
||||
website_url: 'https://example.com',
|
||||
avatar: fixture_file_upload('spec/fixtures/dk.png')
|
||||
)
|
||||
end
|
||||
|
||||
let(:public_email) { build :email, email: 'public@example.com' }
|
||||
|
||||
let(:access_grant) { create :oauth_access_grant, application: application, resource_owner_id: user.id }
|
||||
let(:access_token) { create :oauth_access_token, application: application, resource_owner_id: user.id }
|
||||
|
||||
|
@ -37,7 +33,7 @@ describe 'OpenID Connect requests' do
|
|||
'name' => 'Alice',
|
||||
'nickname' => 'alice',
|
||||
'email' => 'public@example.com',
|
||||
'email_verified' => false,
|
||||
'email_verified' => true,
|
||||
'website' => 'https://example.com',
|
||||
'profile' => 'http://localhost/alice',
|
||||
'picture' => "http://localhost/uploads/-/system/user/avatar/#{user.id}/dk.png",
|
||||
|
@ -62,6 +58,11 @@ describe 'OpenID Connect requests' do
|
|||
get '/oauth/userinfo', params: {}, headers: { 'Authorization' => "Bearer #{access_token.token}" }
|
||||
end
|
||||
|
||||
before do
|
||||
email = create(:email, :confirmed, email: 'public@example.com', user: user)
|
||||
user.update!(public_email: email.email)
|
||||
end
|
||||
|
||||
context 'Application without OpenID scope' do
|
||||
let(:application) { create :oauth_application, scopes: 'api' }
|
||||
|
||||
|
@ -123,7 +124,7 @@ describe 'OpenID Connect requests' do
|
|||
end
|
||||
|
||||
it 'has false in email_verified claim' do
|
||||
expect(json_response['email_verified']).to eq(false)
|
||||
expect(json_response['email_verified']).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@ require 'spec_helper'
|
|||
describe 'view user notifications' do
|
||||
let(:user) do
|
||||
create(:user) do |user|
|
||||
user.emails.create(email: 'original@example.com')
|
||||
user.emails.create(email: 'new@example.com')
|
||||
user.emails.create(email: 'original@example.com', confirmed_at: Time.current)
|
||||
user.emails.create(email: 'new@example.com', confirmed_at: Time.current)
|
||||
user.notification_email = 'original@example.com'
|
||||
user.save!
|
||||
end
|
||||
|
|
|
@ -47,6 +47,39 @@ describe Clusters::UpdateService do
|
|||
expect(cluster.platform.namespace).to eq('custom-namespace')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when service token is empty' do
|
||||
let(:params) do
|
||||
{
|
||||
platform_kubernetes_attributes: {
|
||||
token: ''
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'does not update the token' do
|
||||
current_token = cluster.platform.token
|
||||
is_expected.to eq(true)
|
||||
cluster.platform.reload
|
||||
|
||||
expect(cluster.platform.token).to eq(current_token)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when service token is not empty' do
|
||||
let(:params) do
|
||||
{
|
||||
platform_kubernetes_attributes: {
|
||||
token: 'new secret token'
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it 'updates the token' do
|
||||
is_expected.to eq(true)
|
||||
expect(cluster.platform.token).to eq('new secret token')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invalid params' do
|
||||
|
|
|
@ -2457,6 +2457,8 @@ describe NotificationService, :mailer do
|
|||
group = create(:group)
|
||||
|
||||
project.update(group: group)
|
||||
|
||||
create(:email, :confirmed, user: u_custom_notification_enabled, email: group_notification_email)
|
||||
create(:notification_setting, user: u_custom_notification_enabled, source: group, notification_email: group_notification_email)
|
||||
end
|
||||
|
||||
|
@ -2491,6 +2493,7 @@ describe NotificationService, :mailer do
|
|||
group = create(:group)
|
||||
|
||||
project.update(group: group)
|
||||
create(:email, :confirmed, user: u_member, email: group_notification_email)
|
||||
create(:notification_setting, user: u_member, source: group, notification_email: group_notification_email)
|
||||
end
|
||||
|
||||
|
@ -2584,6 +2587,7 @@ describe NotificationService, :mailer do
|
|||
group = create(:group)
|
||||
|
||||
project.update(group: group)
|
||||
create(:email, :confirmed, user: u_member, email: group_notification_email)
|
||||
create(:notification_setting, user: u_member, source: group, notification_email: group_notification_email)
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'Secure OAuth Authorizations' do
|
||||
context 'when user is confirmed' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it 'asks the user to authorize the application' do
|
||||
expect(page).to have_text "Authorize #{application.name} to use your account?"
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is unconfirmed' do
|
||||
let(:user) { create(:user, confirmed_at: nil) }
|
||||
|
||||
it 'displays an error' do
|
||||
expect(page).to have_text I18n.t('doorkeeper.errors.messages.unconfirmed_email')
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,26 +7,6 @@ RSpec.shared_examples 'a repo type' do
|
|||
it { is_expected.to eq(expected_identifier) }
|
||||
end
|
||||
|
||||
describe '#fetch_id' do
|
||||
it 'finds an id match in the identifier' do
|
||||
expect(described_class.fetch_id(expected_identifier)).to eq(expected_id)
|
||||
end
|
||||
|
||||
it 'does not break on other identifiers' do
|
||||
expect(described_class.fetch_id('wiki-noid')).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#fetch_container!' do
|
||||
it 'returns the container' do
|
||||
expect(described_class.fetch_container!(expected_identifier)).to eq expected_container
|
||||
end
|
||||
|
||||
it 'raises an exception if the identifier is invalid' do
|
||||
expect { described_class.fetch_container!('project-noid') }.to raise_error ArgumentError
|
||||
end
|
||||
end
|
||||
|
||||
describe '#path_suffix' do
|
||||
subject { described_class.path_suffix }
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ RSpec.shared_examples 'an email sent to a user' do
|
|||
it 'is sent to user\'s group notification email' do
|
||||
group_notification_email = 'user+group@example.com'
|
||||
|
||||
create(:email, :confirmed, user: recipient, email: group_notification_email)
|
||||
create(:notification_setting, user: recipient, source: group, notification_email: group_notification_email)
|
||||
|
||||
expect(subject).to deliver_to(group_notification_email)
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
#
|
||||
# Pairs with lib/gitlab/no_cache_headers.rb
|
||||
#
|
||||
|
||||
RSpec.shared_examples 'uncached response' do
|
||||
it 'defines an uncached header response' do
|
||||
expect(response.headers["Cache-Control"]).to include("no-store", "no-cache")
|
||||
expect(response.headers["Pragma"]).to eq("no-cache")
|
||||
expect(response.headers["Expires"]).to eq("Fri, 01 Jan 1990 00:00:00 GMT")
|
||||
end
|
||||
end
|
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe 'admin/application_settings/_eks' do
|
||||
let_it_be(:admin) { create(:admin) }
|
||||
let(:page) { Capybara::Node::Simple.new(rendered) }
|
||||
|
||||
before do
|
||||
assign(:application_setting, application_setting)
|
||||
allow(view).to receive(:current_user) { admin }
|
||||
allow(view).to receive(:expanded) { true }
|
||||
end
|
||||
|
||||
shared_examples 'EKS secret access key input' do
|
||||
it 'renders an empty password field' do
|
||||
render
|
||||
expect(rendered).to have_field('Secret access key', type: 'password')
|
||||
expect(page.find_field('Secret access key').value).to be_blank
|
||||
end
|
||||
end
|
||||
|
||||
context 'when eks_secret_access_key is not set' do
|
||||
let(:application_setting) { build(:application_setting) }
|
||||
|
||||
include_examples 'EKS secret access key input'
|
||||
end
|
||||
|
||||
context 'when eks_secret_access_key is set' do
|
||||
let(:application_setting) { build(:application_setting, eks_secret_access_key: 'eks_secret_access_key') }
|
||||
|
||||
include_examples 'EKS secret access key input'
|
||||
end
|
||||
end
|
|
@ -7,7 +7,7 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker do
|
|||
|
||||
describe '#perform' do
|
||||
context 'when a token needs to be notified' do
|
||||
let!(:pat) { create(:personal_access_token, expires_at: 5.days.from_now) }
|
||||
let_it_be(:pat) { create(:personal_access_token, expires_at: 5.days.from_now) }
|
||||
|
||||
it 'uses notification service to send the email' do
|
||||
expect_next_instance_of(NotificationService) do |notification_service|
|
||||
|
@ -23,7 +23,7 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker do
|
|||
end
|
||||
|
||||
context 'when no tokens need to be notified' do
|
||||
let!(:pat) { create(:personal_access_token, expires_at: 5.days.from_now, expire_notification_delivered: true) }
|
||||
let_it_be(:pat) { create(:personal_access_token, expires_at: 5.days.from_now, expire_notification_delivered: true) }
|
||||
|
||||
it "doesn't use notification service to send the email" do
|
||||
expect_next_instance_of(NotificationService) do |notification_service|
|
||||
|
@ -33,7 +33,23 @@ RSpec.describe PersonalAccessTokens::ExpiringWorker, type: :worker do
|
|||
worker.perform
|
||||
end
|
||||
|
||||
it "doesn't change the notificationd delivered of the token" do
|
||||
it "doesn't change the notification delivered of the token" do
|
||||
expect { worker.perform }.not_to change { pat.reload.expire_notification_delivered }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a token is an impersonation token' do
|
||||
let_it_be(:pat) { create(:personal_access_token, :impersonation, expires_at: 5.days.from_now) }
|
||||
|
||||
it "doesn't use notification service to send the email" do
|
||||
expect_next_instance_of(NotificationService) do |notification_service|
|
||||
expect(notification_service).not_to receive(:access_token_about_to_expire).with(pat.user)
|
||||
end
|
||||
|
||||
worker.perform
|
||||
end
|
||||
|
||||
it "doesn't change the notification delivered of the token" do
|
||||
expect { worker.perform }.not_to change { pat.reload.expire_notification_delivered }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -355,7 +355,7 @@ describe PostReceive do
|
|||
|
||||
context "webhook" do
|
||||
it "fetches the correct project" do
|
||||
expect(Project).to receive(:find_by).with(id: project.id.to_s)
|
||||
expect(Project).to receive(:find_by).with(id: project.id)
|
||||
|
||||
perform
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue