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: ...@@ -4,7 +4,12 @@ include:
- local: /lib/gitlab/ci/templates/Code-Quality.gitlab-ci.yml - local: /lib/gitlab/ci/templates/Code-Quality.gitlab-ci.yml
.dedicated-runner: &dedicated-runner .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: tags:
- gitlab-org - gitlab-org
...@@ -557,7 +562,7 @@ rspec-mysql: ...@@ -557,7 +562,7 @@ rspec-mysql:
parallel: 50 parallel: 50
.rspec-quarantine: &rspec-quarantine .rspec-quarantine: &rspec-quarantine
retry: 0 <<: *only-schedules-master
script: script:
- export CACHE_CLASSES=true - export CACHE_CLASSES=true
- scripts/gitaly-test-spawn - scripts/gitaly-test-spawn
...@@ -588,7 +593,7 @@ static-analysis: ...@@ -588,7 +593,7 @@ static-analysis:
- tmp/rubocop_cache - tmp/rubocop_cache
# Documentation checks: # 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 # - Make sure cURL examples in API docs use the full switches
docs lint: docs lint:
<<: *dedicated-runner <<: *dedicated-runner
...@@ -607,6 +612,8 @@ docs lint: ...@@ -607,6 +612,8 @@ docs lint:
- bundle exec nanoc - bundle exec nanoc
# Check the internal links # Check the internal links
- bundle exec nanoc check internal_links - bundle exec nanoc check internal_links
# Check the internal anchor links
- bundle exec nanoc check internal_anchors
downtime_check: downtime_check:
<<: *rake-exec <<: *rake-exec
...@@ -1168,4 +1175,3 @@ schedule:review-performance: ...@@ -1168,4 +1175,3 @@ schedule:review-performance:
<<: *review-schedules-only <<: *review-schedules-only
script: script:
- wait_for_job_to_be_done "schedule:review-deploy" - wait_for_job_to_be_done "schedule:review-deploy"
...@@ -50,7 +50,7 @@ export default { ...@@ -50,7 +50,7 @@ export default {
}, },
modalText() { 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 commitId = _.escape(this.commitShortSha);
const linkEnd = '</a>'; const linkEnd = '</a>';
const name = _.escape(this.name); const name = _.escape(this.name);
......
...@@ -504,22 +504,28 @@ export default { ...@@ -504,22 +504,28 @@ export default {
class="table-section section-10 deployment-column d-none d-sm-none d-md-block" class="table-section section-10 deployment-column d-none d-sm-none d-md-block"
role="gridcell" 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 by
<user-avatar-link <user-avatar-link
:link-href="deploymentUser.web_url" :link-href="deploymentUser.web_url"
:img-src="deploymentUser.avatar_url" :img-src="deploymentUser.avatar_url"
:img-alt="userImageAltDescription" :img-alt="userImageAltDescription"
:tooltip-text="deploymentUser.username" :tooltip-text="deploymentUser.username"
class="js-deploy-user-container" class="js-deploy-user-container float-none"
/> />
</span> </span>
</div> </div>
<div class="table-section section-15 d-none d-sm-none d-md-block" role="gridcell"> <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> <span class="flex-truncate-child">{{ buildName }}</span>
</a> </a>
</div> </div>
......
...@@ -72,10 +72,9 @@ export default { ...@@ -72,10 +72,9 @@ export default {
<gl-button <gl-button
v-gl-tooltip v-gl-tooltip
v-gl-modal.confirm-rollback-modal v-gl-modal.confirm-rollback-modal
variant="secondary"
:disabled="isLoading" :disabled="isLoading"
:title="title" :title="title"
class="d-none d-md-block" class="d-none d-md-block text-secondary"
@click="onClick" @click="onClick"
> >
<icon v-if="isLastDeployment" name="repeat" /> <icon v-else name="redo" /> <icon v-if="isLastDeployment" name="repeat" /> <icon v-else name="redo" />
......
...@@ -39,7 +39,7 @@ export default { ...@@ -39,7 +39,7 @@ export default {
:aria-label="title" :aria-label="title"
:href="terminalPath" :href="terminalPath"
:class="{ disabled: disabled }" :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" /> <icon name="terminal" />
</a> </a>
......
...@@ -81,9 +81,6 @@ export default { ...@@ -81,9 +81,6 @@ export default {
const formData = { const formData = {
update: { update: {
state_event: this.form.find('input[name="update[state_event]"]').val(), 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()], assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()],
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(), milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(), issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
......
...@@ -18,5 +18,3 @@ export const timeWindows = { ...@@ -18,5 +18,3 @@ export const timeWindows = {
threeDays: __('3 days'), threeDays: __('3 days'),
oneWeek: __('1 week'), 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 * method that converts a predetermined time window to minutes
...@@ -6,27 +6,26 @@ import { timeWindows, msPerMinute } from './constants'; ...@@ -6,27 +6,26 @@ import { timeWindows, msPerMinute } from './constants';
* @param {String} timeWindow - The time window to convert to minutes * @param {String} timeWindow - The time window to convert to minutes
* @returns {number} The time window in minutes * @returns {number} The time window in minutes
*/ */
const getTimeDifferenceMinutes = timeWindow => { const getTimeDifferenceSeconds = timeWindow => {
switch (timeWindow) { switch (timeWindow) {
case timeWindows.thirtyMinutes: case timeWindows.thirtyMinutes:
return 30; return 60 * 30;
case timeWindows.threeHours: case timeWindows.threeHours:
return 60 * 3; return 60 * 60 * 3;
case timeWindows.oneDay: case timeWindows.oneDay:
return 60 * 24 * 1; return 60 * 60 * 24 * 1;
case timeWindows.threeDays: case timeWindows.threeDays:
return 60 * 24 * 3; return 60 * 60 * 24 * 3;
case timeWindows.oneWeek: case timeWindows.oneWeek:
return 60 * 24 * 7 * 1; return 60 * 60 * 24 * 7 * 1;
default: default:
return 60 * 8; return 60 * 60 * 8;
} }
}; };
export const getTimeDiff = selectedTimeWindow => { export const getTimeDiff = selectedTimeWindow => {
const end = Date.now(); const end = Date.now() / 1000; // convert milliseconds to seconds
const timeDifferenceMinutes = getTimeDifferenceMinutes(selectedTimeWindow); const start = end - getTimeDifferenceSeconds(selectedTimeWindow);
const start = new Date(end - timeDifferenceMinutes * msPerMinute).getTime();
return { start, end }; return { start, end };
}; };
......
...@@ -115,8 +115,11 @@ export default { ...@@ -115,8 +115,11 @@ export default {
author() { author() {
return this.getUserData; return this.getUserData;
}, },
canUpdateIssue() { canToggleIssueState() {
return this.getNoteableData.current_user.can_update; return (
this.getNoteableData.current_user.can_update &&
this.getNoteableData.state !== constants.MERGED
);
}, },
endpoint() { endpoint() {
return this.getNoteableData.create_note_path; return this.getNoteableData.create_note_path;
...@@ -415,7 +418,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown" ...@@ -415,7 +418,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
</div> </div>
<loading-button <loading-button
v-if="canUpdateIssue" v-if="canToggleIssueState"
:loading="isToggleStateButtonLoading" :loading="isToggleStateButtonLoading"
:container-class="[ :container-class="[
actionButtonClassNames, actionButtonClassNames,
......
...@@ -4,6 +4,7 @@ import { mapGetters, mapActions } from 'vuex'; ...@@ -4,6 +4,7 @@ import { mapGetters, mapActions } from 'vuex';
import { escape } from 'underscore'; import { escape } from 'underscore';
import { truncateSha } from '~/lib/utils/text_utility'; import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; 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 { s__, sprintf } from '../../locale';
import Flash from '../../flash'; import Flash from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
...@@ -23,7 +24,7 @@ export default { ...@@ -23,7 +24,7 @@ export default {
noteBody, noteBody,
TimelineEntryItem, TimelineEntryItem,
}, },
mixins: [noteable, resolvable], mixins: [noteable, resolvable, draftMixin],
props: { props: {
note: { note: {
type: Object, type: Object,
...@@ -73,9 +74,6 @@ export default { ...@@ -73,9 +74,6 @@ export default {
'is-editable': this.note.current_user.can_edit, 'is-editable': this.note.current_user.can_edit,
}; };
}, },
canResolve() {
return this.note.resolvable && !!this.getUserData.id;
},
canReportAsAbuse() { canReportAsAbuse() {
return !!this.note.report_abuse_path && this.author.id !== this.getUserData.id; return !!this.note.report_abuse_path && this.author.id !== this.getUserData.id;
}, },
...@@ -156,12 +154,16 @@ export default { ...@@ -156,12 +154,16 @@ export default {
this.$refs.noteBody.resetAutoSave(); this.$refs.noteBody.resetAutoSave();
this.$emit('updateSuccess'); this.$emit('updateSuccess');
}, },
formUpdateHandler(noteText, parentElement, callback) { formUpdateHandler(noteText, parentElement, callback, resolveDiscussion) {
this.$emit('handleUpdateNote', { this.$emit('handleUpdateNote', {
note: this.note, note: this.note,
noteText, noteText,
resolveDiscussion,
callback: () => this.updateSuccess(), callback: () => this.updateSuccess(),
}); });
if (this.isDraft) return;
const data = { const data = {
endpoint: this.note.path, endpoint: this.note.path,
note: { note: {
...@@ -234,6 +236,7 @@ export default { ...@@ -234,6 +236,7 @@ export default {
<div class="timeline-content"> <div class="timeline-content">
<div class="note-header"> <div class="note-header">
<note-header v-once :author="author" :created-at="note.created_at" :note-id="note.id"> <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-if="commit" v-html="actionText"></span>
<span v-else class="d-none d-sm-inline">&middot;</span> <span v-else class="d-none d-sm-inline">&middot;</span>
</note-header> </note-header>
...@@ -247,12 +250,15 @@ export default { ...@@ -247,12 +250,15 @@ export default {
:can-award-emoji="note.current_user.can_award_emoji" :can-award-emoji="note.current_user.can_award_emoji"
:can-delete="note.current_user.can_edit" :can-delete="note.current_user.can_edit"
:can-report-as-abuse="canReportAsAbuse" :can-report-as-abuse="canReportAsAbuse"
:can-resolve="note.current_user.can_resolve" :can-resolve="canResolve"
:report-abuse-path="note.report_abuse_path" :report-abuse-path="note.report_abuse_path"
:resolvable="note.resolvable" :resolvable="note.resolvable || note.isDraft"
:is-resolved="note.resolved" :is-resolved="note.resolved || note.resolve_discussion"
:is-resolving="isResolving" :is-resolving="isResolving"
:resolved-by="note.resolved_by" :resolved-by="note.resolved_by"
:is-draft="note.isDraft"
:resolve-discussion="note.isDraft && note.resolve_discussion"
:discussion-id="discussionId"
@handleEdit="editHandler" @handleEdit="editHandler"
@handleDelete="deleteHandler" @handleDelete="deleteHandler"
@handleResolve="resolveHandler" @handleResolve="resolveHandler"
......
...@@ -7,6 +7,7 @@ export const COMMENT = 'comment'; ...@@ -7,6 +7,7 @@ export const COMMENT = 'comment';
export const OPENED = 'opened'; export const OPENED = 'opened';
export const REOPENED = 'reopened'; export const REOPENED = 'reopened';
export const CLOSED = 'closed'; export const CLOSED = 'closed';
export const MERGED = 'merged';
export const EMOJI_THUMBSUP = 'thumbsup'; export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown'; export const EMOJI_THUMBSDOWN = 'thumbsdown';
export const ISSUE_NOTEABLE_TYPE = 'issue'; 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 { ...@@ -84,10 +84,7 @@ export default {
</div> </div>
</div> </div>
<div> <div>
<div <div v-if="isFetchingMergeRequests" class="qa-related-merge-requests-loading-icon">
v-if="isFetchingMergeRequests"
class="related-related-merge-requests-icon qa-related-merge-requests-loading-icon"
>
<gl-loading-icon label="Fetching related merge requests" class="py-2" /> <gl-loading-icon label="Fetching related merge requests" class="py-2" />
</div> </div>
<ul v-else class="content-list related-items-list"> <ul v-else class="content-list related-items-list">
......
...@@ -74,8 +74,7 @@ export default { ...@@ -74,8 +74,7 @@ export default {
} }
if (!this.users.length) { if (!this.users.length) {
const emptyTooltipLabel = const emptyTooltipLabel = __('Assignee(s)');
this.issuableType === 'issue' ? __('Assignee(s)') : __('Assignee');
names.push(emptyTooltipLabel); names.push(emptyTooltipLabel);
} }
...@@ -90,6 +89,27 @@ export default { ...@@ -90,6 +89,27 @@ export default {
return counter; 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: { methods: {
assignSelf() { assignSelf() {
...@@ -154,6 +174,15 @@ export default { ...@@ -154,6 +174,15 @@ export default {
</button> </button>
</div> </div>
<div class="value hide-collapsed"> <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"> <template v-if="hasNoUsers">
<span class="assign-yourself no-value"> <span class="assign-yourself no-value">
No assignee No assignee
......
...@@ -162,7 +162,7 @@ export default { ...@@ -162,7 +162,7 @@ export default {
</template> </template>
<icon name="commit" class="commit-icon js-commit-icon" /> <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"> <div class="commit-title flex-truncate-parent">
<span v-if="title" class="flex-truncate-child"> <span v-if="title" class="flex-truncate-child">
......
...@@ -25,6 +25,18 @@ $item-weight-max-width: 48px; ...@@ -25,6 +25,18 @@ $item-weight-max-width: 48px;
flex-grow: 1; 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-open,
.issue-token-state-icon-closed, .issue-token-state-icon-closed,
.confidential-icon, .confidential-icon,
......
...@@ -12,34 +12,6 @@ ...@@ -12,34 +12,6 @@
.environments-container { .environments-container {
.ci-table { .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 { .commit-title {
margin: 0; margin: 0;
} }
...@@ -49,47 +21,16 @@ ...@@ -49,47 +21,16 @@
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
} }
.dropdown-menu {
.fa {
margin-right: 6px;
color: $gl-text-color-secondary;
}
}
.build-link, .build-link,
.ref-name { .ref-name {
color: $gl-text-color; 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 { .folder-icon {
margin-right: 3px; margin-right: 3px;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
display: inline-block; display: inline-block;
vertical-align: text-top; vertical-align: text-top;
.fa:nth-child(1) {
margin-right: 3px;
}
} }
.folder-name { .folder-name {
...@@ -103,12 +44,6 @@ ...@@ -103,12 +44,6 @@
text-align: center; text-align: center;
} }
.branch-commit {
.commit-sha {
margin-right: 0;
}
}
.no-btn { .no-btn {
border: 0; border: 0;
background: none; background: none;
...@@ -168,11 +103,6 @@ ...@@ -168,11 +103,6 @@
opacity: 0.25; opacity: 0.25;
} }
.prometheus-graph-overlay {
fill: none;
opacity: 0;
pointer-events: all;
}
.rect-text-metric { .rect-text-metric {
fill: $white-light; fill: $white-light;
...@@ -203,276 +133,10 @@ ...@@ -203,276 +133,10 @@
stroke: $gray-darkest; 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 { .environments-actions {
.external-url, .external-url,
.monitoring-url, .monitoring-url,
.terminal-button, .terminal-button {
.stop-env-link {
width: 38px; 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 @@ ...@@ -498,6 +498,16 @@
flex: 1; flex: 1;
} }
.issuable-meta {
.author-link {
display: inline-block;
}
.issuable-comments {
height: 18px;
}
}
.merge-request-title { .merge-request-title {
margin-bottom: 2px; margin-bottom: 2px;
......
...@@ -67,6 +67,10 @@ ...@@ -67,6 +67,10 @@
} }
} }
.classification-label {
background-color: $red-500;
}
.toggle-wrapper { .toggle-wrapper {
margin-top: 5px; margin-top: 5px;
} }
...@@ -1158,6 +1162,8 @@ pre.light-well { ...@@ -1158,6 +1162,8 @@ pre.light-well {
.cannot-be-merged:hover { .cannot-be-merged:hover {
color: $red-500; color: $red-500;
margin-top: 2px; margin-top: 2px;
position: relative;
z-index: 2;
} }
.private-forks-notice .private-fork-icon { .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 ...@@ -124,7 +124,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end end
def visible_application_setting_attributes def visible_application_setting_attributes
ApplicationSettingsHelper.visible_attributes + [ [
*::ApplicationSettingsHelper.visible_attributes,
*::ApplicationSettingsHelper.external_authorization_service_attributes,
:domain_blacklist_file, :domain_blacklist_file,
disabled_oauth_sign_in_sources: [], disabled_oauth_sign_in_sources: [],
import_sources: [], import_sources: [],
......
...@@ -192,12 +192,7 @@ module IssuableActions ...@@ -192,12 +192,7 @@ module IssuableActions
def bulk_update_params def bulk_update_params
permitted_keys_array = permitted_keys.dup permitted_keys_array = permitted_keys.dup
permitted_keys_array << { assignee_ids: [] }
if resource_name == 'issue'
permitted_keys_array << { assignee_ids: [] }
else
permitted_keys_array.unshift(:assignee_id)
end
params.require(:update).permit(permitted_keys_array) params.require(:update).permit(permitted_keys_array)
end end
......
...@@ -190,15 +190,15 @@ module IssuableCollections ...@@ -190,15 +190,15 @@ module IssuableCollections
end end
end end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def preload_for_collection def preload_for_collection
common_attributes = [:author, :assignees, :labels, :milestone]
@preload_for_collection ||= case collection_type @preload_for_collection ||= case collection_type
when 'Issue' when 'Issue'
[:project, :author, :assignees, :labels, :milestone, project: :namespace] common_attributes + [:project, project: :namespace]
when 'MergeRequest' when 'MergeRequest'
[ common_attributes + [:target_project, source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits]
:target_project, :author, :assignee, :labels, :milestone,
source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
]
end end
end end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end end
# frozen_string_literal: true # frozen_string_literal: true
module ProjectUnauthorized module ProjectUnauthorized
extend ActiveSupport::Concern
# EE would override this
def project_unauthorized_proc 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
end end
...@@ -14,8 +14,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -14,8 +14,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37434 # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/40260
# Also https://gitlab.com/gitlab-org/gitlab-ce/issues/40260
Gitlab::GitalyClient.allow_n_plus_1_calls do Gitlab::GitalyClient.allow_n_plus_1_calls do
render render
end end
......
...@@ -22,7 +22,7 @@ class HelpController < ApplicationController ...@@ -22,7 +22,7 @@ class HelpController < ApplicationController
end end
def show def show
@path = clean_path_info(path_params[:path]) @path = Rack::Utils.clean_path_info(path_params[:path])
respond_to do |format| respond_to do |format|
format.any(:markdown, :md, :html) do format.any(:markdown, :md, :html) do
...@@ -75,35 +75,4 @@ class HelpController < ApplicationController ...@@ -75,35 +75,4 @@ class HelpController < ApplicationController
params params
end 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 end
...@@ -25,7 +25,7 @@ class Projects::BranchesController < Projects::ApplicationController ...@@ -25,7 +25,7 @@ class Projects::BranchesController < Projects::ApplicationController
@refs_pipelines = @project.ci_pipelines.latest_successful_for_refs(@branches.map(&:name)) @refs_pipelines = @project.ci_pipelines.latest_successful_for_refs(@branches.map(&:name))
@merged_branch_names = repository.merged_branch_names(@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 Gitlab::GitalyClient.allow_n_plus_1_calls do
@max_commits = @branches.reduce(0) do |memo, branch| @max_commits = @branches.reduce(0) do |memo, branch|
diverging_commit_counts = repository.diverging_commit_counts(branch) diverging_commit_counts = repository.diverging_commit_counts(branch)
......
...@@ -193,7 +193,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -193,7 +193,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
return unless Feature.enabled?(:metrics_time_window, project) return unless Feature.enabled?(:metrics_time_window, project)
return unless params[:start].present? || params[:end].present? return unless params[:start].present? || params[:end].present?
params.require([:start, :end]).values_at(:start, :end) params.require([:start, :end])
end end
def search_environment_names def search_environment_names
......
...@@ -20,7 +20,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont ...@@ -20,7 +20,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
def merge_request_params_attributes def merge_request_params_attributes
[ [
:allow_collaboration, :allow_collaboration,
:assignee_id,
:description, :description,
:force_remove_source_branch, :force_remove_source_branch,
:lock_version, :lock_version,
...@@ -35,6 +34,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont ...@@ -35,6 +34,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:title, :title,
:discussion_locked, :discussion_locked,
label_ids: [], label_ids: [],
assignee_ids: [],
update_task: [:index, :checked, :line_number, :line_source] update_task: [:index, :checked, :line_number, :line_source]
] ]
end end
......
...@@ -345,6 +345,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -345,6 +345,7 @@ class ProjectsController < Projects::ApplicationController
:container_registry_enabled, :container_registry_enabled,
:default_branch, :default_branch,
:description, :description,
:external_authorization_classification_label,
:import_url, :import_url,
:issues_tracker, :issues_tracker,
:issues_tracker_id, :issues_tracker_id,
......
...@@ -15,7 +15,7 @@ class RootController < Dashboard::ProjectsController ...@@ -15,7 +15,7 @@ class RootController < Dashboard::ProjectsController
before_action :redirect_logged_user, if: -> { current_user.present? } before_action :redirect_logged_user, if: -> { current_user.present? }
def index 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 Gitlab::GitalyClient.allow_n_plus_1_calls do
super super
end end
......
...@@ -439,22 +439,6 @@ class IssuableFinder ...@@ -439,22 +439,6 @@ class IssuableFinder
end end
# rubocop: enable CodeReuse/ActiveRecord # 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? def filter_by_no_assignee?
# Assignee_id takes precedence over assignee_username # Assignee_id takes precedence over assignee_username
[NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE [NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE
...@@ -478,6 +462,20 @@ class IssuableFinder ...@@ -478,6 +462,20 @@ class IssuableFinder
end end
# rubocop: enable CodeReuse/ActiveRecord # 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 # rubocop: disable CodeReuse/ActiveRecord
def by_milestone(items) def by_milestone(items)
if milestones? if milestones?
......
...@@ -144,18 +144,4 @@ class IssuesFinder < IssuableFinder ...@@ -144,18 +144,4 @@ class IssuesFinder < IssuableFinder
current_user.blank? current_user.blank?
end 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 end
...@@ -81,7 +81,7 @@ class ProjectsFinder < UnionFinder ...@@ -81,7 +81,7 @@ class ProjectsFinder < UnionFinder
if private_only? if private_only?
current_user.authorized_projects current_user.authorized_projects
else else
Project.public_or_visible_to_user(current_user, params[:visibility_level]) Project.public_or_visible_to_user(current_user)
end end
end end
end end
......
...@@ -119,6 +119,39 @@ module ApplicationSettingsHelper ...@@ -119,6 +119,39 @@ module ApplicationSettingsHelper
options_for_select(options, selected) options_for_select(options, selected)
end 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 def visible_attributes
[ [
:admin_notification_email, :admin_notification_email,
...@@ -238,6 +271,18 @@ module ApplicationSettingsHelper ...@@ -238,6 +271,18 @@ module ApplicationSettingsHelper
] ]
end 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? def expanded_by_default?
Rails.env.test? Rails.env.test?
end end
......
...@@ -69,7 +69,7 @@ module BoardsHelper ...@@ -69,7 +69,7 @@ module BoardsHelper
end end
def board_sidebar_user_data def board_sidebar_user_data
dropdown_options = issue_assignees_dropdown_options dropdown_options = assignees_dropdown_options('issue')
{ {
toggle: 'dropdown', toggle: 'dropdown',
......
...@@ -17,8 +17,8 @@ module FormHelper ...@@ -17,8 +17,8 @@ module FormHelper
end end
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', toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data',
title: 'Select assignee', title: 'Select assignee',
filter: true, filter: true,
...@@ -28,8 +28,8 @@ module FormHelper ...@@ -28,8 +28,8 @@ module FormHelper
first_user: current_user&.username, first_user: current_user&.username,
null_user: true, null_user: true,
current_user: true, current_user: true,
project_id: @project&.id, project_id: (@target_project || @project)&.id,
field_name: 'issue[assignee_ids][]', field_name: "#{issuable_type}[assignee_ids][]",
default_label: 'Unassigned', default_label: 'Unassigned',
'max-select': 1, 'max-select': 1,
'dropdown-header': 'Assignee', 'dropdown-header': 'Assignee',
...@@ -39,5 +39,36 @@ module FormHelper ...@@ -39,5 +39,36 @@ module FormHelper
current_user_info: UserSerializer.new.represent(current_user) 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
end end
...@@ -15,11 +15,14 @@ module IssuablesHelper ...@@ -15,11 +15,14 @@ module IssuablesHelper
sidebar_gutter_collapsed? ? _('Expand sidebar') : _('Collapse sidebar') sidebar_gutter_collapsed? ? _('Expand sidebar') : _('Collapse sidebar')
end end
def sidebar_assignee_tooltip_label(issuable) def assignees_label(issuable, include_value: true)
if issuable.assignee label = 'Assignee'.pluralize(issuable.assignees.count)
issuable.assignee.name
if include_value
sanitized_list = sanitize_name(issuable.assignee_list)
"#{label}: #{sanitized_list}"
else else
issuable.allows_multiple_assignees? ? _('Assignee(s)') : _('Assignee') label
end end
end end
......
...@@ -303,6 +303,16 @@ module ProjectsHelper ...@@ -303,6 +303,16 @@ module ProjectsHelper
@path.present? @path.present?
end 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 private
def get_project_nav_tabs(project, current_user) def get_project_nav_tabs(project, current_user)
......
...@@ -24,10 +24,12 @@ module Emails ...@@ -24,10 +24,12 @@ module Emails
end end
# rubocop: disable CodeReuse/ActiveRecord # 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) 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)) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
......
...@@ -4,6 +4,7 @@ class Notify < BaseMailer ...@@ -4,6 +4,7 @@ class Notify < BaseMailer
include ActionDispatch::Routing::PolymorphicRoutes include ActionDispatch::Routing::PolymorphicRoutes
include GitlabRoutingHelper include GitlabRoutingHelper
include EmailsHelper include EmailsHelper
include IssuablesHelper
include Emails::Issues include Emails::Issues
include Emails::MergeRequests include Emails::MergeRequests
...@@ -24,6 +25,7 @@ class Notify < BaseMailer ...@@ -24,6 +25,7 @@ class Notify < BaseMailer
helper MembersHelper helper MembersHelper
helper AvatarsHelper helper AvatarsHelper
helper GitlabRoutingHelper helper GitlabRoutingHelper
helper IssuablesHelper
def test_email(recipient_email, subject, body) def test_email(recipient_email, subject, body)
mail(to: recipient_email, mail(to: recipient_email,
......
...@@ -213,6 +213,40 @@ class ApplicationSetting < ApplicationRecord ...@@ -213,6 +213,40 @@ class ApplicationSetting < ApplicationRecord
validate :terms_exist, if: :enforce_terms? 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 :ensure_uuid!
before_validation :strip_sentry_values before_validation :strip_sentry_values
......
...@@ -110,7 +110,7 @@ class Blob < SimpleDelegator ...@@ -110,7 +110,7 @@ class Blob < SimpleDelegator
end end
def load_all_data! 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 Gitlab::GitalyClient.allow_n_plus_1_calls do
super(project.repository) if project super(project.repository) if project
end end
......
...@@ -750,6 +750,10 @@ module Ci ...@@ -750,6 +750,10 @@ module Ci
self.sha == sha || self.source_sha == sha self.sha == sha || self.source_sha == sha
end end
def triggered_by?(current_user)
user == current_user
end
private private
def ci_yaml_from_repo 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 ...@@ -67,13 +67,6 @@ module Issuable
allow_nil: true, allow_nil: true,
prefix: true prefix: true
delegate :name,
:email,
:public_email,
to: :assignee,
allow_nil: true,
prefix: true
validates :author, presence: true validates :author, presence: true
validates :title, presence: true, length: { maximum: 255 } validates :title, presence: true, length: { maximum: 255 }
validate :milestone_is_valid validate :milestone_is_valid
...@@ -88,6 +81,19 @@ module Issuable ...@@ -88,6 +81,19 @@ module Issuable
scope :only_opened, -> { with_state(:opened) } scope :only_opened, -> { with_state(:opened) }
scope :closed, -> { with_state(:closed) } 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 :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_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') } 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 ...@@ -104,6 +110,7 @@ module Issuable
participant :author participant :author
participant :notes_with_associations participant :notes_with_associations
participant :assignees
strip_attributes :title strip_attributes :title
...@@ -270,6 +277,10 @@ module Issuable ...@@ -270,6 +277,10 @@ module Issuable
end end
end end
def assignee_or_author?(user)
author_id == user.id || assignees.exists?(user.id)
end
def today? def today?
Date.today == created_at.to_date Date.today == created_at.to_date
end end
...@@ -314,11 +325,7 @@ module Issuable ...@@ -314,11 +325,7 @@ module Issuable
end end
if old_assignees != assignees if old_assignees != assignees
if self.is_a?(Issue) changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
else
changes[:assignee] = [old_assignees&.first&.hook_attrs, assignee&.hook_attrs]
end
end end
if self.respond_to?(:total_time_spent) if self.respond_to?(:total_time_spent)
...@@ -355,10 +362,18 @@ module Issuable ...@@ -355,10 +362,18 @@ module Issuable
def card_attributes def card_attributes
{ {
'Author' => author.try(:name), 'Author' => author.try(:name),
'Assignee' => assignee.try(:name) 'Assignee' => assignee_list
} }
end end
def assignee_list
assignees.map(&:name).to_sentence
end
def assignee_username_list
assignees.map(&:username).to_sentence
end
def notes_with_associations def notes_with_associations
# If A has_many Bs, and B has_many Cs, and you do # 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 # `A.includes(b: :c).each { |a| a.b.includes(:c) }`, sadly ActiveRecord
......
...@@ -49,10 +49,6 @@ class Issue < ApplicationRecord ...@@ -49,10 +49,6 @@ class Issue < ApplicationRecord
scope :in_projects, ->(project_ids) { where(project_id: project_ids) } 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 :with_due_date, -> { where.not(due_date: nil) }
scope :without_due_date, -> { where(due_date: nil) } scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) } scope :due_before, ->(date) { where('issues.due_date < ?', date) }
...@@ -75,8 +71,6 @@ class Issue < ApplicationRecord ...@@ -75,8 +71,6 @@ class Issue < ApplicationRecord
attr_spammable :title, spam_title: true attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true attr_spammable :description, spam_description: true
participant :assignees
state_machine :state, initial: :opened do state_machine :state, initial: :opened do
event :close do event :close do
transition [:opened] => :closed transition [:opened] => :closed
...@@ -155,22 +149,6 @@ class Issue < ApplicationRecord ...@@ -155,22 +149,6 @@ class Issue < ApplicationRecord
Gitlab::HookData::IssueBuilder.new(self).build Gitlab::HookData::IssueBuilder.new(self).build
end 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. # `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false) def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}" reference = "#{self.class.reference_prefix}#{iid}"
...@@ -230,7 +208,13 @@ class Issue < ApplicationRecord ...@@ -230,7 +208,13 @@ class Issue < ApplicationRecord
def visible_to_user?(user = nil) def visible_to_user?(user = nil)
return false unless project && project.feature_available?(:issues, user) 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 end
def check_for_spam? def check_for_spam?
...@@ -298,7 +282,7 @@ class Issue < ApplicationRecord ...@@ -298,7 +282,7 @@ class Issue < ApplicationRecord
# Returns `true` if this Issue is visible to everybody. # Returns `true` if this Issue is visible to everybody.
def publicly_visible? def publicly_visible?
project.public? && !confidential? project.public? && !confidential? && !::Gitlab::ExternalAuthorization.enabled?
end end
def expire_etag_cache def expire_etag_cache
......
...@@ -16,6 +16,7 @@ class MergeRequest < ApplicationRecord ...@@ -16,6 +16,7 @@ class MergeRequest < ApplicationRecord
include LabelEventable include LabelEventable
include ReactiveCaching include ReactiveCaching
include FromUnion include FromUnion
include DeprecatedAssignee
self.reactive_cache_key = ->(model) { [model.project.id, model.iid] } self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
self.reactive_cache_refresh_interval = 10.minutes self.reactive_cache_refresh_interval = 10.minutes
...@@ -69,8 +70,7 @@ class MergeRequest < ApplicationRecord ...@@ -69,8 +70,7 @@ class MergeRequest < ApplicationRecord
has_many :suggestions, through: :notes has_many :suggestions, through: :notes
has_many :merge_request_assignees has_many :merge_request_assignees
# Will be deprecated at https://gitlab.com/gitlab-org/gitlab-ce/issues/59457 has_many :assignees, class_name: "User", through: :merge_request_assignees
belongs_to :assignee, class_name: "User"
serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
...@@ -79,10 +79,6 @@ class MergeRequest < ApplicationRecord ...@@ -79,10 +79,6 @@ class MergeRequest < ApplicationRecord
after_update :reload_diff_if_branch_changed after_update :reload_diff_if_branch_changed
after_save :ensure_metrics 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 # When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests # It allows us to close or modify broken merge requests
attr_accessor :allow_broken attr_accessor :allow_broken
...@@ -188,19 +184,14 @@ class MergeRequest < ApplicationRecord ...@@ -188,19 +184,14 @@ class MergeRequest < ApplicationRecord
end end
scope :join_project, -> { joins(:target_project) } scope :join_project, -> { joins(:target_project) }
scope :references_project, -> { references(: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, -> { 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], latest_merge_request_diff: [:merge_request_diff_commits],
metrics: [:latest_closed_by, :merged_by], metrics: [:latest_closed_by, :merged_by],
target_project: [:route, { namespace: :route }], target_project: [:route, { namespace: :route }],
source_project: [:route, { namespace: :route }]) source_project: [:route, { namespace: :route }])
} }
participant :assignee
after_save :keep_around_commit after_save :keep_around_commit
alias_attribute :project, :target_project alias_attribute :project, :target_project
...@@ -337,31 +328,6 @@ class MergeRequest < ApplicationRecord ...@@ -337,31 +328,6 @@ class MergeRequest < ApplicationRecord
Gitlab::HookData::MergeRequestBuilder.new(self).build Gitlab::HookData::MergeRequestBuilder.new(self).build
end 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. # `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false) def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}" reference = "#{self.class.reference_prefix}#{iid}"
...@@ -682,15 +648,6 @@ class MergeRequest < ApplicationRecord ...@@ -682,15 +648,6 @@ class MergeRequest < ApplicationRecord
merge_request_diff || create_merge_request_diff merge_request_diff || create_merge_request_diff
end 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 def create_merge_request_diff
fetch_ref! fetch_ref!
...@@ -1208,7 +1165,7 @@ class MergeRequest < ApplicationRecord ...@@ -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_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_TARGET_BRANCH_NAME', value: target_branch.to_s)
variables.append(key: 'CI_MERGE_REQUEST_TITLE', value: title) 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_MILESTONE', value: milestone.title) if milestone
variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present? variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present?
variables.concat(source_project_variables) variables.concat(source_project_variables)
......
...@@ -459,41 +459,14 @@ class Project < ApplicationRecord ...@@ -459,41 +459,14 @@ class Project < ApplicationRecord
# Returns a collection of projects that is either public or visible to the # Returns a collection of projects that is either public or visible to the
# logged in user. # logged in user.
# def self.public_or_visible_to_user(user = nil)
# requested_visiblity_levels: Normally all projects that are visible if user
# to the user (e.g. internal and public) are queried, but this where('EXISTS (?) OR projects.visibility_level IN (?)',
# parameter allows the caller to narrow the search space to optimize user.authorizations_for_projects,
# database queries. For instance, a caller may only want to see Gitlab::VisibilityLevel.levels_for_user(user))
# internal projects. Instead of querying for internal and public else
# projects and throwing away public projects, this parameter allows public_to_user
# the query to be targeted for only internal projects. end
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)
end end
# project features may be "disabled", "internal", "enabled" or "public". If "internal", # project features may be "disabled", "internal", "enabled" or "public". If "internal",
...@@ -674,6 +647,10 @@ class Project < ApplicationRecord ...@@ -674,6 +647,10 @@ class Project < ApplicationRecord
{ scope: :project, status: auto_devops&.enabled || Feature.enabled?(:force_autodevops_on_by_default, self) } { scope: :project, status: auto_devops&.enabled || Feature.enabled?(:force_autodevops_on_by_default, self) }
end end
def multiple_mr_assignees_enabled?
Feature.enabled?(:multiple_merge_request_assignees, self)
end
def daily_statistics_enabled? def daily_statistics_enabled?
Feature.enabled?(:project_daily_statistics, self, default_enabled: true) Feature.enabled?(:project_daily_statistics, self, default_enabled: true)
end end
...@@ -2062,6 +2039,11 @@ class Project < ApplicationRecord ...@@ -2062,6 +2039,11 @@ class Project < ApplicationRecord
fetch_branch_allows_collaboration(user, branch_name) fetch_branch_allows_collaboration(user, branch_name)
end end
def external_authorization_classification_label
super || ::Gitlab::CurrentSettings.current_application_settings
.external_authorization_service_default_label
end
def licensed_features def licensed_features
[] []
end end
......
...@@ -22,6 +22,13 @@ class BasePolicy < DeclarativePolicy::Base ...@@ -22,6 +22,13 @@ class BasePolicy < DeclarativePolicy::Base
Gitlab::CurrentSettings.current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) Gitlab::CurrentSettings.current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC)
end 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 rule { default }.enable :read_cross_project
end end
...@@ -14,6 +14,10 @@ module Ci ...@@ -14,6 +14,10 @@ module Ci
@subject.external? @subject.external?
end end
condition(:triggerer_of_pipeline) do
@subject.triggered_by?(@user)
end
# Disallow users without permissions from accessing internal pipelines # Disallow users without permissions from accessing internal pipelines
rule { ~can?(:read_build) & ~external_pipeline }.policy do rule { ~can?(:read_build) & ~external_pipeline }.policy do
prevent :read_pipeline prevent :read_pipeline
...@@ -29,6 +33,14 @@ module Ci ...@@ -29,6 +33,14 @@ module Ci
enable :destroy_pipeline enable :destroy_pipeline
end 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) def ref_protected?(user, project, tag, ref)
access = ::Gitlab::UserAccess.new(user, project: project) access = ::Gitlab::UserAccess.new(user, project: project)
......
...@@ -89,6 +89,15 @@ class ProjectPolicy < BasePolicy ...@@ -89,6 +89,15 @@ class ProjectPolicy < BasePolicy
::Gitlab::CurrentSettings.current_application_settings.mirror_available ::Gitlab::CurrentSettings.current_application_settings.mirror_available
end 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 # 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 # 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 # (`:read_issue_iid` or `:read_merge_request_iid`) but then wouldn't be
...@@ -417,6 +426,25 @@ class ProjectPolicy < BasePolicy ...@@ -417,6 +426,25 @@ class ProjectPolicy < BasePolicy
rule { ~can_have_multiple_clusters & has_clusters }.prevent :add_cluster 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 private
def team_member? def team_member?
......
...@@ -11,4 +11,6 @@ class IssuableSidebarExtrasEntity < Grape::Entity ...@@ -11,4 +11,6 @@ class IssuableSidebarExtrasEntity < Grape::Entity
expose :subscribed do |issuable| expose :subscribed do |issuable|
issuable.subscribed?(request.current_user, issuable.project) issuable.subscribed?(request.current_user, issuable.project)
end end
expose :assignees, using: API::Entities::UserBasic
end end
# frozen_string_literal: true # frozen_string_literal: true
class IssueSidebarExtrasEntity < IssuableSidebarExtrasEntity class IssueSidebarExtrasEntity < IssuableSidebarExtrasEntity
expose :assignees, using: API::Entities::UserBasic
end 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 # frozen_string_literal: true
class MergeRequestBasicEntity < Grape::Entity class MergeRequestBasicEntity < Grape::Entity
expose :assignee_id
expose :merge_status expose :merge_status
expose :merge_error expose :merge_error
expose :state expose :state
...@@ -9,7 +8,7 @@ class MergeRequestBasicEntity < Grape::Entity ...@@ -9,7 +8,7 @@ class MergeRequestBasicEntity < Grape::Entity
expose :rebase_in_progress?, as: :rebase_in_progress expose :rebase_in_progress?, as: :rebase_in_progress
expose :milestone, using: API::Entities::Milestone expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity expose :labels, using: LabelEntity
expose :assignee, using: API::Entities::UserBasic expose :assignees, using: API::Entities::UserBasic
expose :task_status, :task_status_short expose :task_status, :task_status_short
expose :lock_version, :lock_version expose :lock_version, :lock_version
end end
...@@ -8,9 +8,9 @@ class MergeRequestSerializer < BaseSerializer ...@@ -8,9 +8,9 @@ class MergeRequestSerializer < BaseSerializer
entity = entity =
case opts[:serializer] case opts[:serializer]
when 'sidebar' when 'sidebar'
MergeRequestSidebarBasicEntity IssuableSidebarBasicEntity
when 'sidebar_extras' when 'sidebar_extras'
IssuableSidebarExtrasEntity MergeRequestSidebarExtrasEntity
when 'basic' when 'basic'
MergeRequestBasicEntity MergeRequestBasicEntity
else 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 ...@@ -7,23 +7,7 @@ class PipelineSerializer < BaseSerializer
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def represent(resource, opts = {}) def represent(resource, opts = {})
if resource.is_a?(ActiveRecord::Relation) if resource.is_a?(ActiveRecord::Relation)
resource = resource.preload([ resource = resource.preload(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
if paginated? if paginated?
...@@ -51,4 +35,26 @@ class PipelineSerializer < BaseSerializer ...@@ -51,4 +35,26 @@ class PipelineSerializer < BaseSerializer
data = represent(resource, { only: [{ details: [:stages] }], preload: true }) data = represent(resource, { only: [{ details: [:stages] }], preload: true })
data.dig(:details, :stages) || [] data.dig(:details, :stages) || []
end 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 end
...@@ -2,9 +2,17 @@ ...@@ -2,9 +2,17 @@
module ApplicationSettings module ApplicationSettings
class UpdateService < ApplicationSettings::BaseService class UpdateService < ApplicationSettings::BaseService
include ValidatesClassificationLabel
attr_reader :params, :application_setting attr_reader :params, :application_setting
def execute 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)) update_terms(@params.delete(:terms))
if params.key?(:performance_bar_allowed_group_path) if params.key?(:performance_bar_allowed_group_path)
......
...@@ -37,7 +37,7 @@ module Ci ...@@ -37,7 +37,7 @@ module Ci
variables_attributes: params[:variables_attributes], variables_attributes: params[:variables_attributes],
project: project, project: project,
current_user: current_user, current_user: current_user,
push_options: params[:push_options], push_options: params[:push_options] || {},
chat_data: params[:chat_data], chat_data: params[:chat_data],
**extra_options(options)) **extra_options(options))
......
...@@ -16,6 +16,7 @@ module Clusters ...@@ -16,6 +16,7 @@ module Clusters
error_code: error.respond_to?(:error_code) ? error.error_code : nil, error_code: error.respond_to?(:error_code) ? error.error_code : nil,
service: self.class.name, service: self.class.name,
app_id: app.id, app_id: app.id,
app_name: app.name,
project_ids: app.cluster.project_ids, project_ids: app.cluster.project_ids,
group_ids: app.cluster.group_ids group_ids: app.cluster.group_ids
} }
...@@ -30,6 +31,19 @@ module Clusters ...@@ -30,6 +31,19 @@ module Clusters
Gitlab::Sentry.track_acceptable_exception(error, extra: meta) Gitlab::Sentry.track_acceptable_exception(error, extra: meta)
end 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 def logger
@logger ||= Gitlab::Kubernetes::Logger.build @logger ||= Gitlab::Kubernetes::Logger.build
end end
......
...@@ -7,8 +7,10 @@ module Clusters ...@@ -7,8 +7,10 @@ module Clusters
return unless app.scheduled? return unless app.scheduled?
app.make_installing! app.make_installing!
log_event(:begin_install)
helm_api.install(install_command) helm_api.install(install_command)
log_event(:schedule_wait_for_installation)
ClusterWaitForAppInstallationWorker.perform_in( ClusterWaitForAppInstallationWorker.perform_in(
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
rescue Kubeclient::HttpError => e rescue Kubeclient::HttpError => e
......
...@@ -8,8 +8,10 @@ module Clusters ...@@ -8,8 +8,10 @@ module Clusters
app.make_updating! app.make_updating!
log_event(:begin_patch)
helm_api.update(update_command) helm_api.update(update_command)
log_event(:schedule_wait_for_patch)
ClusterWaitForAppInstallationWorker.perform_in( ClusterWaitForAppInstallationWorker.perform_in(
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
rescue Kubeclient::HttpError => e rescue Kubeclient::HttpError => e
......
...@@ -9,10 +9,12 @@ module Clusters ...@@ -9,10 +9,12 @@ module Clusters
begin begin
app.make_updating! app.make_updating!
log_event(:begin_upgrade)
# install_command works with upgrades too # install_command works with upgrades too
# as it basically does `helm upgrade --install` # as it basically does `helm upgrade --install`
helm_api.update(install_command) helm_api.update(install_command)
log_event(:schedule_wait_for_upgrade)
ClusterWaitForAppInstallationWorker.perform_in( ClusterWaitForAppInstallationWorker.perform_in(
ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
rescue Kubeclient::HttpError => e 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 ...@@ -79,7 +79,7 @@ module Git
limited_commits, limited_commits,
event_message, event_message,
commits_count: commits_count, 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 # Dependent code may modify the push data, so return a duplicate each time
......
...@@ -34,14 +34,20 @@ class IssuableBaseService < BaseService ...@@ -34,14 +34,20 @@ class IssuableBaseService < BaseService
end end
def filter_assignee(issuable) 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 if params[:assignee_ids].map(&:to_s) == [IssuableFinder::NONE]
params[:assignee_id] = "" params[:assignee_ids] = []
elsif assignee_ids.any?
params[:assignee_ids] = assignee_ids
else else
params.delete(:assignee_id) unless assignee_can_read?(issuable, assignee_id) params.delete(:assignee_ids)
end end
end end
...@@ -352,7 +358,7 @@ class IssuableBaseService < BaseService ...@@ -352,7 +358,7 @@ class IssuableBaseService < BaseService
end end
def has_changes?(issuable, old_labels: [], old_assignees: []) 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| attrs_changed = valid_attrs.any? do |attr|
issuable.previous_changes.include?(attr.to_s) issuable.previous_changes.include?(attr.to_s)
......
...@@ -20,7 +20,7 @@ module Issues ...@@ -20,7 +20,7 @@ module Issues
private private
def create_assignee_note(issue, old_assignees) def create_assignee_note(issue, old_assignees)
SystemNoteService.change_issue_assignees( SystemNoteService.change_issuable_assignees(
issue, issue.project, current_user, old_assignees) issue, issue.project, current_user, old_assignees)
end end
...@@ -31,26 +31,6 @@ module Issues ...@@ -31,26 +31,6 @@ module Issues
issue.project.execute_services(issue_data, hooks_scope) issue.project.execute_services(issue_data, hooks_scope)
end 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) def update_project_counter_caches?(issue)
super || issue.confidential_changed? super || issue.confidential_changed?
end end
......
...@@ -39,7 +39,7 @@ module Issues ...@@ -39,7 +39,7 @@ module Issues
if issue.assignees != old_assignees if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees) create_assignee_note(issue, old_assignees)
notification_service.async.reassigned_issue(issue, current_user, 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 end
if issue.previous_changes.include?('confidential') if issue.previous_changes.include?('confidential')
......
...@@ -49,9 +49,9 @@ module MergeRequests ...@@ -49,9 +49,9 @@ module MergeRequests
MergeRequestMetricsService.new(merge_request.metrics) MergeRequestMetricsService.new(merge_request.metrics)
end end
def create_assignee_note(merge_request) def create_assignee_note(merge_request, old_assignees)
SystemNoteService.change_assignee( SystemNoteService.change_issuable_assignees(
merge_request, merge_request.project, current_user, merge_request.assignee) merge_request, merge_request.project, current_user, old_assignees)
end end
def create_pipeline_for(merge_request, user) 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 ...@@ -14,7 +14,9 @@ module MergeRequests
private private
def refresh_merge_requests! 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)) Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits))
# Be sure to close outstanding MRs before reloading them to avoid generating an # Be sure to close outstanding MRs before reloading them to avoid generating an
# empty diff during a manual merge # empty diff during a manual merge
close_upon_missing_source_branch_ref close_upon_missing_source_branch_ref
......
...@@ -24,13 +24,13 @@ module MergeRequests ...@@ -24,13 +24,13 @@ module MergeRequests
update_task_event(merge_request) || update(merge_request) update_task_event(merge_request) || update(merge_request)
end end
# rubocop:disable Metrics/AbcSize
def handle_changes(merge_request, options) def handle_changes(merge_request, options)
old_associations = options.fetch(:old_associations, {}) old_associations = options.fetch(:old_associations, {})
old_labels = old_associations.fetch(:labels, []) old_labels = old_associations.fetch(:labels, [])
old_mentioned_users = old_associations.fetch(:mentioned_users, []) 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) todo_service.mark_pending_todos_as_done(merge_request, current_user)
end end
...@@ -45,15 +45,10 @@ module MergeRequests ...@@ -45,15 +45,10 @@ module MergeRequests
merge_request.target_branch) merge_request.target_branch)
end end
if merge_request.previous_changes.include?('assignee_id') if merge_request.assignees != old_assignees
reassigned_merge_request_args = [merge_request, current_user] create_assignee_note(merge_request, old_assignees)
notification_service.async.reassigned_merge_request(merge_request, current_user, old_assignees)
old_assignee_id = merge_request.previous_changes['assignee_id'].first todo_service.reassigned_issuable(merge_request, current_user, old_assignees)
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)
end end
if merge_request.previous_changes.include?('target_branch') || if merge_request.previous_changes.include?('target_branch') ||
...@@ -81,7 +76,6 @@ module MergeRequests ...@@ -81,7 +76,6 @@ module MergeRequests
) )
end end
end end
# rubocop:enable Metrics/AbcSize
def handle_task_changes(merge_request) def handle_task_changes(merge_request)
todo_service.mark_pending_todos_as_done(merge_request, current_user) todo_service.mark_pending_todos_as_done(merge_request, current_user)
......
...@@ -247,15 +247,15 @@ module NotificationRecipientService ...@@ -247,15 +247,15 @@ module NotificationRecipientService
attr_reader :target attr_reader :target
attr_reader :current_user attr_reader :current_user
attr_reader :action attr_reader :action
attr_reader :previous_assignee attr_reader :previous_assignees
attr_reader :skip_current_user 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 @target = target
@current_user = current_user @current_user = current_user
@action = action @action = action
@custom_action = custom_action @custom_action = custom_action
@previous_assignee = previous_assignee @previous_assignees = previous_assignees
@skip_current_user = skip_current_user @skip_current_user = skip_current_user
end end
...@@ -270,11 +270,7 @@ module NotificationRecipientService ...@@ -270,11 +270,7 @@ module NotificationRecipientService
# Re-assign is considered as a mention of the new assignee # Re-assign is considered as a mention of the new assignee
case custom_action case custom_action
when :reassign_merge_request when :reassign_merge_request, :reassign_issue
add_recipients(previous_assignee, :mention, nil)
add_recipients(target.assignee, :mention, NotificationReason::ASSIGNED)
when :reassign_issue
previous_assignees = Array(previous_assignee)
add_recipients(previous_assignees, :mention, nil) add_recipients(previous_assignees, :mention, nil)
add_recipients(target.assignees, :mention, NotificationReason::ASSIGNED) add_recipients(target.assignees, :mention, NotificationReason::ASSIGNED)
end end
...@@ -287,17 +283,11 @@ module NotificationRecipientService ...@@ -287,17 +283,11 @@ module NotificationRecipientService
# receive them, too. # receive them, too.
add_mentions(current_user, target: target) 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 # We use the `:participating` notification level in order to match existing legacy behavior as captured
# in existing specs (notification_service_spec.rb ~ line 507) # 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 add_labels_subscribers
end end
......
...@@ -95,8 +95,8 @@ class NotificationService ...@@ -95,8 +95,8 @@ class NotificationService
# When we reassign an issue we should send an email to: # When we reassign an issue we should send an email to:
# #
# * issue old assignee if their notification level is not Disabled # * issue old assignees if their notification level is not Disabled
# * issue new assignee 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" # * users with custom level checked with "reassign issue"
# #
def reassigned_issue(issue, current_user, previous_assignees = []) def reassigned_issue(issue, current_user, previous_assignees = [])
...@@ -104,7 +104,7 @@ class NotificationService ...@@ -104,7 +104,7 @@ class NotificationService
issue, issue,
current_user, current_user,
action: "reassign", action: "reassign",
previous_assignee: previous_assignees previous_assignees: previous_assignees
) )
previous_assignee_ids = previous_assignees.map(&:id) previous_assignee_ids = previous_assignees.map(&:id)
...@@ -140,7 +140,7 @@ class NotificationService ...@@ -140,7 +140,7 @@ class NotificationService
# When create a merge request we should send an email to: # When create a merge request we should send an email to:
# #
# * mr author # * 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 # * project team members with notification level higher then Participating
# * watchers of the mr's labels # * watchers of the mr's labels
# * users with custom level checked with "new merge request" # * users with custom level checked with "new merge request"
...@@ -184,23 +184,25 @@ class NotificationService ...@@ -184,23 +184,25 @@ class NotificationService
# When we reassign a merge_request we should send an email to: # 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 old assignees if their notification level is not Disabled
# * merge_request assignee 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" # * 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( recipients = NotificationRecipientService.build_recipients(
merge_request, merge_request,
current_user, current_user,
action: "reassign", action: "reassign",
previous_assignee: previous_assignee previous_assignees: previous_assignees
) )
previous_assignee_ids = previous_assignees.map(&:id)
recipients.each do |recipient| recipients.each do |recipient|
mailer.reassigned_merge_request_email( mailer.reassigned_merge_request_email(
recipient.user.id, recipient.user.id,
merge_request.id, merge_request.id,
previous_assignee&.id, previous_assignee_ids,
current_user.id, current_user.id,
recipient.reason recipient.reason
).deliver_later ).deliver_later
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module Projects module Projects
class CreateService < BaseService class CreateService < BaseService
include ValidatesClassificationLabel
def initialize(user, params) def initialize(user, params)
@current_user, @params = user, params.dup @current_user, @params = user, params.dup
@skip_wiki = @params.delete(:skip_wiki) @skip_wiki = @params.delete(:skip_wiki)
...@@ -45,6 +47,8 @@ module Projects ...@@ -45,6 +47,8 @@ module Projects
relations_block&.call(@project) relations_block&.call(@project)
yield(@project) if block_given? 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 # If the block added errors, don't try to save the project
return @project if @project.errors.any? return @project if @project.errors.any?
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
module Projects module Projects
class UpdateService < BaseService class UpdateService < BaseService
include UpdateVisibilityLevel include UpdateVisibilityLevel
include ValidatesClassificationLabel
ValidationError = Class.new(StandardError) ValidationError = Class.new(StandardError)
...@@ -14,6 +15,8 @@ module Projects ...@@ -14,6 +15,8 @@ module Projects
yield if block_given? yield if block_given?
validate_classification_label(project, :external_authorization_classification_label)
# If the block added errors, don't try to save the project # If the block added errors, don't try to save the project
return update_failed! if project.errors.any? 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 ...@@ -69,7 +69,7 @@ module SystemNoteService
# Called when the assignees of an Issue is changed or removed # Called when the assignees of an Issue is changed or removed
# #
# issue - Issue object # issuable - Issuable object (responds to assignees)
# project - Project owning noteable # project - Project owning noteable
# author - User performing the change # author - User performing the change
# assignees - Users being assigned, or nil # assignees - Users being assigned, or nil
...@@ -85,9 +85,9 @@ module SystemNoteService ...@@ -85,9 +85,9 @@ module SystemNoteService
# "assigned to @user1 and @user2" # "assigned to @user1 and @user2"
# #
# Returns the created Note object # Returns the created Note object
def change_issue_assignees(issue, project, author, old_assignees) def change_issuable_assignees(issuable, project, author, old_assignees)
unassigned_users = old_assignees - issue.assignees unassigned_users = old_assignees - issuable.assignees
added_users = issue.assignees.to_a - old_assignees added_users = issuable.assignees.to_a - old_assignees
text_parts = [] text_parts = []
text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any? text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
...@@ -95,7 +95,7 @@ module SystemNoteService ...@@ -95,7 +95,7 @@ module SystemNoteService
body = text_parts.join(' and ') 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 end
# Called when the milestone of a Noteable is changed # Called when the milestone of a Noteable is changed
......
...@@ -49,12 +49,12 @@ class TodoService ...@@ -49,12 +49,12 @@ class TodoService
todo_users.each(&:update_todos_count_cache) todo_users.each(&:update_todos_count_cache)
end 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 = []) def reassigned_issuable(issuable, current_user, old_assignees = [])
create_assignment_todo(issue, current_user, old_assignees) create_assignment_todo(issuable, current_user, old_assignees)
end end
# When create a merge request we should: # When create a merge request we should:
...@@ -82,14 +82,6 @@ class TodoService ...@@ -82,14 +82,6 @@ class TodoService
mark_pending_todos_as_done(merge_request, current_user) mark_pending_todos_as_done(merge_request, current_user)
end 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: # When merge a merge request we should:
# #
# * mark all pending todos related to the target for the current user as done # * mark all pending todos related to the target for the current user as done
......
...@@ -8,6 +8,7 @@ class VerifyPagesDomainService < BaseService ...@@ -8,6 +8,7 @@ class VerifyPagesDomainService < BaseService
# How long verification lasts for # How long verification lasts for
VERIFICATION_PERIOD = 7.days VERIFICATION_PERIOD = 7.days
REMOVAL_DELAY = 1.week.freeze
attr_reader :domain attr_reader :domain
...@@ -36,7 +37,7 @@ class VerifyPagesDomainService < BaseService ...@@ -36,7 +37,7 @@ class VerifyPagesDomainService < BaseService
# Prevent any pre-existing grace period from being truncated # Prevent any pre-existing grace period from being truncated
reverify = [domain.enabled_until, VERIFICATION_PERIOD.from_now].compact.max 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) domain.save!(validate: false)
if was_disabled if was_disabled
...@@ -49,18 +50,20 @@ class VerifyPagesDomainService < BaseService ...@@ -49,18 +50,20 @@ class VerifyPagesDomainService < BaseService
end end
def unverify_domain! def unverify_domain!
if domain.verified? was_verified = domain.verified?
domain.assign_attributes(verified_at: nil)
domain.save!(validate: false)
notify(:verification_failed) domain.assign_attributes(verified_at: nil)
end 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}") error("Couldn't verify #{domain.domain}")
end end
def disable_domain! def disable_domain!
domain.assign_attributes(verified_at: nil, enabled_until: nil) domain.assign_attributes(verified_at: nil, enabled_until: nil)
domain.remove_at ||= REMOVAL_DELAY.from_now
domain.save!(validate: false) domain.save!(validate: false)
notify(:disabled) 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 @@ ...@@ -68,7 +68,7 @@
.settings-content .settings-content
= render 'terms' = 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?) } %section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header .settings-header
......
...@@ -24,8 +24,9 @@ ...@@ -24,8 +24,9 @@
Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises. Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
%br %br
Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}. 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 .row.prepend-top-default
.col-md-8 .col-md-8
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
.alert-wrapper .alert-wrapper
= render "layouts/broadcast" = render "layouts/broadcast"
= render "layouts/header/read_only_banner" = render "layouts/header/read_only_banner"
= render "layouts/nav/classification_level_banner"
= yield :flash_message = yield :flash_message
= render "shared/ping_consent" = render "shared/ping_consent"
- unless @hide_breadcrumbs - 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 ...@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to') = merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)} Author: #{sanitize_name(@merge_request.author_name)}
Assignee: #{sanitize_name(@merge_request.assignee_name)} = assignees_label(@merge_request)
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- if @issue.assignees.any? - if @issue.assignees.any?
%p %p
Assignee: #{@issue.assignee_list} = assignees_label(@issue)
%p %p
This issue is due on: #{@issue.due_date.to_s(:medium)} This issue is due on: #{@issue.due_date.to_s(:medium)}
......
...@@ -2,6 +2,6 @@ The following issue is due on <%= @issue.due_date %>: ...@@ -2,6 +2,6 @@ The following issue is due on <%= @issue.due_date %>:
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= @issue.author_name %> Author: <%= @issue.author_name %>
Assignee: <%= @issue.assignee_list %> <%= assignees_label(@issue) %>
<%= @issue.description %> <%= @issue.description %>
...@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m ...@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to') = merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)} 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 ...@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to') = merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)} 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 ...@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to') = merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)} Author: #{sanitize_name(@merge_request.author_name)}
Assignee: #{sanitize_name(@merge_request.assignee_name)} = assignees_label(@merge_request)
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
- if @issue.assignees.any? - if @issue.assignees.any?
%p %p
Assignee: #{@issue.assignee_list} = assignees_label(@issue)
- if @issue.description - if @issue.description
%div %div
......
...@@ -2,6 +2,6 @@ New Issue was created. ...@@ -2,6 +2,6 @@ New Issue was created.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= sanitize_name(@issue.author_name) %> Author: <%= sanitize_name(@issue.author_name) %>
Assignee: <%= @issue.assignee_list %> <%= assignees_label(@issue) %>
<%= @issue.description %> <%= @issue.description %>
...@@ -2,6 +2,6 @@ You have been mentioned in an issue. ...@@ -2,6 +2,6 @@ You have been mentioned in an issue.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %> Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= sanitize_name(@issue.author_name) %> Author: <%= sanitize_name(@issue.author_name) %>
Assignee: <%= sanitize_name(@issue.assignee_list) %> <%= assignees_label(@issue) %>
<%= @issue.description %> <%= @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