Skip to content
Snippets Groups Projects
Commit 3fe9588b authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent ad8eea38
No related branches found
No related tags found
No related merge requests found
Showing
with 569 additions and 62 deletions
Loading
Loading
@@ -6,8 +6,8 @@
/doc/ @axil @marcia @eread @mikelewis
 
# Frontend maintainers should see everything in `app/assets/`
app/assets/ @ClemMakesApps @fatihacet @filipa @mikegreiling @timzallmann @kushalpandya @pslaughter @wortschi @ntepluhina
*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @mikegreiling @timzallmann @kushalpandya @pslaughter @wortschi @ntepluhina
app/assets/ @ClemMakesApps @fatihacet @filipa @mikegreiling @timzallmann @kushalpandya @pslaughter @wortschi @ntepluhina @iamphill
*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @mikegreiling @timzallmann @kushalpandya @pslaughter @wortschi @ntepluhina @iamphill
 
# Database maintainers should review changes in `db/`
db/ @gitlab-org/maintainers/database
Loading
Loading
Loading
Loading
@@ -122,6 +122,7 @@ schedule:review-build-cng:
- source scripts/utils.sh
- install_api_client_dependencies_with_apk
- source scripts/review_apps/review-apps.sh
- export REVIEW_APP_CONFIG_CHANGED=$(base_config_changed)
script:
- date
- check_kube_domain
Loading
Loading
Please view this file on the master branch, on stable branches it's out of date.
 
## 12.4.1
### Security (6 changes)
- Do not display project labels that are not visible for user accessing group labels.
- Do not index system notes for issue update.
- Redact search results based on Ability.allowed?.
- Do not show private cross references in epic notes.
- Filter out packages the user does'nt have permission to see at group level.
- Fixes a Open Redirect issue in `InternalRedirect`.
## 12.4.0
 
### Security (2 changes)
Loading
Loading
Loading
Loading
@@ -4,11 +4,12 @@ entry.
 
## 12.4.1
 
### Security (12 changes)
### Security (14 changes)
 
- Standardize error response when route is missing.
- Do not display project labels that are not visible for user accessing group labels.
- Show cross-referenced label and milestones in issues' activities only to authorized users.
- Show cross-referenced label and milestones in issues' activities only to authorized users.
- Analyze incoming GraphQL queries and check for recursion.
- Disallow unprivileged users from commenting on private repository commits.
- Don't allow maintainers of a target project to delete the source branch of a merge request from a fork.
Loading
Loading
@@ -17,6 +18,7 @@ entry.
- Return 404 on LFS request if project doesn't exist.
- Mask sentry auth token in Error Tracking dashboard.
- Fixes a Open Redirect issue in `InternalRedirect`.
- Remove deploy access level when project/group link is deleted.
- Sanitize all wiki markup formats with GitLab sanitization pipelines.
 
 
Loading
Loading
<script>
import { flatten, isNumber } from 'underscore';
import { GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import { roundOffFloat } from '~/lib/utils/common_utils';
import { hexToRgb } from '~/lib/utils/color_utils';
import { areaOpacityValues, symbolSizes, colorValues } from '../../constants';
import { graphDataValidatorForAnomalyValues } from '../../utils';
import MonitorTimeSeriesChart from './time_series.vue';
/**
* Series indexes
*/
const METRIC = 0;
const UPPER = 1;
const LOWER = 2;
/**
* Boundary area appearance
*/
const AREA_COLOR = colorValues.anomalyAreaColor;
const AREA_OPACITY = areaOpacityValues.default;
const AREA_COLOR_RGBA = `rgba(${hexToRgb(AREA_COLOR).join(',')},${AREA_OPACITY})`;
/**
* The anomaly component highlights when a metric shows
* some anomalous behavior.
*
* It shows both a metric line and a boundary band in a
* time series chart, the boundary band shows the normal
* range of values the metric should take.
*
* This component accepts 3 queries, which contain the
* "metric", "upper" limit and "lower" limit.
*
* The upper and lower series are "stacked areas" visually
* to create the boundary band, and if any "metric" value
* is outside this band, it is highlighted to warn users.
*
* The boundary band stack must be painted above the 0 line
* so the area is shown correctly. If any of the values of
* the data are negative, the chart data is shifted to be
* above 0 line.
*
* The data passed to the time series is will always be
* positive, but reformatted to show the original values of
* data.
*
*/
export default {
components: {
GlLineChart,
GlChartSeriesLabel,
MonitorTimeSeriesChart,
},
inheritAttrs: false,
props: {
graphData: {
type: Object,
required: true,
validator: graphDataValidatorForAnomalyValues,
},
},
computed: {
series() {
return this.graphData.queries.map(query => {
const values = query.result[0] ? query.result[0].values : [];
return {
label: query.label,
data: values.filter(([, value]) => !Number.isNaN(value)),
};
});
},
/**
* If any of the values of the data is negative, the
* chart data is shifted to the lowest value
*
* This offset is the lowest value.
*/
yOffset() {
const values = flatten(this.series.map(ser => ser.data.map(([, y]) => y)));
const min = values.length ? Math.floor(Math.min(...values)) : 0;
return min < 0 ? -min : 0;
},
metricData() {
const originalMetricQuery = this.graphData.queries[0];
const metricQuery = { ...originalMetricQuery };
metricQuery.result[0].values = metricQuery.result[0].values.map(([x, y]) => [
x,
y + this.yOffset,
]);
return {
...this.graphData,
type: 'line-chart',
queries: [metricQuery],
};
},
metricSeriesConfig() {
return {
type: 'line',
symbol: 'circle',
symbolSize: (val, params) => {
if (this.isDatapointAnomaly(params.dataIndex)) {
return symbolSizes.anomaly;
}
// 0 causes echarts to throw an error, use small number instead
// see https://gitlab.com/gitlab-org/gitlab-ui/issues/423
return 0.001;
},
showSymbol: true,
itemStyle: {
color: params => {
if (this.isDatapointAnomaly(params.dataIndex)) {
return colorValues.anomalySymbol;
}
return colorValues.primaryColor;
},
},
};
},
chartOptions() {
const [, upperSeries, lowerSeries] = this.series;
const calcOffsetY = (data, offsetCallback) =>
data.map((value, dataIndex) => {
const [x, y] = value;
return [x, y + offsetCallback(dataIndex)];
});
const yAxisWithOffset = {
name: this.yAxisLabel,
axisLabel: {
formatter: num => roundOffFloat(num - this.yOffset, 3).toString(),
},
};
/**
* Boundary is rendered by 2 series: An invisible
* series (opacity: 0) stacked on a visible one.
*
* Order is important, lower boundary is stacked
* *below* the upper boundary.
*/
const boundarySeries = [];
if (upperSeries.data.length && lowerSeries.data.length) {
// Lower boundary, plus the offset if negative values
boundarySeries.push(
this.makeBoundarySeries({
name: this.formatLegendLabel(lowerSeries),
data: calcOffsetY(lowerSeries.data, () => this.yOffset),
}),
);
// Upper boundary, minus the lower boundary
boundarySeries.push(
this.makeBoundarySeries({
name: this.formatLegendLabel(upperSeries),
data: calcOffsetY(upperSeries.data, i => -this.yValue(LOWER, i)),
areaStyle: {
color: AREA_COLOR,
opacity: AREA_OPACITY,
},
}),
);
}
return { yAxis: yAxisWithOffset, series: boundarySeries };
},
},
methods: {
formatLegendLabel(query) {
return query.label;
},
yValue(seriesIndex, dataIndex) {
const d = this.series[seriesIndex].data[dataIndex];
return d && d[1];
},
yValueFormatted(seriesIndex, dataIndex) {
const y = this.yValue(seriesIndex, dataIndex);
return isNumber(y) ? y.toFixed(3) : '';
},
isDatapointAnomaly(dataIndex) {
const yVal = this.yValue(METRIC, dataIndex);
const yUpper = this.yValue(UPPER, dataIndex);
const yLower = this.yValue(LOWER, dataIndex);
return (isNumber(yUpper) && yVal > yUpper) || (isNumber(yLower) && yVal < yLower);
},
makeBoundarySeries(series) {
const stackKey = 'anomaly-boundary-series-stack';
return {
type: 'line',
stack: stackKey,
lineStyle: {
width: 0,
color: AREA_COLOR_RGBA, // legend color
},
color: AREA_COLOR_RGBA, // tooltip color
symbol: 'none',
...series,
};
},
},
};
</script>
<template>
<monitor-time-series-chart
v-bind="$attrs"
:graph-data="metricData"
:option="chartOptions"
:series-config="metricSeriesConfig"
>
<slot></slot>
<template v-slot:tooltipContent="slotProps">
<div
v-for="(content, seriesIndex) in slotProps.tooltip.content"
:key="seriesIndex"
class="d-flex justify-content-between"
>
<gl-chart-series-label :color="content.color">
{{ content.name }}
</gl-chart-series-label>
<div class="prepend-left-32">
{{ yValueFormatted(seriesIndex, content.dataIndex) }}
</div>
</div>
</template>
</monitor-time-series-chart>
</template>
<script>
import { s__, __ } from '~/locale';
import _ from 'underscore';
import { GlLink, GlButton, GlTooltip, GlResizeObserverDirective } from '@gitlab/ui';
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
import { roundOffFloat } from '~/lib/utils/common_utils';
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
import Icon from '~/vue_shared/components/icon.vue';
import { chartHeight, graphTypes, lineTypes, symbolSizes, dateFormats } from '../../constants';
import {
chartHeight,
graphTypes,
lineTypes,
lineWidths,
symbolSizes,
dateFormats,
} from '../../constants';
import { makeDataSeries } from '~/helpers/monitor_helper';
import { graphDataValidatorForValues } from '../../utils';
 
Loading
Loading
@@ -30,6 +38,16 @@ export default {
required: true,
validator: graphDataValidatorForValues.bind(null, false),
},
option: {
type: Object,
required: false,
default: () => ({}),
},
seriesConfig: {
type: Object,
required: false,
default: () => ({}),
},
deploymentData: {
type: Array,
required: false,
Loading
Loading
@@ -96,29 +114,35 @@ export default {
const lineWidth =
appearance && appearance.line && appearance.line.width
? appearance.line.width
: undefined;
: lineWidths.default;
const areaStyle = {
opacity:
appearance && appearance.area && typeof appearance.area.opacity === 'number'
? appearance.area.opacity
: undefined,
};
const series = makeDataSeries(query.result, {
name: this.formatLegendLabel(query),
lineStyle: {
type: lineType,
width: lineWidth,
color: this.primaryColor,
},
showSymbol: false,
areaStyle: this.graphData.type === 'area-chart' ? areaStyle : undefined,
...this.seriesConfig,
});
 
return acc.concat(series);
}, []);
},
chartOptionSeries() {
return (this.option.series || []).concat(this.scatterSeries ? [this.scatterSeries] : []);
},
chartOptions() {
const option = _.omit(this.option, 'series');
return {
series: this.chartOptionSeries,
xAxis: {
name: __('Time'),
type: 'time',
Loading
Loading
@@ -135,8 +159,8 @@ export default {
formatter: num => roundOffFloat(num, 3).toString(),
},
},
series: this.scatterSeries,
dataZoom: [this.dataZoomConfig],
...option,
};
},
dataZoomConfig() {
Loading
Loading
@@ -144,6 +168,14 @@ export default {
 
return handleIcon ? { handleIcon } : {};
},
/**
* This method returns the earliest time value in all series of a chart.
* Takes a chart data with data to populate a timeseries.
* data should be an array of data points [t, y] where t is a ISO formatted date,
* and is sorted by t (time).
* @returns {(String|null)} earliest x value from all series, or null when the
* chart series data is empty.
*/
earliestDatapoint() {
return this.chartData.reduce((acc, series) => {
const { data } = series;
Loading
Loading
@@ -230,10 +262,11 @@ export default {
this.tooltip.sha = deploy.sha.substring(0, 8);
this.tooltip.commitUrl = deploy.commitUrl;
} else {
const { seriesName, color } = dataPoint;
const { seriesName, color, dataIndex } = dataPoint;
const value = yVal.toFixed(3);
this.tooltip.content.push({
name: seriesName,
dataIndex,
value,
color,
});
Loading
Loading
@@ -306,23 +339,27 @@ export default {
</template>
<template v-else>
<template slot="tooltipTitle">
<div class="text-nowrap">
{{ tooltip.title }}
</div>
<slot name="tooltipTitle">
<div class="text-nowrap">
{{ tooltip.title }}
</div>
</slot>
</template>
<template slot="tooltipContent">
<div
v-for="(content, key) in tooltip.content"
:key="key"
class="d-flex justify-content-between"
>
<gl-chart-series-label :color="isMultiSeries ? content.color : ''">
{{ content.name }}
</gl-chart-series-label>
<div class="prepend-left-32">
{{ content.value }}
<slot name="tooltipContent" :tooltip="tooltip">
<div
v-for="(content, key) in tooltip.content"
:key="key"
class="d-flex justify-content-between"
>
<gl-chart-series-label :color="isMultiSeries ? content.color : ''">
{{ content.name }}
</gl-chart-series-label>
<div class="prepend-left-32">
{{ content.value }}
</div>
</div>
</div>
</slot>
</template>
</template>
</component>
Loading
Loading
Loading
Loading
@@ -11,6 +11,7 @@ import {
} from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import MonitorTimeSeriesChart from './charts/time_series.vue';
import MonitorAnomalyChart from './charts/anomaly.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
import MonitorEmptyChart from './charts/empty_chart.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
Loading
Loading
@@ -19,7 +20,6 @@ import { downloadCSVOptions, generateLinkToChartOptions } from '../utils';
export default {
components: {
MonitorSingleStatChart,
MonitorTimeSeriesChart,
MonitorEmptyChart,
Icon,
GlDropdown,
Loading
Loading
@@ -67,6 +67,12 @@ export default {
const data = new Blob([this.csvText], { type: 'text/plain' });
return window.URL.createObjectURL(data);
},
monitorChartComponent() {
if (this.isPanelType('anomaly-chart')) {
return MonitorAnomalyChart;
}
return MonitorTimeSeriesChart;
},
},
methods: {
getGraphAlerts(queries) {
Loading
Loading
@@ -93,13 +99,14 @@ export default {
v-if="isPanelType('single-stat') && graphDataHasMetrics"
:graph-data="graphData"
/>
<monitor-time-series-chart
<component
:is="monitorChartComponent"
v-else-if="graphDataHasMetrics"
:graph-data="graphData"
:deployment-data="deploymentData"
:project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.queries)"
group-id="monitor-area-chart"
group-id="panel-type-chart"
>
<div class="d-flex align-items-center">
<alert-widget
Loading
Loading
@@ -141,6 +148,6 @@ export default {
</gl-dropdown-item>
</gl-dropdown>
</div>
</monitor-time-series-chart>
</component>
<monitor-empty-chart v-else :graph-title="graphData.title" />
</template>
Loading
Loading
@@ -14,13 +14,28 @@ export const graphTypes = {
};
 
export const symbolSizes = {
anomaly: 8,
default: 14,
};
 
export const areaOpacityValues = {
default: 0.2,
};
export const colorValues = {
primaryColor: '#1f78d1', // $blue-500 (see variables.scss)
anomalySymbol: '#db3b21',
anomalyAreaColor: '#1f78d1',
};
export const lineTypes = {
default: 'solid',
};
 
export const lineWidths = {
default: 2,
};
export const timeWindows = {
thirtyMinutes: __('30 minutes'),
threeHours: __('3 hours'),
Loading
Loading
Loading
Loading
@@ -131,4 +131,20 @@ export const downloadCSVOptions = title => {
return { category, action, label: 'Chart title', property: title };
};
 
/**
* This function validates the graph data contains exactly 3 queries plus
* value validations from graphDataValidatorForValues.
* @param {Object} isValues
* @param {Object} graphData the graph data response from a prometheus request
* @returns {boolean} true if the data is valid
*/
export const graphDataValidatorForAnomalyValues = graphData => {
const anomalySeriesCount = 3; // metric, upper, lower
return (
graphData.queries &&
graphData.queries.length === anomalySeriesCount &&
graphDataValidatorForValues(false, graphData)
);
};
export default {};
<script>
/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
import { __ } from '~/locale';
import { s__ } from '~/locale';
import projectFeatureSetting from './project_feature_setting.vue';
import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue';
import projectSettingRow from './project_setting_row.vue';
Loading
Loading
@@ -13,7 +12,7 @@ import {
} from '../constants';
import { toggleHiddenClassBySelector } from '../external';
 
const PAGE_FEATURE_ACCESS_LEVEL = __('Everyone');
const PAGE_FEATURE_ACCESS_LEVEL = s__('ProjectSettings|Everyone');
 
export default {
components: {
Loading
Loading
@@ -207,7 +206,10 @@ export default {
<template>
<div>
<div class="project-visibility-setting">
<project-setting-row :help-path="visibilityHelpPath" label="Project visibility">
<project-setting-row
:help-path="visibilityHelpPath"
:label="s__('ProjectSettings|Project visibility')"
>
<div class="project-feature-controls">
<div class="select-wrapper">
<select
Loading
Loading
@@ -220,17 +222,17 @@ export default {
<option
:value="visibilityOptions.PRIVATE"
:disabled="!visibilityAllowed(visibilityOptions.PRIVATE)"
>{{ __('Private') }}</option
>{{ s__('ProjectSettings|Private') }}</option
>
<option
:value="visibilityOptions.INTERNAL"
:disabled="!visibilityAllowed(visibilityOptions.INTERNAL)"
>{{ __('Internal') }}</option
>{{ s__('ProjectSettings|Internal') }}</option
>
<option
:value="visibilityOptions.PUBLIC"
:disabled="!visibilityAllowed(visibilityOptions.PUBLIC)"
>{{ __('Public') }}</option
>{{ s__('ProjectSettings|Public') }}</option
>
</select>
<i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
Loading
Loading
@@ -243,14 +245,15 @@ export default {
type="hidden"
name="project[request_access_enabled]"
/>
<input v-model="requestAccessEnabled" type="checkbox" /> Allow users to request access
<input v-model="requestAccessEnabled" type="checkbox" />
{{ s__('ProjectSettings|Allow users to request access') }}
</label>
</project-setting-row>
</div>
<div :class="{ 'highlight-changes': highlightChangesClass }" class="project-feature-settings">
<project-setting-row
label="Issues"
help-text="Lightweight issue tracking system for this project"
:label="s__('ProjectSettings|Issues')"
:help-text="s__('ProjectSettings|Lightweight issue tracking system for this project')"
>
<project-feature-setting
v-model="issuesAccessLevel"
Loading
Loading
@@ -258,7 +261,10 @@ export default {
name="project[project_feature_attributes][issues_access_level]"
/>
</project-setting-row>
<project-setting-row label="Repository" help-text="View and edit files in this project">
<project-setting-row
:label="s__('ProjectSettings|Repository')"
:help-text="s__('ProjectSettings|View and edit files in this project')"
>
<project-feature-setting
v-model="repositoryAccessLevel"
:options="featureAccessLevelOptions"
Loading
Loading
@@ -267,8 +273,8 @@ export default {
</project-setting-row>
<div class="project-feature-setting-group">
<project-setting-row
label="Merge requests"
help-text="Submit changes to be merged upstream"
:label="s__('ProjectSettings|Merge requests')"
:help-text="s__('ProjectSettings|Submit changes to be merged upstream')"
>
<project-feature-setting
v-model="mergeRequestsAccessLevel"
Loading
Loading
@@ -277,7 +283,10 @@ export default {
name="project[project_feature_attributes][merge_requests_access_level]"
/>
</project-setting-row>
<project-setting-row label="Pipelines" help-text="Build, test, and deploy your changes">
<project-setting-row
:label="s__('ProjectSettings|Pipelines')"
:help-text="s__('ProjectSettings|Build, test, and deploy your changes')"
>
<project-feature-setting
v-model="buildsAccessLevel"
:options="repoFeatureAccessLevelOptions"
Loading
Loading
@@ -288,11 +297,17 @@ export default {
<project-setting-row
v-if="registryAvailable"
:help-path="registryHelpPath"
label="Container registry"
help-text="Every project can have its own space to store its Docker images"
:label="s__('ProjectSettings|Container registry')"
:help-text="
s__('ProjectSettings|Every project can have its own space to store its Docker images')
"
>
<div v-if="showContainerRegistryPublicNote" class="text-muted">
{{ __('Note: the container registry is always visible when a project is public') }}
{{
s__(
'ProjectSettings|Note: the container registry is always visible when a project is public',
)
}}
</div>
<project-feature-toggle
v-model="containerRegistryEnabled"
Loading
Loading
@@ -303,8 +318,10 @@ export default {
<project-setting-row
v-if="lfsAvailable"
:help-path="lfsHelpPath"
label="Git Large File Storage"
help-text="Manages large files such as audio, video, and graphics files"
:label="s__('ProjectSettings|Git Large File Storage')"
:help-text="
s__('ProjectSettings|Manages large files such as audio, video, and graphics files')
"
>
<project-feature-toggle
v-model="lfsEnabled"
Loading
Loading
@@ -315,8 +332,10 @@ export default {
<project-setting-row
v-if="packagesAvailable"
:help-path="packagesHelpPath"
label="Packages"
help-text="Every project can have its own space to store its packages"
:label="s__('ProjectSettings|Packages')"
:help-text="
s__('ProjectSettings|Every project can have its own space to store its packages')
"
>
<project-feature-toggle
v-model="packagesEnabled"
Loading
Loading
@@ -325,7 +344,10 @@ export default {
/>
</project-setting-row>
</div>
<project-setting-row label="Wiki" help-text="Pages for project documentation">
<project-setting-row
:label="s__('ProjectSettings|Wiki')"
:help-text="s__('ProjectSettings|Pages for project documentation')"
>
<project-feature-setting
v-model="wikiAccessLevel"
:options="featureAccessLevelOptions"
Loading
Loading
@@ -333,8 +355,8 @@ export default {
/>
</project-setting-row>
<project-setting-row
label="Snippets"
help-text="Share code pastes with others out of Git repository"
:label="s__('ProjectSettings|Snippets')"
:help-text="s__('ProjectSettings|Share code pastes with others out of Git repository')"
>
<project-feature-setting
v-model="snippetsAccessLevel"
Loading
Loading
@@ -346,7 +368,9 @@ export default {
v-if="pagesAvailable && pagesAccessControlEnabled"
:help-path="pagesHelpPath"
:label="s__('ProjectSettings|Pages')"
:help-text="__('With GitLab Pages you can host your static websites on GitLab')"
:help-text="
s__('ProjectSettings|With GitLab Pages you can host your static websites on GitLab')
"
>
<project-feature-setting
v-model="pagesAccessLevel"
Loading
Loading
@@ -358,10 +382,13 @@ export default {
<project-setting-row v-if="canDisableEmails" class="mb-3">
<label class="js-emails-disabled">
<input :value="emailsDisabled" type="hidden" name="project[emails_disabled]" />
<input v-model="emailsDisabled" type="checkbox" /> {{ __('Disable email notifications') }}
<input v-model="emailsDisabled" type="checkbox" />
{{ s__('ProjectSettings|Disable email notifications') }}
</label>
<span class="form-text text-muted">{{
__('This setting will override user notification preferences for all project members.')
s__(
'ProjectSettings|This setting will override user notification preferences for all project members.',
)
}}</span>
</project-setting-row>
</div>
Loading
Loading
Loading
Loading
@@ -97,11 +97,13 @@ export default {
},
},
methods: {
openRow() {
if (this.isFolder) {
openRow(e) {
if (e.target.tagName === 'A') return;
if (this.isFolder && !e.metaKey) {
this.$router.push(this.routerLinkTo);
} else {
visitUrl(this.url);
visitUrl(this.url, e.metaKey);
}
},
},
Loading
Loading
# frozen_string_literal: true
class Groups::GroupLinksController < Groups::ApplicationController
before_action :check_feature_flag!
before_action :authorize_admin_group!
def create
shared_with_group = Group.find(params[:shared_with_group_id]) if params[:shared_with_group_id].present?
if shared_with_group
result = Groups::GroupLinks::CreateService
.new(shared_with_group, current_user, group_link_create_params)
.execute(group)
return render_404 if result[:http_status] == 404
flash[:alert] = result[:message] if result[:status] == :error
else
flash[:alert] = _('Please select a group.')
end
redirect_to group_group_members_path(group)
end
private
def group_link_create_params
params.permit(:shared_group_access, :expires_at)
end
def check_feature_flag!
render_404 unless Feature.enabled?(:share_group_with_group)
end
end
Loading
Loading
@@ -3,6 +3,10 @@
module WorkerAttributes
extend ActiveSupport::Concern
 
# Resource boundaries that workers can declare through the
# `worker_resource_boundary` attribute
VALID_RESOURCE_BOUNDARIES = [:memory, :cpu, :unknown].freeze
class_methods do
def feature_category(value)
raise "Invalid category. Use `feature_category_not_owned!` to mark a worker as not owned" if value == :not_owned
Loading
Loading
@@ -24,6 +28,48 @@ module WorkerAttributes
get_worker_attribute(:feature_category) == :not_owned
end
 
# This should be set for jobs that need to be run immediately, or, if
# they are delayed, risk creating inconsistencies in the application
# that could being perceived by the user as incorrect behavior
# (ie, a bug)
# See doc/development/sidekiq_style_guide.md#Latency-Sensitive-Jobs
# for details
def latency_sensitive_worker!
worker_attributes[:latency_sensitive] = true
end
# Returns a truthy value if the worker is latency sensitive.
# See doc/development/sidekiq_style_guide.md#Latency-Sensitive-Jobs
# for details
def latency_sensitive_worker?
worker_attributes[:latency_sensitive]
end
# Set this attribute on a job when it will call to services outside of the
# application, such as 3rd party applications, other k8s clusters etc See
# doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies for
# details
def worker_has_external_dependencies!
worker_attributes[:external_dependencies] = true
end
# Returns a truthy value if the worker has external dependencies.
# See doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies
# for details
def worker_has_external_dependencies?
worker_attributes[:external_dependencies]
end
def worker_resource_boundary(boundary)
raise "Invalid boundary" unless VALID_RESOURCE_BOUNDARIES.include? boundary
worker_attributes[:resource_boundary] = boundary
end
def get_worker_resource_boundary
worker_attributes[:resource_boundary] || :unknown
end
protected
 
# Returns a worker attribute declared on this class or its parent class.
Loading
Loading
Loading
Loading
@@ -30,6 +30,10 @@ class Group < Namespace
has_many :members_and_requesters, as: :source, class_name: 'GroupMember'
 
has_many :milestones
has_many :shared_group_links, foreign_key: :shared_with_group_id, class_name: 'GroupGroupLink'
has_many :shared_with_group_links, foreign_key: :shared_group_id, class_name: 'GroupGroupLink'
has_many :shared_groups, through: :shared_group_links, source: :shared_group
has_many :shared_with_groups, through: :shared_with_group_links, source: :shared_with_group
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :shared_projects, through: :project_group_links, source: :project
 
Loading
Loading
@@ -376,11 +380,12 @@ class Group < Namespace
 
return GroupMember::OWNER if user.admin?
 
members_with_parents
.where(user_id: user)
.reorder(access_level: :desc)
.first&.
access_level || GroupMember::NO_ACCESS
max_member_access = members_with_parents.where(user_id: user)
.reorder(access_level: :desc)
.first
&.access_level
max_member_access || max_member_access_for_user_from_shared_groups(user) || GroupMember::NO_ACCESS
end
 
def mattermost_team_params
Loading
Loading
@@ -474,6 +479,26 @@ class Group < Namespace
errors.add(:visibility_level, "#{visibility} is not allowed since there are sub-groups with higher visibility.")
end
 
def max_member_access_for_user_from_shared_groups(user)
return unless Feature.enabled?(:share_group_with_group)
group_group_link_table = GroupGroupLink.arel_table
group_member_table = GroupMember.arel_table
group_group_links_query = GroupGroupLink.where(shared_group_id: self_and_ancestors_ids)
cte = Gitlab::SQL::CTE.new(:group_group_links_cte, group_group_links_query)
link = GroupGroupLink
.with(cte.to_arel)
.from([group_member_table, cte.alias_to(group_group_link_table)])
.where(group_member_table[:user_id].eq(user.id))
.where(group_member_table[:source_id].eq(group_group_link_table[:shared_with_group_id]))
.reorder(Arel::Nodes::Descending.new(group_group_link_table[:group_access]))
.first
link&.group_access
end
def self.groups_including_descendants_by(group_ids)
Gitlab::ObjectHierarchy
.new(Group.where(id: group_ids))
Loading
Loading
# frozen_string_literal: true
class GroupGroupLink < ApplicationRecord
include Expirable
belongs_to :shared_group, class_name: 'Group', foreign_key: :shared_group_id
belongs_to :shared_with_group, class_name: 'Group', foreign_key: :shared_with_group_id
validates :shared_group, presence: true
validates :shared_group_id, uniqueness: { scope: [:shared_with_group_id],
message: _('The group has already been shared with this group') }
validates :shared_with_group, presence: true
validates :group_access, inclusion: { in: Gitlab::Access.values },
presence: true
def self.access_options
Gitlab::Access.options
end
def self.default_access
Gitlab::Access::DEVELOPER
end
end
# frozen_string_literal: true
module Groups
module GroupLinks
class CreateService < BaseService
def execute(shared_group)
unless group && shared_group &&
can?(current_user, :admin_group, shared_group) &&
can?(current_user, :read_group, group)
return error('Not Found', 404)
end
link = GroupGroupLink.new(
shared_group: shared_group,
shared_with_group: group,
group_access: params[:shared_group_access],
expires_at: params[:expires_at]
)
if link.save
group.refresh_members_authorized_projects
success(link: link)
else
error(link.errors.full_messages.to_sentence, 409)
end
end
end
end
end
Loading
Loading
@@ -7,5 +7,5 @@
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
 
%p.append-bottom-0
%p
= render "ci/variables/content"
Loading
Loading
@@ -5,6 +5,7 @@ class AuthorizedProjectsWorker
prepend WaitableWorker
 
feature_category :authentication_and_authorization
latency_sensitive_worker!
 
# This is a workaround for a Ruby 2.3.7 bug. rspec-mocks cannot restore the
# visibility of prepended modules. See https://github.com/rspec/rspec-mocks/issues/1231
Loading
Loading
Loading
Loading
@@ -5,6 +5,8 @@ class BuildFinishedWorker
include PipelineQueue
 
queue_namespace :pipeline_processing
latency_sensitive_worker!
worker_resource_boundary :cpu
 
# rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
Loading
Loading
Loading
Loading
@@ -6,6 +6,7 @@ class BuildHooksWorker
 
queue_namespace :pipeline_hooks
feature_category :continuous_integration
latency_sensitive_worker!
 
# rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment