Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-05-27 21:08:05 +00:00
parent 88ccc935c0
commit 47579e24f3
88 changed files with 1474 additions and 430 deletions

View File

@ -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.

View File

@ -1 +1 @@
8.32.0
8.32.1

View File

@ -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');

View File

@ -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 }) => {

View File

@ -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,
),

View File

@ -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,

View File

@ -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);
}

View File

@ -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>

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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!

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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'),

View File

@ -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')

View File

@ -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)

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Prevent emails to user on expiry of impersonation token
merge_request: 32140
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Update U2F docs for Firefox 67+
merge_request: 32289
author: Takuya Noguchi
type: other

View File

@ -0,0 +1,5 @@
---
title: Fix alignment of button text on the Edit Release page
merge_request: 33104
author:
type: fixed

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
```

View File

@ -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 |

View File

@ -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 -->

View File

@ -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

View File

@ -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

View File

@ -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`.

View File

@ -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.

View File

@ -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)** |

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 ""

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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' },

View File

@ -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>

View File

@ -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();
});
});
});
});

View File

@ -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 = '&lt;img/src=&#39;x&#39;onerror=alert(document.domain)&gt';
expect(branchOptionHtml).toEqual(expect.stringContaining(escapedBranch));
});
});

View File

@ -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)

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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)

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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