Commit de24e9b8 authored by GitLab Release Tools Bot's avatar GitLab Release Tools Bot

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce

parents 3c727ede e861af40
......@@ -4,7 +4,12 @@ include:
- local: /lib/gitlab/ci/templates/Code-Quality.gitlab-ci.yml
.dedicated-runner: &dedicated-runner
retry: 1
retry:
max: 1 # This is confusing but this means "2 runs at max".
when:
- unknown_failure
- api_failure
- runner_system_failure
tags:
- gitlab-org
......@@ -557,7 +562,7 @@ rspec-mysql:
parallel: 50
.rspec-quarantine: &rspec-quarantine
retry: 0
<<: *only-schedules-master
script:
- export CACHE_CLASSES=true
- scripts/gitaly-test-spawn
......@@ -588,7 +593,7 @@ static-analysis:
- tmp/rubocop_cache
# Documentation checks:
# - Check validity of relative links
# - Check validity of relative links, and anchors
# - Make sure cURL examples in API docs use the full switches
docs lint:
<<: *dedicated-runner
......@@ -607,6 +612,8 @@ docs lint:
- bundle exec nanoc
# Check the internal links
- bundle exec nanoc check internal_links
# Check the internal anchor links
- bundle exec nanoc check internal_anchors
downtime_check:
<<: *rake-exec
......@@ -1168,4 +1175,3 @@ schedule:review-performance:
<<: *review-schedules-only
script:
- wait_for_job_to_be_done "schedule:review-deploy"
......@@ -50,7 +50,7 @@ export default {
},
modalText() {
const linkStart = `<a class="commit-sha" href="${_.escape(this.commitUrl)}">`;
const linkStart = `<a class="commit-sha mr-0" href="${_.escape(this.commitUrl)}">`;
const commitId = _.escape(this.commitShortSha);
const linkEnd = '</a>';
const name = _.escape(this.name);
......
......@@ -504,22 +504,28 @@ export default {
class="table-section section-10 deployment-column d-none d-sm-none d-md-block"
role="gridcell"
>
<span v-if="shouldRenderDeploymentID"> {{ deploymentInternalId }} </span>
<span v-if="shouldRenderDeploymentID" class="text-break-word">
{{ deploymentInternalId }}
</span>
<span v-if="!model.isFolder && deploymentHasUser">
<span v-if="!model.isFolder && deploymentHasUser" class="text-break-word">
by
<user-avatar-link
:link-href="deploymentUser.web_url"
:img-src="deploymentUser.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="deploymentUser.username"
class="js-deploy-user-container"
class="js-deploy-user-container float-none"
/>
</span>
</div>
<div class="table-section section-15 d-none d-sm-none d-md-block" role="gridcell">
<a v-if="shouldRenderBuildName" :href="buildPath" class="build-link flex-truncate-parent">
<a
v-if="shouldRenderBuildName"
:href="buildPath"
class="build-link cgray flex-truncate-parent"
>
<span class="flex-truncate-child">{{ buildName }}</span>
</a>
</div>
......
......@@ -72,10 +72,9 @@ export default {
<gl-button
v-gl-tooltip
v-gl-modal.confirm-rollback-modal
variant="secondary"
:disabled="isLoading"
:title="title"
class="d-none d-md-block"
class="d-none d-md-block text-secondary"
@click="onClick"
>
<icon v-if="isLastDeployment" name="repeat" /> <icon v-else name="redo" />
......
......@@ -39,7 +39,7 @@ export default {
:aria-label="title"
:href="terminalPath"
:class="{ disabled: disabled }"
class="btn terminal-button d-none d-sm-none d-md-block"
class="btn terminal-button d-none d-sm-none d-md-block text-secondary"
>
<icon name="terminal" />
</a>
......
......@@ -81,9 +81,6 @@ export default {
const formData = {
update: {
state_event: this.form.find('input[name="update[state_event]"]').val(),
// For Merge Requests
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
// For Issues
assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()],
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
......
......@@ -18,5 +18,3 @@ export const timeWindows = {
threeDays: __('3 days'),
oneWeek: __('1 week'),
};
export const msPerMinute = 60000;
import { timeWindows, msPerMinute } from './constants';
import { timeWindows } from './constants';
/**
* method that converts a predetermined time window to minutes
......@@ -6,27 +6,26 @@ import { timeWindows, msPerMinute } from './constants';
* @param {String} timeWindow - The time window to convert to minutes
* @returns {number} The time window in minutes
*/
const getTimeDifferenceMinutes = timeWindow => {
const getTimeDifferenceSeconds = timeWindow => {
switch (timeWindow) {
case timeWindows.thirtyMinutes:
return 30;
return 60 * 30;
case timeWindows.threeHours:
return 60 * 3;
return 60 * 60 * 3;
case timeWindows.oneDay:
return 60 * 24 * 1;
return 60 * 60 * 24 * 1;
case timeWindows.threeDays:
return 60 * 24 * 3;
return 60 * 60 * 24 * 3;
case timeWindows.oneWeek:
return 60 * 24 * 7 * 1;
return 60 * 60 * 24 * 7 * 1;
default:
return 60 * 8;
return 60 * 60 * 8;
}
};
export const getTimeDiff = selectedTimeWindow => {
const end = Date.now();
const timeDifferenceMinutes = getTimeDifferenceMinutes(selectedTimeWindow);
const start = new Date(end - timeDifferenceMinutes * msPerMinute).getTime();
const end = Date.now() / 1000; // convert milliseconds to seconds
const start = end - getTimeDifferenceSeconds(selectedTimeWindow);
return { start, end };
};
......
......@@ -115,8 +115,11 @@ export default {
author() {
return this.getUserData;
},
canUpdateIssue() {
return this.getNoteableData.current_user.can_update;
canToggleIssueState() {
return (
this.getNoteableData.current_user.can_update &&
this.getNoteableData.state !== constants.MERGED
);
},
endpoint() {
return this.getNoteableData.create_note_path;
......@@ -415,7 +418,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
</div>
<loading-button
v-if="canUpdateIssue"
v-if="canToggleIssueState"
:loading="isToggleStateButtonLoading"
:container-class="[
actionButtonClassNames,
......
......@@ -4,6 +4,7 @@ import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore';
import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import draftMixin from 'ee_else_ce/notes/mixins/draft';
import { s__, sprintf } from '../../locale';
import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
......@@ -23,7 +24,7 @@ export default {
noteBody,
TimelineEntryItem,
},
mixins: [noteable, resolvable],
mixins: [noteable, resolvable, draftMixin],
props: {
note: {
type: Object,
......@@ -73,9 +74,6 @@ export default {
'is-editable': this.note.current_user.can_edit,
};
},
canResolve() {
return this.note.resolvable && !!this.getUserData.id;
},
canReportAsAbuse() {
return !!this.note.report_abuse_path && this.author.id !== this.getUserData.id;
},
......@@ -156,12 +154,16 @@ export default {
this.$refs.noteBody.resetAutoSave();
this.$emit('updateSuccess');
},
formUpdateHandler(noteText, parentElement, callback) {
formUpdateHandler(noteText, parentElement, callback, resolveDiscussion) {
this.$emit('handleUpdateNote', {
note: this.note,
noteText,
resolveDiscussion,
callback: () => this.updateSuccess(),
});
if (this.isDraft) return;
const data = {
endpoint: this.note.path,
note: {
......@@ -234,6 +236,7 @@ export default {
<div class="timeline-content">
<div class="note-header">
<note-header v-once :author="author" :created-at="note.created_at" :note-id="note.id">
<slot slot="note-header-info" name="note-header-info"></slot>
<span v-if="commit" v-html="actionText"></span>
<span v-else class="d-none d-sm-inline">&middot;</span>
</note-header>
......@@ -247,12 +250,15 @@ export default {
:can-award-emoji="note.current_user.can_award_emoji"
:can-delete="note.current_user.can_edit"
:can-report-as-abuse="canReportAsAbuse"
:can-resolve="note.current_user.can_resolve"
:can-resolve="canResolve"
:report-abuse-path="note.report_abuse_path"
:resolvable="note.resolvable"
:is-resolved="note.resolved"
:resolvable="note.resolvable || note.isDraft"
:is-resolved="note.resolved || note.resolve_discussion"
:is-resolving="isResolving"
:resolved-by="note.resolved_by"
:is-draft="note.isDraft"
:resolve-discussion="note.isDraft && note.resolve_discussion"
:discussion-id="discussionId"
@handleEdit="editHandler"
@handleDelete="deleteHandler"
@handleResolve="resolveHandler"
......
......@@ -7,6 +7,7 @@ export const COMMENT = 'comment';
export const OPENED = 'opened';
export const REOPENED = 'reopened';
export const CLOSED = 'closed';
export const MERGED = 'merged';
export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown';
export const ISSUE_NOTEABLE_TYPE = 'issue';
......
export default {
computed: {
isDraft: () => false,
canResolve() {
return this.note.current_user.can_resolve;
},
},
};
......@@ -84,10 +84,7 @@ export default {
</div>
</div>
<div>
<div
v-if="isFetchingMergeRequests"
class="related-related-merge-requests-icon qa-related-merge-requests-loading-icon"
>
<div v-if="isFetchingMergeRequests" class="qa-related-merge-requests-loading-icon">
<gl-loading-icon label="Fetching related merge requests" class="py-2" />
</div>
<ul v-else class="content-list related-items-list">
......
......@@ -74,8 +74,7 @@ export default {
}
if (!this.users.length) {
const emptyTooltipLabel =
this.issuableType === 'issue' ? __('Assignee(s)') : __('Assignee');
const emptyTooltipLabel = __('Assignee(s)');
names.push(emptyTooltipLabel);
}
......@@ -90,6 +89,27 @@ export default {
return counter;
},
mergeNotAllowedTooltipMessage() {
const assigneesCount = this.users.length;
if (this.issuableType !== 'merge_request' || assigneesCount === 0) {
return null;
}
const cannotMergeCount = this.users.filter(u => u.can_merge === false).length;
const canMergeCount = assigneesCount - cannotMergeCount;
if (canMergeCount === assigneesCount) {
// Everyone can merge
return null;
} else if (cannotMergeCount === assigneesCount && assigneesCount > 1) {
return 'No one can merge';
} else if (assigneesCount === 1) {
return 'Cannot merge';
}
return `${canMergeCount}/${assigneesCount} can merge`;
},
},
methods: {
assignSelf() {
......@@ -154,6 +174,15 @@ export default {
</button>
</div>
<div class="value hide-collapsed">
<span
v-if="mergeNotAllowedTooltipMessage"
v-tooltip
:title="mergeNotAllowedTooltipMessage"
data-placement="left"
class="float-right cannot-be-merged"
>
<i aria-hidden="true" data-hidden="true" class="fa fa-exclamation-triangle"></i>
</span>
<template v-if="hasNoUsers">
<span class="assign-yourself no-value">
No assignee
......
......@@ -162,7 +162,7 @@ export default {
</template>
<icon name="commit" class="commit-icon js-commit-icon" />
<gl-link :href="commitUrl" class="commit-sha"> {{ shortSha }} </gl-link>
<gl-link :href="commitUrl" class="commit-sha mr-0"> {{ shortSha }} </gl-link>
<div class="commit-title flex-truncate-parent">
<span v-if="title" class="flex-truncate-child">
......
......@@ -25,6 +25,18 @@ $item-weight-max-width: 48px;
flex-grow: 1;
}
.issue-token-state-icon-open {
color: $green-500;
}
.issue-token-state-icon-closed {
color: $blue-500;
}
.merge-request-status.closed {
color: $red-500;
}
.issue-token-state-icon-open,
.issue-token-state-icon-closed,
.confidential-icon,
......
......@@ -12,34 +12,6 @@
.environments-container {
.ci-table {
.deployment-column {
> span {
word-break: break-all;
}
.avatar {
float: none;
}
}
.btn-group {
> .btn:not(.btn-danger) {
color: $gl-text-color-secondary;
}
svg path {
fill: $gl-text-color-secondary;
}
.dropdown {
outline: none;
}
}
.btn .text-center {
display: inline;
}
.commit-title {
margin: 0;
}
......@@ -49,47 +21,16 @@
color: $gl-text-color-secondary;
}
.dropdown-menu {
.fa {
margin-right: 6px;
color: $gl-text-color-secondary;
}
}
.build-link,
.ref-name {
color: $gl-text-color;
}
.stop-env-link,
.external-url {
color: $gl-text-color-secondary;
.stop-env-icon {
font-size: 14px;
}
}
.deployment .build-column {
.build-link {
color: $gl-text-color;
}
.avatar {
float: none;
margin-right: 0;
}
}
.folder-icon {
margin-right: 3px;
color: $gl-text-color-secondary;
display: inline-block;
vertical-align: text-top;
.fa:nth-child(1) {
margin-right: 3px;
}
}
.folder-name {
......@@ -103,12 +44,6 @@
text-align: center;
}
.branch-commit {
.commit-sha {
margin-right: 0;
}
}
.no-btn {
border: 0;
background: none;
......@@ -168,11 +103,6 @@
opacity: 0.25;
}
.prometheus-graph-overlay {
fill: none;
opacity: 0;
pointer-events: all;
}
.rect-text-metric {
fill: $white-light;
......@@ -203,276 +133,10 @@
stroke: $gray-darkest;
}
.prometheus-graphs {
.dropdowns {
.dropdown-menu-toggle {
svg {
position: absolute;
right: 5%;
top: 25%;
}
}
.dropdown-menu-toggle,
.dropdown-menu {
width: 240px;
}
}
}
.environments-actions {
.external-url,
.monitoring-url,
.terminal-button,
.stop-env-link {
.terminal-button {
width: 38px;
}
}
.prometheus-panel {
margin-top: 20px;
}
.prometheus-graph-group {
display: flex;
flex-wrap: wrap;
padding: $gl-padding / 2;
}
.prometheus-graph {
padding: $gl-padding / 2;
}
.prometheus-graph-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $gl-padding-8;
h5 {
font-size: $gl-font-size-large;
margin: 0;
}
}
.prometheus-graph-cursor {
position: absolute;
background: $gray-600;
width: 1px;
}
.prometheus-graph-flag {
display: block;
min-width: 160px;
border: 0;
box-shadow: 0 1px 4px 0 $black-transparent;
h5 {
padding: 0;
margin: 0;
font-size: 14px;
line-height: 1.2;
}
.deploy-meta-content {
border-bottom: 1px solid $white-dark;
svg {
height: 15px;
vertical-align: bottom;
}
}
&.popover {
padding: 0;
&.left {
left: auto;
right: 0;
margin-right: 10px;
> .arrow {
right: -14px;
border-left-color: $border-color;
}
> .arrow::after {
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 4px solid $gray-50;
}
.arrow-shadow {
right: -3px;
box-shadow: 1px 0 9px 0 $black-transparent;
}
}
&.right {
left: 0;
right: auto;
margin-left: 10px;
> .arrow {
left: -7px;
border-right-color: $border-color;
}
> .arrow::after {
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-right: 4px solid $gray-50;
}
.arrow-shadow {
left: -3px;
box-shadow: 1px 0 8px 0 $black-transparent;
}
}
> .arrow {
top: 10px;
margin: 0;
}
.arrow-shadow {
content: '';
position: absolute;
width: 7px;
height: 7px;
background-color: transparent;
transform: rotate(45deg);
top: 13px;
}
> .popover-title,
> .popover-content,
> .popover-header,
> .popover-body {
padding: 8px;
font-size: 12px;
white-space: nowrap;
position: relative;
}
> .popover-title {
background-color: $gray-50;
border-radius: $border-radius-default $border-radius-default 0 0;
}
}
strong {
font-weight: 600;
}
}
.prometheus-table {
border-collapse: collapse;
padding: 0;
margin: 0;
td {
vertical-align: middle;
+ td {
padding-left: 8px;
vertical-align: top;
}
}
.legend-metric-title {
font-size: 12px;
vertical-align: middle;
}
}
.prometheus-svg-container {
position: relative;
height: 0;
width: 100%;
padding: 0;
padding-bottom: 100%;
.text-metric-usage {
fill: $black;
font-weight: $gl-font-weight-normal;
font-size: 12px;
}
> svg {
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
text {
fill: $gl-text-color;
stroke-width: 0;
}
.text-metric-bold {
font-weight: $gl-font-weight-bold;
}
.label-axis-text {
fill: $black;
font-weight: $gl-font-weight-normal;
font-size: 10px;
}
.legend-axis-text {
fill: $black;
}
.tick {
> line {
stroke: $gray-darker;
}
> text {
fill: $gray-600;
font-size: 10px;
}
}
.y-label-text,
.x-label-text {
fill: $gray-darkest;
}
.axis-tick {
stroke: $gray-darker;
}
.deploy-info-text {
dominant-baseline: text-before-edge;
font-size: 12px;
}
.deploy-info-text-link {
font-family: $monospace-font;
fill: $blue-600;
&:hover {
fill: $blue-800;
}
}
@include media-breakpoint-down(sm) {
.label-axis-text,
.text-metric-usage,
.legend-axis-text {
font-size: 8px;
}
.tick > text {
font-size: 8px;
}
}
}
}
.prometheus-table-row-highlight {
background-color: $gray-100;
}
......@@ -498,6 +498,16 @@
flex: 1;
}
.issuable-meta {
.author-link {
display: inline-block;
}
.issuable-comments {
height: 18px;
}
}
.merge-request-title {
margin-bottom: 2px;
......
......@@ -67,6 +67,10 @@
}
}
.classification-label {
background-color: $red-500;
}
.toggle-wrapper {
margin-top: 5px;
}
......@@ -1158,6 +1162,8 @@ pre.light-well {
.cannot-be-merged:hover {
color: $red-500;
margin-top: 2px;
position: relative;
z-index: 2;
}
.private-forks-notice .private-fork-icon {
......
.prometheus-graphs {
.dropdowns {
.dropdown-menu-toggle {
svg {
position: absolute;
right: 5%;
top: 25%;
}
}
.dropdown-menu-toggle,
.dropdown-menu {
width: 240px;
}
}
}
.prometheus-panel {
margin-top: 20px;
}
.prometheus-graph-group {
display: flex;
flex-wrap: wrap;
padding: $gl-padding / 2;
}
.prometheus-graph {
padding: $gl-padding / 2;
}
.prometheus-graph-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: $gl-padding-8;
h5 {
font-size: $gl-font-size-large;
margin: 0;
}
}
.prometheus-graph-cursor {
position: absolute;
background: $gray-600;
width: 1px;
}
.prometheus-graph-flag {
display: block;
min-width: 160px;
border: 0;
box-shadow: 0 1px 4px 0 $black-transparent;
h5 {
padding: 0;
margin: 0;
font-size: 14px;
line-height: 1.2;
}
.deploy-meta-content {
border-bottom: 1px solid $white-dark;
svg {
height: 15px;
vertical-align: bottom;
}
}
&.popover {
padding: 0;
&.left {
left: auto;
right: 0;
margin-right: 10px;
> .arrow {
right: -14px;
border-left-color: $border-color;
}
> .arrow::after {
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 4px solid $gray-50;
}
.arrow-shadow {
right: -3px;
box-shadow: 1px 0 9px 0 $black-transparent;
}
}
&.right {
left: 0;
right: auto;
margin-left: 10px;
> .arrow {
left: -7px;
border-right-color: $border-color;
}
> .arrow::after {
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-right: 4px solid $gray-50;
}
.arrow-shadow {
left: -3px;
box-shadow: 1px 0 8px 0 $black-transparent;
}
}
> .arrow {
top: 10px;
margin: 0;
}
.arrow-shadow {
content: '';
position: absolute;
width: 7px;
height: 7px;
background-color: transparent;
transform: rotate(45deg);
top: 13px;
}
> .popover-title,
> .popover-content,
> .popover-header,
> .popover-body {
padding: 8px;
font-size: 12px;
white-space: nowrap;
position: relative;
}
> .popover-title {
background-color: $gray-50;
border-radius: $border-radius-default $border-radius-default 0 0;
}
}
strong {
font-weight: 600;
}
}
.prometheus-table {
border-collapse: collapse;
padding: 0;
margin: 0;
td {
vertical-align: middle;
+ td {
padding-left: 8px;
vertical-align: top;
}
}
.legend-metric-title {
font-size: 12px;
vertical-align: middle;
}
}
.prometheus-svg-container {
position: relative;
height: 0;
width: 100%;
padding: 0;
padding-bottom: 100%;
.text-metric-usage {
fill: $black;
font-weight: $gl-font-weight-normal;
font-size: 12px;
}
> svg {
position: absolute;
height: 100%;
width: 100%;
left: 0;
top: 0;
text {
fill: $gl-text-color;
stroke-width: 0;
}
.text-metric-bold {
font-weight: $gl-font-weight-bold;
}
.label-axis-text {
fill: $black;
font-weight: $gl-font-weight-normal;
font-size: 10px;
}
.legend-axis-text {
fill: $black;
}
.tick {
> line {
stroke: $gray-darker;
}
> text {
fill: $gray-600;
font-size: 10px;
}
}
.y-label-text,
.x-label-text {
fill: $gray-darkest;
}
.axis-tick {
stroke: $gray-darker;
}
.deploy-info-text {
dominant-baseline: text-before-edge;
font-size: 12px;
}
.deploy-info-text-link {
font-family: $monospace-font;
fill: $blue-600;
&:hover {
fill: $blue-800;
}
}
@include media-breakpoint-down(sm) {
.label-axis-text,
.text-metric-usage,
.legend-axis-text {
font-size: 8px;
}
.tick > text {
font-size: 8px;
}
}
}
}
.prometheus-table-row-highlight {
background-color: $gray-100;
}
.prometheus-graph-overlay {
fill: none;
opacity: 0;
pointer-events: all;
}
......@@ -124,7 +124,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def visible_application_setting_attributes
ApplicationSettingsHelper.visible_attributes + [
[
*::ApplicationSettingsHelper.visible_attributes,
*::ApplicationSettingsHelper.external_authorization_service_attributes,
:domain_blacklist_file,
disabled_oauth_sign_in_sources: [],
import_sources: [],
......
......@@ -192,12 +192,7 @@ module IssuableActions
def bulk_update_params
permitted_keys_array = permitted_keys.dup
if resource_name == 'issue'
permitted_keys_array << { assignee_ids: [] }
else
permitted_keys_array.unshift(:assignee_id)
end
permitted_keys_array << { assignee_ids: [] }
params.require(:update).permit(permitted_keys_array)
end
......
......@@ -190,15 +190,15 @@ module IssuableCollections
end
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def preload_for_collection
common_attributes = [:author, :assignees, :labels, :milestone]
@preload_for_collection ||= case collection_type
when 'Issue'
[:project, :author, :assignees, :labels, :milestone, project: :namespace]
common_attributes + [:project, project: :namespace]
when 'MergeRequest'
[
:target_project, :author, :assignee, :labels, :milestone,
source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
]
common_attributes + [:target_project, source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits]
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
# frozen_string_literal: true
module ProjectUnauthorized
extend ActiveSupport::Concern
# EE would override this
def project_unauthorized_proc
# no-op
lambda do |project|
if project
label = project.external_authorization_classification_label
rejection_reason = nil
unless ::Gitlab::ExternalAuthorization.access_allowed?(current_user, label)
rejection_reason = ::Gitlab::ExternalAuthorization.rejection_reason(current_user, label)
rejection_reason ||= _('External authorization denied access to this project')
end
if rejection_reason
access_denied!(rejection_reason)
end
end
end
end
end
......@@ -14,8 +14,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
respond_to do |format|
format.html do
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37434
# Also https://gitlab.com/gitlab-org/gitlab-ce/issues/40260
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/40260
Gitlab::GitalyClient.allow_n_plus_1_calls do
render
end
......
......@@ -22,7 +22,7 @@ class HelpController < ApplicationController
end
def show
@path = clean_path_info(path_params[:path])
@path = Rack::Utils.clean_path_info(path_params[:path])
respond_to do |format|
format.any(:markdown, :md, :html) do
......@@ -75,35 +75,4 @@ class HelpController < ApplicationController
params
end
PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
# Taken from ActionDispatch::FileHandler
# Cleans up the path, to prevent directory traversal outside the doc folder.
def clean_path_info(path_info)
parts = path_info.split(PATH_SEPS)
clean = []
# Walk over each part of the path
parts.each do |part|
# Turn `one//two` or `one/./two` into `one/two`.
next if part.empty? || part == '.'
if part == '..'
# Turn `one/two/../` into `one`
clean.pop
else
# Add simple folder names to the clean path.
clean << part
end
end
# If the path was an absolute path (i.e. `/` or `/one/two`),
# add `/` to the front of the clean path.
clean.unshift '/' if parts.empty? || parts.first.empty?
# Join all the clean path parts by the path separator.
::File.join(*clean)
end
end
......@@ -25,7 +25,7 @@ class Projects::BranchesController < Projects::ApplicationController
@refs_pipelines = @project.ci_pipelines.latest_successful_for_refs(@branches.map(&:name))
@merged_branch_names = repository.merged_branch_names(@branches.map(&:name))
# n+1: https://gitlab.com/gitlab-org/gitaly/issues/992
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/48097
Gitlab::GitalyClient.allow_n_plus_1_calls do
@max_commits = @branches.reduce(0) do |memo, branch|
diverging_commit_counts = repository.diverging_commit_counts(branch)
......
......@@ -193,7 +193,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
return unless Feature.enabled?(:metrics_time_window, project)
return unless params[:start].present? || params[:end].present?
params.require([:start, :end]).values_at(:start, :end)
params.require([:start, :end])
end
def search_environment_names
......
......@@ -20,7 +20,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
def merge_request_params_attributes
[
:allow_collaboration,
:assignee_id,
:description,
:force_remove_source_branch,
:lock_version,
......@@ -35,6 +34,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:title,
:discussion_locked,
label_ids: [],
assignee_ids: [],
update_task: [:index, :checked, :line_number, :line_source]
]
end
......
......@@ -345,6 +345,7 @@ class ProjectsController < Projects::ApplicationController
:container_registry_enabled,
:default_branch,
:description,
:external_authorization_classification_label,
:import_url,
:issues_tracker,
:issues_tracker_id,
......
......@@ -15,7 +15,7 @@ class RootController < Dashboard::ProjectsController
before_action :redirect_logged_user, if: -> { current_user.present? }
def index
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37434
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/40260
Gitlab::GitalyClient.allow_n_plus_1_calls do
super
end
......
......@@ -439,22 +439,6 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def by_assignee(items)
if filter_by_no_assignee?
items.where(assignee_id: nil)
elsif filter_by_any_assignee?
items.where('assignee_id IS NOT NULL')
elsif assignee
items.where(assignee_id: assignee.id)
elsif assignee_id? || assignee_username? # assignee not found
items.none
else
items
end
end
# rubocop: enable CodeReuse/ActiveRecord
def filter_by_no_assignee?
# Assignee_id takes precedence over assignee_username
[NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE
......@@ -478,6 +462,20 @@ class IssuableFinder
end
# rubocop: enable CodeReuse/ActiveRecord
def by_assignee(items)
if filter_by_no_assignee?
items.unassigned
elsif filter_by_any_assignee?
items.assigned
elsif assignee
items.assigned_to(assignee)
elsif assignee_id? || assignee_username? # assignee not found
items.none
else
items
end
end
# rubocop: disable CodeReuse/ActiveRecord
def by_milestone(items)
if milestones?
......
......@@ -144,18 +144,4 @@ class IssuesFinder < IssuableFinder
current_user.blank?
end
def by_assignee(items)
if filter_by_no_assignee?
items.unassigned
elsif filter_by_any_assignee?
items.assigned
elsif assignee
items.assigned_to(assignee)
elsif assignee_id? || assignee_username? # assignee not found
items.none
else
items
end
end
end
......@@ -81,7 +81,7 @@ class ProjectsFinder < UnionFinder
if private_only?
current_user.authorized_projects
else
Project.public_or_visible_to_user(current_user, params[:visibility_level])
Project.public_or_visible_to_user(current_user)
end
end
end
......
......@@ -119,6 +119,39 @@ module ApplicationSettingsHelper
options_for_select(options, selected)
end
def external_authorization_description
_("If enabled, access to projects will be validated on an external service"\
" using their classification label.")
end
def external_authorization_timeout_help_text
_("Time in seconds GitLab will wait for a response from the external "\
"service. When the service does not respond in time, access will be "\
"denied.")
end
def external_authorization_url_help_text
_("When leaving the URL blank, classification labels can still be "\
"specified without disabling cross project features or performing "\
"external authorization checks.")
end
def external_authorization_client_certificate_help_text
_("The X509 Certificate to use when mutual TLS is required to communicate "\
"with the external authorization service. If left blank, the server "\
"certificate is still validated when accessing over HTTPS.")
end
def external_authorization_client_key_help_text
_("The private key to use when a client certificate is provided. This value "\
"is encrypted at rest.")
end
def external_authorization_client_pass_help_text
_("The passphrase required to decrypt the private key. This is optional "\
"and the value is encrypted at rest.")
end
def visible_attributes
[
:admin_notification_email,
......@@ -238,6 +271,18 @@ module ApplicationSettingsHelper
]
end
def external_authorization_service_attributes
[
:external_auth_client_cert,
:external_auth_client_key,
:external_auth_client_key_pass,
:external_authorization_service_default_label,
:external_authorization_service_enabled,
:external_authorization_service_timeout,
:external_authorization_service_url
]
end
def expanded_by_default?
Rails.env.test?
end
......
......@@ -69,7 +69,7 @@ module BoardsHelper
end
def board_sidebar_user_data
dropdown_options = issue_assignees_dropdown_options
dropdown_options = assignees_dropdown_options('issue')
{
toggle: 'dropdown',
......
......@@ -17,8 +17,8 @@ module FormHelper
end
end
def issue_assignees_dropdown_options
{
def assignees_dropdown_options(issuable_type)
dropdown_data = {
toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data',
title: 'Select assignee',
filter: true,
......@@ -28,8 +28,8 @@ module FormHelper
first_user: current_user&.username,
null_user: true,
current_user: true,
project_id: @project&.id,
field_name: 'issue[assignee_ids][]',
project_id: (@target_project || @project)&.id,
field_name: "#{issuable_type}[assignee_ids][]",
default_label: 'Unassigned',
'max-select': 1,
'dropdown-header': 'Assignee',
......@@ -39,5 +39,36 @@ module FormHelper
current_user_info: UserSerializer.new.represent(current_user)
}
}
type = issuable_type.to_s
if type == 'issue' && issue_supports_multiple_assignees? ||
type == 'merge_request' && merge_request_supports_multiple_assignees?
dropdown_data = multiple_assignees_dropdown_options(dropdown_data)
end
dropdown_data
end
# Overwritten
def issue_supports_multiple_assignees?
false
end
# Overwritten
def merge_request_supports_multiple_assignees?
false
end
private
def multiple_assignees_dropdown_options(options)
new_options = options.dup
new_options[:title] = 'Select assignee(s)'
new_options[:data][:'dropdown-header'] = 'Assignee(s)'
new_options[:data].delete(:'max-select')
new_options
end
end
......@@ -15,11 +15,14 @@ module IssuablesHelper
sidebar_gutter_collapsed? ? _('Expand sidebar') : _('Collapse sidebar')
end
def sidebar_assignee_tooltip_label(issuable)
if issuable.assignee
issuable.assignee.name
def assignees_label(issuable, include_value: true)
label = 'Assignee'.pluralize(issuable.assignees.count)
if include_value
sanitized_list = sanitize_name(issuable.assignee_list)
"#{label}: #{sanitized_list}"
else
issuable.allows_multiple_assignees? ? _('Assignee(s)') : _('Assignee')
label
end
end
......
......@@ -303,6 +303,16 @@ module ProjectsHelper
@path.present?
end
def external_classification_label_help_message
default_label = ::Gitlab::CurrentSettings.current_application_settings
.external_authorization_service_default_label
s_(
"ExternalAuthorizationService|When no classification label is set the "\
"default label `%{default_label}` will be used."
) % { default_label: default_label }
end
private
def get_project_nav_tabs(project, current_user)
......
......@@ -24,10 +24,12 @@ module Emails
end
# rubocop: disable CodeReuse/ActiveRecord
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id, reason = nil)
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_ids, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id)
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
@previous_assignees = []
@previous_assignees = User.where(id: previous_assignee_ids) if previous_assignee_ids.any?
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end
# rubocop: enable CodeReuse/ActiveRecord
......
......@@ -4,6 +4,7 @@ class Notify < BaseMailer
include ActionDispatch::Routing::PolymorphicRoutes
include GitlabRoutingHelper
include EmailsHelper
include IssuablesHelper
include Emails::Issues
include Emails::MergeRequests
......@@ -24,6 +25,7 @@ class Notify < BaseMailer
helper MembersHelper
helper AvatarsHelper
helper GitlabRoutingHelper
helper IssuablesHelper
def test_email(recipient_email, subject, body)
mail(to: recipient_email,
......
......@@ -213,6 +213,40 @@ class ApplicationSetting < ApplicationRecord
validate :terms_exist, if: :enforce_terms?
validates :external_authorization_service_default_label,
presence: true,
if: :external_authorization_service_enabled
validates :external_authorization_service_url,
url: true, allow_blank: true,
if: :external_authorization_service_enabled
validates :external_authorization_service_timeout,
numericality: { greater_than: 0, less_than_or_equal_to: 10 },
if: :external_authorization_service_enabled
validates :external_auth_client_key,
presence: true,
if: -> (setting) { setting.external_auth_client_cert.present? }
validates_with X509CertificateCredentialsValidator,
certificate: :external_auth_client_cert,
pkey: :external_auth_client_key,
pass: :external_auth_client_key_pass,
if: -> (setting) { setting.external_auth_client_cert.present? }
attr_encrypted :external_auth_client_key,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-gcm',
encode: true
attr_encrypted :external_auth_client_key_pass,
mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated,
algorithm: 'aes-256-gcm',
encode: true
before_validation :ensure_uuid!
before_validation :strip_sentry_values
......
......@@ -110,7 +110,7 @@ class Blob < SimpleDelegator
end
def load_all_data!
# Endpoint needed: gitlab-org/gitaly#756
# Endpoint needed: https://gitlab.com/gitlab-org/gitaly/issues/756
Gitlab::GitalyClient.allow_n_plus_1_calls do
super(project.repository) if project
end
......
......@@ -750,6 +750,10 @@ module Ci
self.sha == sha || self.source_sha == sha
end
def triggered_by?(current_user)
user == current_user
end
private
def ci_yaml_from_repo
......
# frozen_string_literal: true
# This module handles backward compatibility for import/export of Merge Requests after
# multiple assignees feature was introduced. Also, it handles the scenarios where
# the #26496 background migration hasn't finished yet.
# Ideally, most of this code should be removed at #59457.
module DeprecatedAssignee
extend ActiveSupport::Concern
def assignee_ids=(ids)
nullify_deprecated_assignee
super
end
def assignees=(users)
nullify_deprecated_assignee
super
end
def assignee_id=(id)
self.assignee_ids = Array(id)
end
def assignee=(user)
self.assignees = Array(user)
end
def assignee
assignees.first
end
def assignee_id
assignee_ids.first
end
def assignee_ids
if Gitlab::Database.read_only? && pending_assignees_population?
return Array(deprecated_assignee_id)
end
update_assignees_relation
super
end
def assignees
if Gitlab::Database.read_only? && pending_assignees_population?
return User.where(id: deprecated_assignee_id)
end
update_assignees_relation
super
end
private
# This will make the background migration process quicker (#26496) as it'll have less
# assignee_id rows to look through.
def nullify_deprecated_assignee
return unless persisted? && Gitlab::Database.read_only?
update_column(:assignee_id, nil)
end
# This code should be removed in the clean-up phase of the
# background migration (#59457).
def pending_assignees_population?
persisted? && deprecated_assignee_id && merge_request_assignees.empty?
end
# If there's an assignee_id and no relation, it means the background
# migration at #26496 didn't reach this merge request yet.
# This code should be removed in the clean-up phase of the
# background migration (#59457).
def update_assignees_relation
if pending_assignees_population?
transaction do
merge_request_assignees.create!(user_id: deprecated_assignee_id, merge_request_id: id)
update_column(:assignee_id, nil)
end
end
end
def deprecated_assignee_id
read_attribute(:assignee_id)
end
end
......@@ -67,13 +67,6 @@ module Issuable
allow_nil: true,
prefix: true
delegate :name,
:email,
:public_email,
to: :assignee,
allow_nil: true,
prefix: true
validates :author, presence: true
validates :title, presence: true, length: { maximum: 255 }
validate :milestone_is_valid
......@@ -88,6 +81,19 @@ module Issuable
scope :only_opened, -> { with_state(:opened) }
scope :closed, -> { with_state(:closed) }
# rubocop:disable GitlabSecurity/SqlInjection
# The `to_ability_name` method is not an user input.
scope :assigned, -> do
where("EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)")
end
scope :unassigned, -> do
where("NOT EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)")
end
scope :assigned_to, ->(u) do
where("EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE user_id = ? AND #{to_ability_name}_id = #{to_ability_name}s.id)", u.id)
end
# rubocop:enable GitlabSecurity/SqlInjection
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC') }
scope :order_milestone_due_asc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC') }
......@@ -104,6 +110,7 @@ module Issuable
participant :author
participant :notes_with_associations
participant :assignees
strip_attributes :title
......@@ -270,6 +277,10 @@ module Issuable
end
end
def assignee_or_author?(user)
author_id == user.id || assignees.exists?(user.id)
end
def today?
Date.today == created_at.to_date
end
......@@ -314,11 +325,7 @@ module Issuable
end
if old_assignees != assignees
if self.is_a?(Issue)
changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
else
changes[:assignee] = [old_assignees&.first&.hook_attrs, assignee&.hook_attrs]
end
changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
end
if self.respond_to?(:total_time_spent)
......@@ -355,10 +362,18 @@ module Issuable
def card_attributes
{
'Author' => author.try(:name),
'Assignee' => assignee.try(:name)
'Assignee' => assignee_list
}
end
def assignee_list
assignees.map(&:name).to_sentence
end
def assignee_username_list
assignees.map(&:username).to_sentence
end
def notes_with_associations
# If A has_many Bs, and B has_many Cs, and you do
# `A.includes(b: :c).each { |a| a.b.includes(:c) }`, sadly ActiveRecord
......
......@@ -49,10 +49,6 @@ class Issue < ApplicationRecord
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
scope :assigned, -> { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
scope :unassigned, -> { where('NOT EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
scope :assigned_to, ->(u) { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = ? AND issue_id = issues.id)', u.id)}
scope :with_due_date, -> { where.not(due_date: nil) }
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
......@@ -75,8 +71,6 @@ class Issue < ApplicationRecord
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
participant :assignees
state_machine :state, initial: :opened do
event :close do
transition [:opened] => :closed
......@@ -155,22 +149,6 @@ class Issue < ApplicationRecord
Gitlab::HookData::IssueBuilder.new(self).build
end
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
'Author' => author.try(:name),
'Assignee' => assignee_list
}
end
def assignee_or_author?(user)
author_id == user.id || assignees.exists?(user.id)
end
def assignee_list
assignees.map(&:name).to_sentence
end
# `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
......@@ -230,7 +208,13 @@ class Issue < ApplicationRecord
def visible_to_user?(user = nil)
return false unless project && project.feature_available?(:issues, user)
user ? readable_by?(user) : publicly_visible?
return publicly_visible? unless user
return false unless readable_by?(user)
user.full_private_access? ||
::Gitlab::ExternalAuthorization.access_allowed?(
user, project.external_authorization_classification_label)
end
def check_for_spam?
......@@ -298,7 +282,7 @@ class Issue < ApplicationRecord
# Returns `true` if this Issue is visible to everybody.
def publicly_visible?
project.public? && !confidential?
project.public? && !confidential? && !::Gitlab::ExternalAuthorization.enabled?
end
def expire_etag_cache
......
......@@ -16,6 +16,7 @@ class MergeRequest < ApplicationRecord
include LabelEventable
include ReactiveCaching
include FromUnion
include DeprecatedAssignee
self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
self.reactive_cache_refresh_interval = 10.minutes
......@@ -69,8 +70,7 @@ class MergeRequest < ApplicationRecord
has_many :suggestions, through: :notes
has_many :merge_request_assignees
# Will be deprecated at https://gitlab.com/gitlab-org/gitlab-ce/issues/59457
belongs_to :assignee, class_name: "User"
has_many :assignees, class_name: "User", through: :merge_request_assignees
serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
......@@ -79,10 +79,6 @@ class MergeRequest < ApplicationRecord
after_update :reload_diff_if_branch_changed
after_save :ensure_metrics
# Required until the codebase starts using this relation for single or multiple assignees.
# TODO: Remove at gitlab-ee#2004 implementation.
after_save :refresh_merge_request_assignees, if: :assignee_id_changed?
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
attr_accessor :allow_broken
......@@ -188,19 +184,14 @@ class MergeRequest < ApplicationRecord
end
scope :join_project, -> { joins(:target_project) }
scope :references_project, -> { references(:target_project) }
scope :assigned, -> { where("assignee_id IS NOT NULL") }
scope :unassigned, -> { where("assignee_id IS NULL") }
scope :assigned_to, ->(u) { where(assignee_id: u.id)}
scope :with_api_entity_associations, -> {
preload(:author, :assignee, :notes, :labels, :milestone, :timelogs,
preload(:assignees, :author, :notes, :labels, :milestone, :timelogs,
latest_merge_request_diff: [:merge_request_diff_commits],
metrics: [:latest_closed_by, :merged_by],
target_project: [:route, { namespace: :route }],
source_project: [:route, { namespace: :route }])
}
participant :assignee
after_save :keep_around_commit
alias_attribute :project, :target_project
......@@ -337,31 +328,6 @@ class MergeRequest < ApplicationRecord
Gitlab::HookData::MergeRequestBuilder.new(self).build
end
# Returns a Hash of attributes to be used for Twitter card metadata
def card_attributes
{
'Author' => author.try(:name),
'Assignee' => assignee.try(:name)
}
end
# These method are needed for compatibility with issues to not mess view and other code
def assignees
Array(assignee)
end
def assignee_ids
Array(assignee_id)
end
def assignee_ids=(ids)
write_attribute(:assignee_id, ids.last)
end
def assignee_or_author?(user)
author_id == user.id || assignee_id == user.id
end
# `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
......@@ -682,15 +648,6 @@ class MergeRequest < ApplicationRecord
merge_request_diff || create_merge_request_diff
end
def refresh_merge_request_assignees
transaction do
# Using it instead relation.delete_all in order to avoid adding a
# dependent: :delete_all (we already have foreign key cascade deletion).
MergeRequestAssignee.where(merge_request_id: self).delete_all
merge_request_assignees.create(user_id: assignee_id) if assignee_id
end
end
def create_merge_request_diff
fetch_ref!
......@@ -1208,7 +1165,7 @@ class MergeRequest < ApplicationRecord
variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL', value: project.web_url)
variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME', value: target_branch.to_s)
variables.append(key: 'CI_MERGE_REQUEST_TITLE', value: title)
variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee.username) if assignee
variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee_username_list) if assignees.any?
variables.append(key: 'CI_MERGE_REQUEST_MILESTONE', value: milestone.title) if milestone
variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present?
variables.concat(source_project_variables)
......
......@@ -459,41 +459,14 @@ class Project < ApplicationRecord
# Returns a collection of projects that is either public or visible to the
# logged in user.
#
# requested_visiblity_levels: Normally all projects that are visible
# to the user (e.g. internal and public) are queried, but this
# parameter allows the caller to narrow the search space to optimize
# database queries. For instance, a caller may only want to see
# internal projects. Instead of querying for internal and public
# projects and throwing away public projects, this parameter allows
# the query to be targeted for only internal projects.
def self.public_or_visible_to_user(user = nil, requested_visibility_levels = [])
return public_to_user unless user
visible_levels = Gitlab::VisibilityLevel.levels_for_user(user)
include_private = true
requested_visibility_levels = Array(requested_visibility_levels)
if requested_visibility_levels.present?
visible_levels &= requested_visibility_levels
include_private = requested_visibility_levels.include?(Gitlab::VisibilityLevel::PRIVATE)
end
public_or_internal_rel =
if visible_levels.present?
where('projects.visibility_level IN (?)', visible_levels)
else
Project.none
end
private_rel =
if include_private
where('EXISTS (?)', user.authorizations_for_projects)
else
Project.none
end
public_or_internal_rel.or(private_rel)
def self.public_or_visible_to_user(user = nil)
if user
where('EXISTS (?) OR projects.visibility_level IN (?)',
user.authorizations_for_projects,
Gitlab::VisibilityLevel.levels_for_user(user))
else
public_to_user
end
end
# project features may be "disabled", "internal", "enabled" or "public". If "internal",
......@@ -674,6 +647,10 @@ class Project < ApplicationRecord
{ scope: :project, status: auto_devops&.enabled || Feature.enabled?(:force_autodevops_on_by_default, self) }
end
def multiple_mr_assignees_enabled?
Feature.enabled?(:multiple_merge_request_assignees, self)
end
def daily_statistics_enabled?
Feature.enabled?(:project_daily_statistics, self, default_enabled: true)
end
......@@ -2062,6 +2039,11 @@ class Project < ApplicationRecord
fetch_branch_allows_collaboration(user, branch_name)
end
def external_authorization_classification_label
super || ::Gitlab::CurrentSettings.current_application_settings
.external_authorization_service_default_label
end
def licensed_features
[]
end
......
......@@ -22,6 +22,13 @@ class BasePolicy < DeclarativePolicy::Base
Gitlab::CurrentSettings.current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
end
# This is prevented in some cases in `gitlab-ee`
condition(:external_authorization_enabled, scope: :global, score: 0) do
::Gitlab::ExternalAuthorization.perform_check?
end
rule { external_authorization_enabled & ~full_private_access }.policy do
prevent :read_cross_project
end
rule { default }.enable :read_cross_project
end
......@@ -14,6 +14,10 @@ module Ci
@subject.external?
end
condition(:triggerer_of_pipeline) do
@subject.triggered_by?(@user)
end
# Disallow users without permissions from accessing internal pipelines
rule { ~can?(:read_build) & ~external_pipeline }.policy do
prevent :read_pipeline
......@@ -29,6 +33,14 @@ module Ci
enable :destroy_pipeline
end
rule { can?(:admin_pipeline) }.policy do
enable :read_pipeline_variable
end
rule { can?(:update_pipeline) & triggerer_of_pipeline }.policy do
enable :read_pipeline_variable
end
def ref_protected?(user, project, tag, ref)
access = ::Gitlab::UserAccess.new(user, project: project)
......
......@@ -89,6 +89,15 @@ class ProjectPolicy < BasePolicy
::Gitlab::CurrentSettings.current_application_settings.mirror_available
end
with_scope :subject
condition(:classification_label_authorized, score: 32) do
::Gitlab::ExternalAuthorization.access_allowed?(
@user,
@subject.external_authorization_classification_label,
@subject.full_path
)
end
# We aren't checking `:read_issue` or `:read_merge_request` in this case
# because it could be possible for a user to see an issuable-iid
# (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be
......@@ -417,6 +426,25 @@ class ProjectPolicy < BasePolicy
rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster
rule { ~can?(:read_cross_project) & ~classification_label_authorized }.policy do
# Preventing access here still allows the projects to be listed. Listing
# projects doesn't check the `:read_project` ability. But instead counts
# on the `project_authorizations` table.
#
# All other actions should explicitly check read project, which would
# trigger the `classification_label_authorized` condition.
#
# `:read_project_for_iids` is not prevented by this condition, as it is
# used for cross-project reference checks.
prevent :guest_access
prevent :public_access
prevent :public_user_access
prevent :reporter_access
prevent :developer_access
prevent :maintainer_access
prevent :owner_access
end
private
def team_member?
......
......@@ -11,4 +11,6 @@ class IssuableSidebarExtrasEntity < Grape::Entity
expose :subscribed do |issuable|
issuable.subscribed?(request.current_user, issuable.project)
end
expose :assignees, using: API::Entities::UserBasic
end
# frozen_string_literal: true
class IssueSidebarExtrasEntity < IssuableSidebarExtrasEntity
expose :assignees, using: API::Entities::UserBasic
end
# frozen_string_literal: true
class MergeRequestAssigneeEntity < ::API::Entities::UserBasic
expose :can_merge do |assignee, options|
options[:merge_request]&.can_be_merged_by?(assignee)
end
end
# frozen_string_literal: true
class MergeRequestBasicEntity < Grape::Entity
expose :assignee_id
expose :merge_status
expose :merge_error
expose :state
......@@ -9,7 +8,7 @@ class MergeRequestBasicEntity < Grape::Entity
expose :rebase_in_progress?, as: :rebase_in_progress
expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity
expose :assignee, using: API::Entities::UserBasic
expose :assignees, using: API::Entities::UserBasic
expose :task_status, :task_status_short
expose :lock_version, :lock_version
end
......@@ -8,9 +8,9 @@ class MergeRequestSerializer < BaseSerializer
entity =
case opts[:serializer]
when 'sidebar'
MergeRequestSidebarBasicEntity
IssuableSidebarBasicEntity
when 'sidebar_extras'
IssuableSidebarExtrasEntity
MergeRequestSidebarExtrasEntity
when 'basic'
MergeRequestBasicEntity
else
......
# frozen_string_literal: true
class MergeRequestSidebarBasicEntity < IssuableSidebarBasicEntity
expose :assignee, if: lambda { |issuable| issuable.assignee } do
expose :assignee, merge: true, using: API::Entities::UserBasic
expose :can_merge do |issuable|
issuable.can_be_merged_by?(issuable.assignee)
end
end
end
# frozen_string_literal: true
class MergeRequestSidebarExtrasEntity < IssuableSidebarExtrasEntity
expose :assignees do |merge_request|
MergeRequestAssigneeEntity.represent(merge_request.assignees, merge_request: merge_request)
end
end
......@@ -7,23 +7,7 @@ class PipelineSerializer < BaseSerializer
# rubocop: disable CodeReuse/ActiveRecord
def represent(resource, opts = {})
if resource.is_a?(ActiveRecord::Relation)
resource = resource.preload([
:stages,
:retryable_builds,
:cancelable_statuses,
:trigger_requests,
:manual_actions,
:scheduled_actions,
:artifacts,
:merge_request,
{
pending_builds: :project,
project: [:route, { namespace: :route }],
artifacts: {
project: [:route, { namespace: :route }]
}
}
])
resource = resource.preload(preloaded_relations)
end
if paginated?
......@@ -51,4 +35,26 @@ class PipelineSerializer < BaseSerializer
data = represent(resource, { only: [{ details: [:stages] }], preload: true })
data.dig(:details, :stages) || []
end
private
def preloaded_relations
[
:stages,
:retryable_builds,
:cancelable_statuses,
:trigger_requests,
:manual_actions,
:scheduled_actions,
:artifacts,
:merge_request,
{
pending_builds: :project,
project: [:route, { namespace: :route }],
artifacts: {
project: [:route, { namespace: :route }]
}
}
]
end
end
......@@ -2,9 +2,17 @@
module ApplicationSettings
class UpdateService < ApplicationSettings::BaseService
include ValidatesClassificationLabel
attr_reader :params, :application_setting
def execute
validate_classification_label(application_setting, :external_authorization_service_default_label)
if application_setting.errors.any?
return false
end
update_terms(@params.delete(:terms))
if params.key?(:performance_bar_allowed_group_path)
......
......@@ -37,7 +37,7 @@ module Ci
variables_attributes: params[:variables_attributes],
project: project,
current_user: current_user,
push_options: params[:push_options],
push_options: params[:push_options] || {},
chat_data: params[:chat_data],
**extra_options(options))
......
......@@ -16,6 +16,7 @@ module Clusters
error_code: error.respond_to?(:error_code) ? error.error_code : nil,
service: self.class.name,
app_id: app.id,
app_name: app.name,
project_ids: app.cluster.project_ids,
group_ids: app.cluster.group_ids
}
......@@ -30,6 +31,19 @@ module Clusters
Gitlab::Sentry.track_acceptable_exception(error, extra: meta)
end
def log_event(event)
meta = {
service: self.class.name,
app_id: app.id,
app_name: app.name,
project_ids: app.cluster.project_ids,
group_ids: app.cluster.group_ids,
event: event
}
logger.info(meta)
end
def logger
@logger ||= Gitlab::Kubernetes::Logger.build
end
......
......@@ -7,8 +7,10 @@ module Clusters
return unless app.scheduled?
app.make_installing!
log_event(:begin_install)
helm_api.install(install_command)
log_event(:schedule_wait_for_installation)
ClusterWaitForAppInstallationWorker.perform_in(
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
rescue Kubeclient::HttpError => e
......
......@@ -8,8 +8,10 @@ module Clusters
app.make_updating!
log_event(:begin_patch)
helm_api.update(update_command)
log_event(:schedule_wait_for_patch)
ClusterWaitForAppInstallationWorker.perform_in(
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
rescue Kubeclient::HttpError => e
......
......@@ -9,10 +9,12 @@ module Clusters
begin
app.make_updating!
log_event(:begin_upgrade)
# install_command works with upgrades too
# as it basically does `helm upgrade --install`
helm_api.update(install_command)
log_event(:schedule_wait_for_upgrade)
ClusterWaitForAppInstallationWorker.perform_in(
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
rescue Kubeclient::HttpError => e
......
# frozen_string_literal: true
module ValidatesClassificationLabel
def validate_classification_label(record, attribute_name)
return unless ::Gitlab::ExternalAuthorization.enabled?
return unless classification_label_change?(record, attribute_name)
new_label = params[attribute_name].presence
new_label ||= ::Gitlab::CurrentSettings.current_application_settings
.external_authorization_service_default_label
unless ::Gitlab::ExternalAuthorization.access_allowed?(current_user, new_label)
reason = rejection_reason_for_label(new_label)
message = s_('ClassificationLabelUnavailable|is unavailable: %{reason}') % { reason: reason }
record.errors.add(attribute_name, message)
end
end
def rejection_reason_for_label(label)
reason_from_service = ::Gitlab::ExternalAuthorization.rejection_reason(current_user, label).presence
reason_from_service || _("Access to '%{classification_label}' not allowed") % { classification_label: label }
end
def classification_label_change?(record, attribute_name)
params.key?(attribute_name) || record.new_record?
end
end
......@@ -79,7 +79,7 @@ module Git
limited_commits,
event_message,
commits_count: commits_count,
push_options: params[:push_options] || []
push_options: params[:push_options] || {}
)
# Dependent code may modify the push data, so return a duplicate each time
......
......@@ -34,14 +34,20 @@ class IssuableBaseService < BaseService
end
def filter_assignee(issuable)
return unless params[:assignee_id].present?
return if params[:assignee_ids].blank?
assignee_id = params[:assignee_id]
unless issuable.allows_multiple_assignees?
params[:assignee_ids] = params[:assignee_ids].first(1)
end
assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
if assignee_id.to_s == IssuableFinder::NONE
params[:assignee_id] = ""
if params[:assignee_ids].map(&:to_s) == [IssuableFinder::NONE]
params[:assignee_ids] = []
elsif assignee_ids.any?
params[:assignee_ids] = assignee_ids
else
params.delete(:assignee_id) unless assignee_can_read?(issuable, assignee_id)
params.delete(:assignee_ids)
end
end
......@@ -352,7 +358,7 @@ class IssuableBaseService < BaseService
end
def has_changes?(issuable, old_labels: [], old_assignees: [])
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
valid_attrs = [:title, :description, :assignee_ids, :milestone_id, :target_branch]
attrs_changed = valid_attrs.any? do |attr|
issuable.previous_changes.include?(attr.to_s)
......
......@@ -20,7 +20,7 @@ module Issues
private
def create_assignee_note(issue, old_assignees)
SystemNoteService.change_issue_assignees(
SystemNoteService.change_issuable_assignees(
issue, issue.project, current_user, old_assignees)
end
......@@ -31,26 +31,6 @@ module Issues
issue.project.execute_services(issue_data, hooks_scope)
end
# rubocop: disable CodeReuse/ActiveRecord
def filter_assignee(issuable)
return if params[:assignee_ids].blank?
unless issuable.allows_multiple_assignees?
params[:assignee_ids] = params[:assignee_ids].take(1)
end
assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
if params[:assignee_ids].map(&:to_s) == [IssuableFinder::NONE]
params[:assignee_ids] = []
elsif assignee_ids.any?
params[:assignee_ids] = assignee_ids
else
params.delete(:assignee_ids)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def update_project_counter_caches?(issue)
super || issue.confidential_changed?
end
......
......@@ -39,7 +39,7 @@ module Issues
if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees)
notification_service.async.reassigned_issue(issue, current_user, old_assignees)
todo_service.reassigned_issue(issue, current_user, old_assignees)
todo_service.reassigned_issuable(issue, current_user, old_assignees)
end
if issue.previous_changes.include?('confidential')
......
......@@ -49,9 +49,9 @@ module MergeRequests
MergeRequestMetricsService.new(merge_request.metrics)
end
def create_assignee_note(merge_request)
SystemNoteService.change_assignee(
merge_request, merge_request.project, current_user, merge_request.assignee)
def create_assignee_note(merge_request, old_assignees)
SystemNoteService.change_issuable_assignees(
merge_request, merge_request.project, current_user, old_assignees)
end
def create_pipeline_for(merge_request, user)
......
# frozen_string_literal: true
module MergeRequests
class PushOptionsHandlerService
LIMIT = 10
attr_reader :branches, :changes_by_branch, :current_user, :errors,
:project, :push_options, :target_project
def initialize(project, current_user, changes, push_options)
@project = project
@target_project = @project.default_merge_request_target
@current_user = current_user
@branches = get_branches(changes)
@push_options = push_options
@errors = []
end
def execute
validate_service
return self if errors.present?
branches.each do |branch|
execute_for_branch(branch)
rescue Gitlab::Access::AccessDeniedError
errors << 'User access was denied'
rescue StandardError => e
Gitlab::AppLogger.error(e)
errors << 'An unknown error occurred'
end
self
end
private
def get_branches(raw_changes)
Gitlab::ChangesList.new(raw_changes).map do |changes|
next unless Gitlab::Git.branch_ref?(changes[:ref])
# Deleted branch
next if Gitlab::Git.blank_ref?(changes[:newrev])
# Default branch
branch_name = Gitlab::Git.branch_name(changes[:ref])
next if branch_name == target_project.default_branch
branch_name
end.compact.uniq
end
def validate_service
errors << 'User is required' if current_user.nil?
unless target_project.merge_requests_enabled?
errors << "Merge requests are not enabled for project #{target_project.full_path}"
end
if branches.size > LIMIT
errors << "Too many branches pushed (#{branches.size} were pushed, limit is #{LIMIT})"
end
if push_options[:target] && !target_project.repository.branch_exists?(push_options[:target])
errors << "Branch #{push_options[:target]} does not exist"
end
end
# Returns a Hash of branch => MergeRequest
def merge_requests
@merge_requests ||= MergeRequest.from_project(target_project)
.opened
.from_source_branches(branches)
.index_by(&:source_branch)
end
def execute_for_branch(branch)
merge_request = merge_requests[branch]
if merge_request
update!(merge_request)
else
create!(branch)
end
end
def create!(branch)
unless push_options[:create]
errors << "A merge_request.create push option is required to create a merge request for branch #{branch}"
return
end
# Use BuildService to assign the standard attributes of a merge request
merge_request = ::MergeRequests::BuildService.new(
project,
current_user,
create_params(branch)
).execute
unless merge_request.errors.present?
merge_request = ::MergeRequests::CreateService.new(
project,
current_user,
merge_request.attributes.merge(assignees: merge_request.assignees)
).execute
end
collect_errors_from_merge_request(merge_request) unless merge_request.persisted?
end
def update!(merge_request)
merge_request = ::MergeRequests::UpdateService.new(
target_project,
current_user,
update_params
).execute(merge_request)
collect_errors_from_merge_request(merge_request) unless merge_request.valid?
end
def create_params(branch)
params = {
assignees: [current_user],
source_branch: branch,
source_project: project,
target_branch: push_options[:target] || target_project.default_branch,
target_project: target_project
}
if push_options.key?(:merge_when_pipeline_succeeds)
params.merge!(
merge_when_pipeline_succeeds: push_options[:merge_when_pipeline_succeeds],
merge_user: current_user
)
end
params
end
def update_params
params = {}
if push_options.key?(:merge_when_pipeline_succeeds)
params.merge!(
merge_when_pipeline_succeeds: push_options[:merge_when_pipeline_succeeds],
merge_user: current_user
)
end
if push_options.key?(:target)
params[:target_branch] = push_options[:target]
end
params
end
def collect_errors_from_merge_request(merge_request)
merge_request.errors.full_messages.each do |error|
errors << error
end
end
end
end
......@@ -14,7 +14,9 @@ module MergeRequests
private
def refresh_merge_requests!
# n + 1: https://gitlab.com/gitlab-org/gitlab-ce/issues/60289
Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits))
# Be sure to close outstanding MRs before reloading them to avoid generating an
# empty diff during a manual merge
close_upon_missing_source_branch_ref
......
......@@ -24,13 +24,13 @@ module MergeRequests
update_task_event(merge_request) || update(merge_request)
end
# rubocop:disable Metrics/AbcSize
def handle_changes(merge_request, options)
old_associations = options.fetch(:old_associations, {})
old_labels = old_associations.fetch(:labels, [])
old_mentioned_users = old_associations.fetch(:mentioned_users, [])
old_assignees = old_associations.fetch(:assignees, [])
if has_changes?(merge_request, old_labels: old_labels)
if has_changes?(merge_request, old_labels: old_labels, old_assignees: old_assignees)
todo_service.mark_pending_todos_as_done(merge_request, current_user)
end
......@@ -45,15 +45,10 @@ module MergeRequests
merge_request.target_branch)
end
if merge_request.previous_changes.include?('assignee_id')
reassigned_merge_request_args = [merge_request, current_user]
old_assignee_id = merge_request.previous_changes['assignee_id'].first
reassigned_merge_request_args << User.find(old_assignee_id) if old_assignee_id
create_assignee_note(merge_request)
notification_service.async.reassigned_merge_request(*reassigned_merge_request_args)
todo_service.reassigned_merge_request(merge_request, current_user)
if merge_request.assignees != old_assignees
create_assignee_note(merge_request, old_assignees)
notification_service.async.reassigned_merge_request(merge_request, current_user, old_assignees)
todo_service.reassigned_issuable(merge_request, current_user, old_assignees)
end
if merge_request.previous_changes.include?('target_branch') ||
......@@ -81,7 +76,6 @@ module MergeRequests
)
end
end
# rubocop:enable Metrics/AbcSize
def handle_task_changes(merge_request)
todo_service.mark_pending_todos_as_done(merge_request, current_user)
......
......@@ -247,15 +247,15 @@ module NotificationRecipientService
attr_reader :target
attr_reader :current_user
attr_reader :action
attr_reader :previous_assignee
attr_reader :previous_assignees
attr_reader :skip_current_user
def initialize(target, current_user, action:, custom_action: nil, previous_assignee: nil, skip_current_user: true)
def initialize(target, current_user, action:, custom_action: nil, previous_assignees: nil, skip_current_user: true)
@target = target
@current_user = current_user
@action = action
@custom_action = custom_action
@previous_assignee = previous_assignee
@previous_assignees = previous_assignees
@skip_current_user = skip_current_user
end
......@@ -270,11 +270,7 @@ module NotificationRecipientService
# Re-assign is considered as a mention of the new assignee
case custom_action
when :reassign_merge_request
add_recipients(previous_assignee, :mention, nil)
add_recipients(target.assignee, :mention, NotificationReason::ASSIGNED)
when :reassign_issue
previous_assignees = Array(previous_assignee)
when :reassign_merge_request, :reassign_issue
add_recipients(previous_assignees, :mention, nil)
add_recipients(target.assignees, :mention, NotificationReason::ASSIGNED)
end
......@@ -287,17 +283,11 @@ module NotificationRecipientService
# receive them, too.
add_mentions(current_user, target: target)
# Add the assigned users, if any
assignees = case custom_action
when :new_issue
target.assignees
else
target.assignee
end
# We use the `:participating` notification level in order to match existing legacy behavior as captured
# in existing specs (notification_service_spec.rb ~ line 507)
add_recipients(assignees, :participating, NotificationReason::ASSIGNED) if assignees
if target.is_a?(Issuable)
add_recipients(target.assignees, :participating, NotificationReason::ASSIGNED)
end
add_labels_subscribers
end
......
......@@ -95,8 +95,8 @@ class NotificationService
# When we reassign an issue we should send an email to:
#
# * issue old assignee if their notification level is not Disabled
# * issue new assignee if their notification level is not Disabled
# * issue old assignees if their notification level is not Disabled
# * issue new assignees if their notification level is not Disabled
# * users with custom level checked with "reassign issue"
#
def reassigned_issue(issue, current_user, previous_assignees = [])
......@@ -104,7 +104,7 @@ class NotificationService
issue,
current_user,
action: "reassign",
previous_assignee: previous_assignees
previous_assignees: previous_assignees
)
previous_assignee_ids = previous_assignees.map(&:id)
......@@ -140,7 +140,7 @@ class NotificationService
# When create a merge request we should send an email to:
#
# * mr author
# * mr assignee if their notification level is not Disabled
# * mr assignees if their notification level is not Disabled
# * project team members with notification level higher then Participating
# * watchers of the mr's labels
# * users with custom level checked with "new merge request"
......@@ -184,23 +184,25 @@ class NotificationService
# When we reassign a merge_request we should send an email to:
#
# * merge_request old assignee if their notification level is not Disabled
# * merge_request assignee if their notification level is not Disabled
# * merge_request old assignees if their notification level is not Disabled
# * merge_request new assignees if their notification level is not Disabled
# * users with custom level checked with "reassign merge request"
#
def reassigned_merge_request(merge_request, current_user, previous_assignee = nil)
def reassigned_merge_request(merge_request, current_user, previous_assignees = [])
recipients = NotificationRecipientService.build_recipients(
merge_request,
current_user,
action: "reassign",
previous_assignee: previous_assignee
previous_assignees: previous_assignees
)
previous_assignee_ids = previous_assignees.map(&:id)
recipients.each do |recipient|
mailer.reassigned_merge_request_email(
recipient.user.id,
merge_request.id,
previous_assignee&.id,
previous_assignee_ids,
current_user.id,
recipient.reason
).deliver_later
......
......@@ -2,6 +2,8 @@
module Projects
class CreateService < BaseService
include ValidatesClassificationLabel
def initialize(user, params)
@current_user, @params = user, params.dup
@skip_wiki = @params.delete(:skip_wiki)
......@@ -45,6 +47,8 @@ module Projects
relations_block&.call(@project)
yield(@project) if block_given?
validate_classification_label(@project, :external_authorization_classification_label)
# If the block added errors, don't try to save the project
return @project if @project.errors.any?
......
......@@ -3,6 +3,7 @@
module Projects
class UpdateService < BaseService
include UpdateVisibilityLevel
include ValidatesClassificationLabel
ValidationError = Class.new(StandardError)
......@@ -14,6 +15,8 @@ module Projects
yield if block_given?
validate_classification_label(project, :external_authorization_classification_label)
# If the block added errors, don't try to save the project
return update_failed! if project.errors.any?
......
# frozen_string_literal: true
module Projects
class UpdateStatisticsService < BaseService
def execute
return unless project && project.repository.exists?
Rails.logger.info("Updating statistics for project #{project.id}")
project.statistics.refresh!(only: statistics.map(&:to_sym))
end
private
def statistics
params[:statistics]
end
end
end
......@@ -69,7 +69,7 @@ module SystemNoteService
# Called when the assignees of an Issue is changed or removed
#
# issue - Issue object
# issuable - Issuable object (responds to assignees)
# project - Project owning noteable
# author - User performing the change
# assignees - Users being assigned, or nil
......@@ -85,9 +85,9 @@ module SystemNoteService
# "assigned to @user1 and @user2"
#
# Returns the created Note object
def change_issue_assignees(issue, project, author, old_assignees)
unassigned_users = old_assignees - issue.assignees
added_users = issue.assignees.to_a - old_assignees
def change_issuable_assignees(issuable, project, author, old_assignees)
unassigned_users = old_assignees - issuable.assignees
added_users = issuable.assignees.to_a - old_assignees
text_parts = []
text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
......@@ -95,7 +95,7 @@ module SystemNoteService
body = text_parts.join(' and ')
create_note(NoteSummary.new(issue, project, author, body, action: 'assignee'))
create_note(NoteSummary.new(issuable, project, author, body, action: 'assignee'))
end
# Called when the milestone of a Noteable is changed
......
......@@ -49,12 +49,12 @@ class TodoService
todo_users.each(&:update_todos_count_cache)
end
# When we reassign an issue we should:
# When we reassign an issuable we should:
#
# * create a pending todo for new assignee if issue is assigned
# * create a pending todo for new assignee if issuable is assigned
#
def reassigned_issue(issue, current_user, old_assignees = [])
create_assignment_todo(issue, current_user, old_assignees)
def reassigned_issuable(issuable, current_user, old_assignees = [])
create_assignment_todo(issuable, current_user, old_assignees)
end
# When create a merge request we should:
......@@ -82,14 +82,6 @@ class TodoService
mark_pending_todos_as_done(merge_request, current_user)
end
# When we reassign a merge request we should:
#
# * creates a pending todo for new assignee if merge request is assigned
#
def reassigned_merge_request(merge_request, current_user)
create_assignment_todo(merge_request, current_user)
end
# When merge a merge request we should:
#
# * mark all pending todos related to the target for the current user as done
......
......@@ -8,6 +8,7 @@ class VerifyPagesDomainService < BaseService
# How long verification lasts for
VERIFICATION_PERIOD = 7.days
REMOVAL_DELAY = 1.week.freeze
attr_reader :domain
......@@ -36,7 +37,7 @@ class VerifyPagesDomainService < BaseService
# Prevent any pre-existing grace period from being truncated
reverify = [domain.enabled_until, VERIFICATION_PERIOD.from_now].compact.max
domain.assign_attributes(verified_at: Time.now, enabled_until: reverify)
domain.assign_attributes(verified_at: Time.now, enabled_until: reverify, remove_at: nil)
domain.save!(validate: false)
if was_disabled
......@@ -49,18 +50,20 @@ class VerifyPagesDomainService < BaseService
end
def unverify_domain!
if domain.verified?
domain.assign_attributes(verified_at: nil)
domain.save!(validate: false)
was_verified = domain.verified?
notify(:verification_failed)
end
domain.assign_attributes(verified_at: nil)
domain.remove_at ||= REMOVAL_DELAY.from_now unless domain.enabled?
domain.save!(validate: false)
notify(:verification_failed) if was_verified
error("Couldn't verify #{domain.domain}")
end
def disable_domain!
domain.assign_attributes(verified_at: nil, enabled_until: nil)
domain.remove_at ||= REMOVAL_DELAY.from_now
domain.save!(validate: false)
notify(:disabled)
......
# frozen_string_literal: true
# X509CertificateCredentialsValidator
#
# Custom validator to check if certificate-attribute was signed using the
# private key stored in an attrebute.
#
# This can be used as an `ActiveModel::Validator` as follows:
#
# validates_with X509CertificateCredentialsValidator,
# certificate: :client_certificate,
# pkey: :decrypted_private_key,
# pass: :decrypted_passphrase
#
#
# Required attributes:
# - certificate: The name of the accessor that returns the certificate to check
# - pkey: The name of the accessor that returns the private key
# Optional:
# - pass: The name of the accessor that returns the passphrase to decrypt the
# private key
class X509CertificateCredentialsValidator < ActiveModel::Validator
def initialize(*args)
super
# We can't validate if we don't have a private key or certificate attributes
# in which case this validator is useless.
if options[:pkey].nil? || options[:certificate].nil?
raise 'Provide at least `certificate` and `pkey` attribute names'
end
end
def validate(record)
unless certificate = read_certificate(record)
record.errors.add(options[:certificate], _('is not a valid X509 certificate.'))
end
unless private_key = read_private_key(record)
record.errors.add(options[:pkey], _('could not read private key, is the passphrase correct?'))
end
return if private_key.nil? || certificate.nil?
unless certificate.public_key.fingerprint == private_key.public_key.fingerprint
record.errors.add(options[:pkey], _('private key does not match certificate.'))
end
end
private
def read_private_key(record)
OpenSSL::PKey.read(pkey(record).to_s, pass(record).to_s)
rescue OpenSSL::PKey::PKeyError, ArgumentError
# When the primary key could not be read, an ArgumentError is raised.
# This hapens when the passed key is not valid or the passphrase is incorrect
nil
end
def read_certificate(record)
OpenSSL::X509::Certificate.new(certificate(record).to_s)
rescue OpenSSL::X509::CertificateError
nil
end
# rubocop:disable GitlabSecurity/PublicSend
#
# Allowing `#public_send` here because we don't want the validator to really
# care about the names of the attributes or where they come from.
#
# The credentials are mostly stored encrypted so we need to go through the
# accessors to get the values, `read_attribute` bypasses those.
def certificate(record)
record.public_send(options[:certificate])
end
def pkey(record)
record.public_send(options[:pkey])
end
def pass(record)
return unless options[:pass]
record.public_send(options[:pass])
end
# rubocop:enable GitlabSecurity/PublicSend
end
%section.settings.as-external-auth.no-animate#js-external-auth-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('External authentication')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('External Classification Policy Authorization')
.settings-content
= form_for @application_setting, url: admin_application_settings_path(anchor: 'js-external-auth-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
.form-group
.form-check
= f.check_box :external_authorization_service_enabled, class: 'form-check-input'
= f.label :external_authorization_service_enabled, class: 'form-check-label' do
= _('Enable classification control using an external service')
%span.form-text.text-muted
= external_authorization_description
= link_to icon('question-circle'), help_page_path('user/admin_area/settings/external_authorization')
.form-group
= f.label :external_authorization_service_url, _('Service URL'), class: 'label-bold'
= f.text_field :external_authorization_service_url, class: 'form-control'
%span.form-text.text-muted
= external_authorization_url_help_text
.form-group
= f.label :external_authorization_service_timeout, _('External authorization request timeout'), class: 'label-bold'
= f.number_field :external_authorization_service_timeout, class: 'form-control', min: 0.001, max: 10, step: 0.001
%span.form-text.text-muted
= external_authorization_timeout_help_text
= f.label :external_auth_client_cert, _('Client authentication certificate'), class: 'label-bold'
= f.text_area :external_auth_client_cert, class: 'form-control'
%span.form-text.text-muted
= external_authorization_client_certificate_help_text
.form-group
= f.label :external_auth_client_key, _('Client authentication key'), class: 'label-bold'
= f.text_area :external_auth_client_key, class: 'form-control'
%span.form-text.text-muted
= external_authorization_client_key_help_text
.form-group
= f.label :external_auth_client_key_pass, _('Client authentication key password'), class: 'label-bold'
= f.password_field :external_auth_client_key_pass, class: 'form-control'
%span.form-text.text-muted
= external_authorization_client_pass_help_text
.form-group
= f.label :external_authorization_service_default_label, _('Default classification label'), class: 'label-bold'
= f.text_field :external_authorization_service_default_label, class: 'form-control'
= f.submit 'Save changes', class: "btn btn-success"
......@@ -68,7 +68,7 @@
.settings-content
= render 'terms'
= render_if_exists 'admin/application_settings/external_authorization_service_form', expanded: expanded_by_default?
= render 'admin/application_settings/external_authorization_service_form', expanded: expanded_by_default?
%section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
......
......@@ -24,8 +24,9 @@
Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
%br
Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}.
%p= link_to 'Check the current instance configuration ', help_instance_configuration_url
%hr
%p= link_to 'Check the current instance configuration ', help_instance_configuration_url
%hr
.row.prepend-top-default
.col-md-8
......
......@@ -7,6 +7,7 @@
.alert-wrapper
= render "layouts/broadcast"
= render "layouts/header/read_only_banner"
= render "layouts/nav/classification_level_banner"
= yield :flash_message
= render "shared/ping_consent"
- unless @hide_breadcrumbs
......
- if ::Gitlab::ExternalAuthorization.enabled? && @project
= content_for :header_content do
%span.badge.color-label.classification-label.has-tooltip{ title: s_('ExternalAuthorizationService|Classification label') }
= sprite_icon('lock-open', size: 8, css_class: 'inline')
= @project.external_authorization_classification_label
%p
Assignee changed
- if previous_assignees.any?
from
%strong= sanitize_name(previous_assignees.map(&:name).to_sentence)
to
- if issuable.assignees.any?
%strong= sanitize_name(issuable.assignee_list)
- else
%strong Unassigned
- if @domain.remove_at
%p
Unless you verify your domain by
%strong= @domain.remove_at.strftime('%F %T,')
it will be removed from your GitLab project.
- else
%p
If you no longer wish to use this domain with GitLab Pages, please remove it
from your GitLab project and delete any related DNS records.
......@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
Assignee: #{sanitize_name(@merge_request.assignee_name)}
= assignees_label(@merge_request)
......@@ -3,7 +3,7 @@
- if @issue.assignees.any?
%p
Assignee: #{@issue.assignee_list}
= assignees_label(@issue)
%p
This issue is due on: #{@issue.due_date.to_s(:medium)}
......
......@@ -2,6 +2,6 @@ The following issue is due on <%= @issue.due_date %>:
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= @issue.author_name %>
Assignee: <%= @issue.assignee_list %>
<%= assignees_label(@issue) %>
<%= @issue.description %>
......@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
Assignee: #{sanitize_name(@merge_request.assignee_name)}
= assignees_label(@merge_request)
......@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
Assignee: #{sanitize_name(@merge_request.assignee_name)}
= assignees_label(@merge_request)
......@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
Assignee: #{sanitize_name(@merge_request.assignee_name)}
= assignees_label(@merge_request)
......@@ -4,7 +4,7 @@
- if @issue.assignees.any?
%p
Assignee: #{@issue.assignee_list}
= assignees_label(@issue)
- if @issue.description
%div
......
......@@ -2,6 +2,6 @@ New Issue was created.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= sanitize_name(@issue.author_name) %>
Assignee: <%= @issue.assignee_list %>
<%= assignees_label(@issue) %>
<%= @issue.description %>
......@@ -2,6 +2,6 @@ You have been mentioned in an issue.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= sanitize_name(@issue.author_name) %>
Assignee: <%= sanitize_name(@issue.assignee_list) %>
<%= assignees_label(@issue) %>
<%= @issue.description %>
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
File mode changed from 100755 to 100644
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment