Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-07-28 21:11:01 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-07-28 21:11:01 +0300
commit7c5f1bfac791045e54386b9c9bb56ee24afc68ca (patch)
treea11c8dff3994899c25acacb383c0a70522a24cd1
parentd62fd6e04c272d48dccde4033529ca97c27502f6 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/performance/regexp_match.yml20
-rw-r--r--app/assets/javascripts/clusters/forms/components/integration_form.vue3
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters_actions.vue6
-rw-r--r--app/assets/javascripts/observability/client.js12
-rw-r--r--app/assets/javascripts/pages/projects/tracing/show/index.js4
-rw-r--r--app/assets/javascripts/tracing/components/tracing_details.vue90
-rw-r--r--app/assets/javascripts/tracing/components/tracing_empty_state.vue8
-rw-r--r--app/assets/javascripts/tracing/components/tracing_list.vue21
-rw-r--r--app/assets/javascripts/tracing/components/tracing_table_list.vue27
-rw-r--r--app/assets/javascripts/tracing/details_index.vue49
-rw-r--r--app/graphql/mutations/work_items/subscribe.rb41
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/models/application_record.rb2
-rw-r--r--app/models/todo.rb4
-rw-r--r--app/services/ci/pipeline_schedules/base_save_service.rb54
-rw-r--r--app/services/ci/pipeline_schedules/create_service.rb40
-rw-r--r--app/services/ci/pipeline_schedules/update_service.rb34
-rw-r--r--app/services/todos/destroy/group_private_service.rb5
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml4
-rw-r--r--app/views/dashboard/todos/_todo.html.haml1
-rw-r--r--db/migrate/20230714020854_add_name_and_description_to_member_roles.rb22
-rw-r--r--db/schema_migrations/202307140208541
-rw-r--r--db/structure.sql6
-rw-r--r--doc/api/graphql/reference/index.md24
-rw-r--r--doc/ci/environments/index.md2
-rw-r--r--doc/security/rate_limits.md8
-rw-r--r--generator_templates/gitlab_internal_events/event_definition.yml2
-rw-r--r--lib/gitlab/authorized_keys.rb2
-rw-r--r--lib/gitlab/checks/branch_check.rb2
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb6
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata/entry.rb2
-rw-r--r--lib/gitlab/ci/project_config/remote.rb2
-rw-r--r--lib/gitlab/ci/reports/sbom/component.rb17
-rw-r--r--lib/gitlab/content_security_policy/config_loader.rb309
-rw-r--r--lib/gitlab/database/postgres_constraint.rb2
-rw-r--r--lib/gitlab/database/postgres_foreign_key.rb6
-rw-r--r--lib/gitlab/database/postgres_index.rb2
-rw-r--r--lib/gitlab/database/postgres_partition.rb4
-rw-r--r--lib/gitlab/database/postgres_partitioned_table.rb2
-rw-r--r--lib/gitlab/database/reindexing/reindex_concurrently.rb2
-rw-r--r--lib/gitlab/dependency_linker/base_linker.rb2
-rw-r--r--lib/gitlab/dependency_linker/composer_json_linker.rb2
-rw-r--r--lib/gitlab/diff/parser.rb2
-rw-r--r--lib/gitlab/email/reply_parser.rb2
-rw-r--r--lib/gitlab/git/gitmodules_parser.rb2
-rw-r--r--lib/gitlab/metrics/samplers/threads_sampler.rb2
-rw-r--r--lib/gitlab/middleware/sidekiq_web_static.rb2
-rw-r--r--lib/gitlab/middleware/static.rb2
-rw-r--r--lib/gitlab/url_blocker.rb6
-rw-r--r--locale/gitlab.pot70
-rw-r--r--qa/qa/page/project/infrastructure/kubernetes/add_existing.rb46
-rw-r--r--qa/qa/page/project/infrastructure/kubernetes/index.rb25
-rw-r--r--qa/qa/page/project/infrastructure/kubernetes/show.rb30
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb38
-rw-r--r--spec/factories/ci/reports/sbom/components.rb2
-rw-r--r--spec/frontend/tracing/components/tracing_details_spec.js103
-rw-r--r--spec/frontend/tracing/components/tracing_list_spec.js12
-rw-r--r--spec/frontend/tracing/components/tracing_table_list_spec.js16
-rw-r--r--spec/frontend/tracing/details_index_spec.js42
-rw-r--r--spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/sbom/component_spec.rb26
-rw-r--r--spec/lib/gitlab/content_security_policy/config_loader_spec.rb93
-rw-r--r--spec/requests/api/graphql/mutations/work_items/subscribe_spec.rb73
-rw-r--r--spec/services/ci/pipeline_schedules/create_service_spec.rb8
-rw-r--r--spec/services/ci/pipeline_schedules/update_service_spec.rb10
-rw-r--r--spec/services/todos/destroy/group_private_service_spec.rb34
-rw-r--r--spec/support/shared_examples/ci/pipeline_schedules_create_or_update_shared_examples.rb121
-rw-r--r--spec/support/shared_examples/controllers/internal_event_tracking_examples.rb9
68 files changed, 1199 insertions, 432 deletions
diff --git a/.rubocop_todo/performance/regexp_match.yml b/.rubocop_todo/performance/regexp_match.yml
index 2c80a74c538..0248938c84b 100644
--- a/.rubocop_todo/performance/regexp_match.yml
+++ b/.rubocop_todo/performance/regexp_match.yml
@@ -20,26 +20,6 @@ Performance/RegexpMatch:
- 'lib/banzai/filter/references/reference_filter.rb'
- 'lib/bulk_imports/path_normalization.rb'
- 'lib/feature/definition.rb'
- - 'lib/gitlab/authorized_keys.rb'
- - 'lib/gitlab/checks/branch_check.rb'
- - 'lib/gitlab/ci/build/artifacts/metadata.rb'
- - 'lib/gitlab/ci/build/artifacts/metadata/entry.rb'
- - 'lib/gitlab/ci/project_config/remote.rb'
- - 'lib/gitlab/database/postgres_constraint.rb'
- - 'lib/gitlab/database/postgres_foreign_key.rb'
- - 'lib/gitlab/database/postgres_index.rb'
- - 'lib/gitlab/database/postgres_partition.rb'
- - 'lib/gitlab/database/postgres_partitioned_table.rb'
- - 'lib/gitlab/database/reindexing/reindex_concurrently.rb'
- - 'lib/gitlab/dependency_linker/base_linker.rb'
- - 'lib/gitlab/dependency_linker/composer_json_linker.rb'
- - 'lib/gitlab/diff/parser.rb'
- - 'lib/gitlab/email/reply_parser.rb'
- - 'lib/gitlab/git/gitmodules_parser.rb'
- - 'lib/gitlab/metrics/samplers/threads_sampler.rb'
- - 'lib/gitlab/middleware/sidekiq_web_static.rb'
- - 'lib/gitlab/middleware/static.rb'
- - 'lib/gitlab/url_blocker.rb'
- 'lib/tasks/gitlab/update_templates.rake'
- 'lib/uploaded_file.rb'
- 'qa/qa/flow/integrations/slack.rb'
diff --git a/app/assets/javascripts/clusters/forms/components/integration_form.vue b/app/assets/javascripts/clusters/forms/components/integration_form.vue
index b2a8381f937..cd82465f2f0 100644
--- a/app/assets/javascripts/clusters/forms/components/integration_form.vue
+++ b/app/assets/javascripts/clusters/forms/components/integration_form.vue
@@ -74,7 +74,6 @@ export default {
v-gl-tooltip:tooltipcontainer
name="cluster[enabled]"
class="gl-mb-0 js-project-feature-toggle"
- data-qa-selector="integration_status_toggle"
aria-describedby="toggleCluster"
:disabled="!editable"
:label="$options.i18n.toggleLabel"
@@ -111,7 +110,6 @@ export default {
id="cluster_base_domain"
v-model="baseDomainField"
name="cluster[base_domain]"
- data-qa-selector="base_domain_field"
class="col-md-6"
type="text"
/>
@@ -144,7 +142,6 @@ export default {
type="submit"
:disabled="!canSubmit"
:aria-disabled="!canSubmit"
- data-qa-selector="save_changes_button"
>{{ s__('ClusterIntegration|Save changes') }}</gl-button
>
</div>
diff --git a/app/assets/javascripts/clusters_list/components/clusters_actions.vue b/app/assets/javascripts/clusters_list/components/clusters_actions.vue
index 7b97a5af373..c388d3fee71 100644
--- a/app/assets/javascripts/clusters_list/components/clusters_actions.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters_actions.vue
@@ -92,11 +92,7 @@ export default {
<!--TODO: Replace button-group workaround once `split` option for new dropdowns is implemented.-->
<!-- See issue at https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2263-->
- <gl-button-group
- ref="actions"
- data-qa-selector="clusters_actions_button"
- class="gl-w-full gl-mb-3 gl-md-w-auto gl-md-mb-0"
- >
+ <gl-button-group ref="actions" class="gl-w-full gl-mb-3 gl-md-w-auto gl-md-mb-0">
<gl-button
v-gl-modal-directive="shouldTriggerModal && $options.INSTALL_AGENT_MODAL_ID"
:href="defaultActionUrl"
diff --git a/app/assets/javascripts/observability/client.js b/app/assets/javascripts/observability/client.js
index 251c165e7dd..667bbc107dc 100644
--- a/app/assets/javascripts/observability/client.js
+++ b/app/assets/javascripts/observability/client.js
@@ -1,4 +1,5 @@
import axios from '~/lib/utils/axios_utils';
+import mockData from './mock_traces.json';
function enableTraces() {
// TODO remove mocks https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2271
@@ -19,6 +20,16 @@ function isTracingEnabled() {
});
}
+async function fetchTrace(tracingUrl, traceId) {
+ const data = mockData;
+ const trace = data.traces.find((t) => t.trace_id === traceId);
+ const duration = trace.spans.reduce((acc, cur) => acc + cur.duration_nano, 0);
+ return {
+ ...trace,
+ duration: duration / 1000,
+ };
+}
+
async function fetchTraces(tracingUrl) {
const { data } = await axios.get(tracingUrl, { withCredentials: true });
if (!Array.isArray(data.traces)) {
@@ -39,5 +50,6 @@ export function buildClient({ provisioningUrl, tracingUrl }) {
enableTraces: () => enableTraces(provisioningUrl),
isTracingEnabled: () => isTracingEnabled(provisioningUrl),
fetchTraces: () => fetchTraces(tracingUrl),
+ fetchTrace: (traceId) => fetchTrace(tracingUrl, traceId),
};
}
diff --git a/app/assets/javascripts/pages/projects/tracing/show/index.js b/app/assets/javascripts/pages/projects/tracing/show/index.js
new file mode 100644
index 00000000000..107c004aa5f
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/tracing/show/index.js
@@ -0,0 +1,4 @@
+import { initSimpleApp } from '~/helpers/init_simple_app_helper';
+import DetailsIndex from '~/tracing/details_index.vue';
+
+initSimpleApp('#js-tracing-details', DetailsIndex);
diff --git a/app/assets/javascripts/tracing/components/tracing_details.vue b/app/assets/javascripts/tracing/components/tracing_details.vue
new file mode 100644
index 00000000000..d8b2cbc9469
--- /dev/null
+++ b/app/assets/javascripts/tracing/components/tracing_details.vue
@@ -0,0 +1,90 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { createAlert } from '~/alert';
+import { visitUrl, isSafeURL } from '~/lib/utils/url_utility';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ },
+ i18n: {
+ error: s__('Tracing|Failed to load trace details.'),
+ },
+ props: {
+ observabilityClient: {
+ required: true,
+ type: Object,
+ },
+ traceId: {
+ required: true,
+ type: String,
+ },
+ tracingIndexUrl: {
+ required: true,
+ type: String,
+ validator: (val) => isSafeURL(val),
+ },
+ },
+ data() {
+ return {
+ trace: null,
+ loading: false,
+ };
+ },
+ created() {
+ this.validateAndFetch();
+ },
+ methods: {
+ async validateAndFetch() {
+ if (!this.traceId) {
+ createAlert({
+ message: this.$options.i18n.error,
+ });
+ }
+ this.loading = true;
+ try {
+ const enabled = await this.observabilityClient.isTracingEnabled();
+ if (enabled) {
+ await this.fetchTrace();
+ } else {
+ this.goToTracingIndex();
+ }
+ } catch (e) {
+ createAlert({
+ message: this.$options.i18n.error,
+ });
+ } finally {
+ this.loading = false;
+ }
+ },
+ async fetchTrace() {
+ this.loading = true;
+ try {
+ this.trace = await this.observabilityClient.fetchTrace(this.traceId);
+ } catch (e) {
+ createAlert({
+ message: this.$options.i18n.error,
+ });
+ } finally {
+ this.loading = false;
+ }
+ },
+ goToTracingIndex() {
+ visitUrl(this.tracingIndexUrl);
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="loading" class="gl-py-5">
+ <gl-loading-icon size="lg" />
+ </div>
+
+ <!-- TODO Replace with actual trace-details component-->
+ <div v-else-if="trace" data-testid="trace-details">
+ <p>{{ tracingIndexUrl }}</p>
+ <p>{{ trace }}</p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/tracing/components/tracing_empty_state.vue b/app/assets/javascripts/tracing/components/tracing_empty_state.vue
index 4cb3bd6d9f0..3cdb280ef9e 100644
--- a/app/assets/javascripts/tracing/components/tracing_empty_state.vue
+++ b/app/assets/javascripts/tracing/components/tracing_empty_state.vue
@@ -1,15 +1,15 @@
<script>
import EMPTY_TRACING_SVG from '@gitlab/svgs/dist/illustrations/monitoring/tracing.svg?url';
import { GlEmptyState, GlButton } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { s__ } from '~/locale';
export default {
EMPTY_TRACING_SVG,
name: 'TracingEmptyState',
i18n: {
- title: __('Get started with Tracing'),
- description: __('Monitor your applications with GitLab Distributed Tracing.'),
- enableButtonText: __('Enable'),
+ title: s__('Tracing|Get started with Tracing'),
+ description: s__('Tracing|Monitor your applications with GitLab Distributed Tracing.'),
+ enableButtonText: s__('Tracing|Enable'),
},
components: {
GlEmptyState,
diff --git a/app/assets/javascripts/tracing/components/tracing_list.vue b/app/assets/javascripts/tracing/components/tracing_list.vue
index 294e520d7ac..d247e78bb0b 100644
--- a/app/assets/javascripts/tracing/components/tracing_list.vue
+++ b/app/assets/javascripts/tracing/components/tracing_list.vue
@@ -1,7 +1,8 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { s__ } from '~/locale';
import { createAlert } from '~/alert';
+import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import TracingEmptyState from './tracing_empty_state.vue';
import TracingTableList from './tracing_table_list.vue';
@@ -28,7 +29,7 @@ export default {
traces: [],
};
},
- async created() {
+ created() {
this.checkEnabled();
},
methods: {
@@ -41,7 +42,7 @@ export default {
}
} catch (e) {
createAlert({
- message: __('Failed to load page.'),
+ message: s__('Tracing|Failed to load page.'),
});
} finally {
this.loading = false;
@@ -55,7 +56,7 @@ export default {
await this.fetchTraces();
} catch (e) {
createAlert({
- message: __('Failed to enable tracing.'),
+ message: s__('Tracing|Failed to enable tracing.'),
});
} finally {
this.loading = false;
@@ -68,12 +69,15 @@ export default {
this.traces = traces;
} catch (e) {
createAlert({
- message: __('Failed to load traces.'),
+ message: s__('Tracing|Failed to load traces.'),
});
} finally {
this.loading = false;
}
},
+ selectTrace(trace) {
+ visitUrl(joinPaths(window.location.pathname, trace.trace_id));
+ },
},
};
</script>
@@ -87,7 +91,12 @@ export default {
<template v-else-if="tracingEnabled !== null">
<tracing-empty-state v-if="tracingEnabled === false" :enable-tracing="enableTracing" />
- <tracing-table-list v-else :traces="traces" @reload="fetchTraces" />
+ <tracing-table-list
+ v-else
+ :traces="traces"
+ @reload="fetchTraces"
+ @trace-selected="selectTrace"
+ />
</template>
</div>
</template>
diff --git a/app/assets/javascripts/tracing/components/tracing_table_list.vue b/app/assets/javascripts/tracing/components/tracing_table_list.vue
index bbed5520b40..59604890c86 100644
--- a/app/assets/javascripts/tracing/components/tracing_table_list.vue
+++ b/app/assets/javascripts/tracing/components/tracing_table_list.vue
@@ -1,37 +1,37 @@
<script>
import { GlTable, GlLink } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { s__ } from '~/locale';
export const tableDataClass = 'gl-display-flex gl-md-display-table-cell gl-align-items-center';
export default {
name: 'TracingTableList',
i18n: {
- title: __('Traces'),
- emptyText: __('No traces to display.'),
- emptyLinkText: __('Check again'),
+ title: s__('Tracing|Traces'),
+ emptyText: s__('Tracing|No traces to display.'),
+ emptyLinkText: s__('Tracing|Check again'),
},
fields: [
{
key: 'timestamp',
- label: __('Date'),
+ label: s__('Tracing|Date'),
tdClass: tableDataClass,
sortable: true,
},
{
key: 'service_name',
- label: __('Service'),
+ label: s__('Tracing|Service'),
tdClass: tableDataClass,
sortable: true,
},
{
key: 'operation',
- label: __('Operation'),
+ label: s__('Tracing|Operation'),
tdClass: tableDataClass,
sortable: true,
},
{
key: 'duration',
- label: __('Duration'),
+ label: s__('Tracing|Duration'),
thClass: 'gl-w-15p',
tdClass: tableDataClass,
sortable: true,
@@ -47,6 +47,13 @@ export default {
type: Array,
},
},
+ methods: {
+ onSelect(items) {
+ if (items[0]) {
+ this.$emit('trace-selected', items[0]);
+ }
+ },
+ },
};
</script>
@@ -64,6 +71,10 @@ export default {
fixed
stacked="md"
tbody-tr-class="table-row"
+ selectable
+ select-mode="single"
+ selected-variant=""
+ @row-selected="onSelect"
>
<template #cell(timestamp)="data">
{{ data.item.timestamp }}
diff --git a/app/assets/javascripts/tracing/details_index.vue b/app/assets/javascripts/tracing/details_index.vue
new file mode 100644
index 00000000000..5702a88766c
--- /dev/null
+++ b/app/assets/javascripts/tracing/details_index.vue
@@ -0,0 +1,49 @@
+<script>
+import ObservabilityContainer from '~/observability/components/observability_container.vue';
+import TracingDetails from './components/tracing_details.vue';
+
+export default {
+ components: {
+ ObservabilityContainer,
+ TracingDetails,
+ },
+ props: {
+ traceId: {
+ type: String,
+ required: true,
+ },
+ oauthUrl: {
+ type: String,
+ required: true,
+ },
+ tracingUrl: {
+ type: String,
+ required: true,
+ },
+ provisioningUrl: {
+ type: String,
+ required: true,
+ },
+ tracingIndexUrl: {
+ required: true,
+ type: String,
+ },
+ },
+};
+</script>
+
+<template>
+ <observability-container
+ :oauth-url="oauthUrl"
+ :tracing-url="tracingUrl"
+ :provisioning-url="provisioningUrl"
+ >
+ <template #default="{ observabilityClient }">
+ <tracing-details
+ :trace-id="traceId"
+ :tracing-index-url="tracingIndexUrl"
+ :observability-client="observabilityClient"
+ />
+ </template>
+ </observability-container>
+</template>
diff --git a/app/graphql/mutations/work_items/subscribe.rb b/app/graphql/mutations/work_items/subscribe.rb
new file mode 100644
index 00000000000..a29c3416c3d
--- /dev/null
+++ b/app/graphql/mutations/work_items/subscribe.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Mutations
+ module WorkItems
+ class Subscribe < BaseMutation
+ graphql_name 'WorkItemSubscribe'
+
+ argument :id, ::Types::GlobalIDType[::WorkItem],
+ required: true,
+ description: 'Global ID of the work item.'
+
+ argument :subscribed,
+ GraphQL::Types::Boolean,
+ required: true,
+ description: 'Desired state of the subscription.'
+
+ field :work_item, Types::WorkItemType,
+ null: true,
+ description: 'Work item after mutation.'
+
+ authorize :update_subscription
+
+ def resolve(args)
+ work_item = authorized_find!(id: args[:id])
+
+ update_subscription(work_item, args[:subscribed])
+
+ {
+ work_item: work_item,
+ errors: []
+ }
+ end
+
+ private
+
+ def update_subscription(work_item, subscribed_state)
+ work_item.set_subscription(current_user, subscribed_state, work_item.project)
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 16c46d172f3..9a6b3861103 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -183,6 +183,7 @@ module Types
mount_mutation Mutations::SavedReplies::Destroy
mount_mutation Mutations::Uploads::Delete
mount_mutation Mutations::Users::SetNamespaceCommitEmail
+ mount_mutation Mutations::WorkItems::Subscribe, alpha: { milestone: '16.3' }
end
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index 291375f647c..8e5f3f030fb 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -95,7 +95,7 @@ class ApplicationRecord < ActiveRecord::Base
end
def self.underscore
- Gitlab::SafeRequestStore.fetch("model:#{self}:underscore") { to_s.underscore }
+ @underscore ||= to_s.underscore
end
def self.where_exists(query)
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 2f2e731fc7e..d159b51a0eb 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -225,6 +225,10 @@ class Todo < ApplicationRecord
action == MEMBER_ACCESS_REQUESTED
end
+ def review_submitted?
+ action == REVIEW_SUBMITTED
+ end
+
def member_access_type
target.class.name.downcase
end
diff --git a/app/services/ci/pipeline_schedules/base_save_service.rb b/app/services/ci/pipeline_schedules/base_save_service.rb
new file mode 100644
index 00000000000..45d70e5a65d
--- /dev/null
+++ b/app/services/ci/pipeline_schedules/base_save_service.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Ci
+ module PipelineSchedules
+ class BaseSaveService
+ include Gitlab::Utils::StrongMemoize
+
+ def execute
+ schedule.assign_attributes(params)
+
+ return forbidden_to_save unless allowed_to_save?
+ return forbidden_to_save_variables unless allowed_to_save_variables?
+
+ if schedule.save
+ ServiceResponse.success(payload: schedule)
+ else
+ ServiceResponse.error(payload: schedule, message: schedule.errors.full_messages)
+ end
+ end
+
+ private
+
+ attr_reader :project, :user, :params, :schedule
+
+ def allowed_to_save?
+ user.can?(self.class::AUTHORIZE, schedule)
+ end
+
+ def forbidden_to_save
+ # We add the error to the base object too
+ # because model errors are used in the API responses and the `form_errors` helper.
+ schedule.errors.add(:base, authorize_message)
+
+ ServiceResponse.error(payload: schedule, message: [authorize_message], reason: :forbidden)
+ end
+
+ def allowed_to_save_variables?
+ return true if params[:variables_attributes].blank?
+
+ user.can?(:set_pipeline_variables, project)
+ end
+
+ def forbidden_to_save_variables
+ message = _('The current user is not authorized to set pipeline schedule variables')
+
+ # We add the error to the base object too
+ # because model errors are used in the API responses and the `form_errors` helper.
+ schedule.errors.add(:base, message)
+
+ ServiceResponse.error(payload: schedule, message: [message], reason: :forbidden)
+ end
+ end
+ end
+end
diff --git a/app/services/ci/pipeline_schedules/create_service.rb b/app/services/ci/pipeline_schedules/create_service.rb
index c1825865bc0..23775e68399 100644
--- a/app/services/ci/pipeline_schedules/create_service.rb
+++ b/app/services/ci/pipeline_schedules/create_service.rb
@@ -2,46 +2,22 @@
module Ci
module PipelineSchedules
- class CreateService
- def initialize(project, user, params)
- @project = project
- @user = user
- @params = params
+ class CreateService < BaseSaveService
+ AUTHORIZE = :create_pipeline_schedule
+ def initialize(project, user, params)
@schedule = project.pipeline_schedules.new
- end
-
- def execute
- return forbidden unless allowed?
-
- schedule.assign_attributes(params.merge(owner: user))
-
- if schedule.save
- ServiceResponse.success(payload: schedule)
- else
- ServiceResponse.error(payload: schedule, message: schedule.errors.full_messages)
- end
+ @user = user
+ @project = project
+ @params = params.merge(owner: user)
end
private
- attr_reader :project, :user, :params, :schedule
-
- def allowed?
- user.can?(:create_pipeline_schedule, schedule)
- end
-
- def forbidden
- # We add the error to the base object too
- # because model errors are used in the API responses and the `form_errors` helper.
- schedule.errors.add(:base, forbidden_message)
-
- ServiceResponse.error(payload: schedule, message: [forbidden_message], reason: :forbidden)
- end
-
- def forbidden_message
+ def authorize_message
_('The current user is not authorized to create the pipeline schedule')
end
+ strong_memoize_attr :authorize_message
end
end
end
diff --git a/app/services/ci/pipeline_schedules/update_service.rb b/app/services/ci/pipeline_schedules/update_service.rb
index 28c22e0a868..2fd1173ecce 100644
--- a/app/services/ci/pipeline_schedules/update_service.rb
+++ b/app/services/ci/pipeline_schedules/update_service.rb
@@ -2,44 +2,22 @@
module Ci
module PipelineSchedules
- class UpdateService
+ class UpdateService < BaseSaveService
+ AUTHORIZE = :update_pipeline_schedule
+
def initialize(schedule, user, params)
@schedule = schedule
@user = user
+ @project = schedule.project
@params = params
end
- def execute
- return forbidden unless allowed?
-
- schedule.assign_attributes(params)
-
- if schedule.save
- ServiceResponse.success(payload: schedule)
- else
- ServiceResponse.error(message: schedule.errors.full_messages)
- end
- end
-
private
- attr_reader :schedule, :user, :params
-
- def allowed?
- user.can?(:update_pipeline_schedule, schedule)
- end
-
- def forbidden
- # We add the error to the base object too
- # because model errors are used in the API responses and the `form_errors` helper.
- schedule.errors.add(:base, forbidden_message)
-
- ServiceResponse.error(message: [forbidden_message], reason: :forbidden)
- end
-
- def forbidden_message
+ def authorize_message
_('The current user is not authorized to update the pipeline schedule')
end
+ strong_memoize_attr :authorize_message
end
end
end
diff --git a/app/services/todos/destroy/group_private_service.rb b/app/services/todos/destroy/group_private_service.rb
index d7ecbb952aa..60599ca9ca4 100644
--- a/app/services/todos/destroy/group_private_service.rb
+++ b/app/services/todos/destroy/group_private_service.rb
@@ -24,7 +24,10 @@ module Todos
override :authorized_users
def authorized_users
- group.direct_and_indirect_users.select(:id)
+ User.from_union([
+ group.project_users_with_descendants.select(:id),
+ group.members_with_parents.select(:user_id)
+ ], remove_duplicates: false)
end
override :todos_to_remove?
diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml
index 893271ea982..4ecef4b76ce 100644
--- a/app/views/clusters/clusters/user/_form.html.haml
+++ b/app/views/clusters/clusters/user/_form.html.haml
@@ -47,7 +47,7 @@
.form-group
.form-check
- = platform_kubernetes_field.check_box :authorization_type, { data: { qa_selector: 'rbac_checkbox'}, inline: true, class: 'form-check-input' }, 'rbac', 'abac'
+ = platform_kubernetes_field.check_box :authorization_type, { inline: true, class: 'form-check-input' }, 'rbac', 'abac'
= platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold'
%small.form-text.text-muted
= '%{help_text} %{help_link}'.html_safe % { help_text: rbac_help_text, help_link: rbac_help_link }
@@ -73,4 +73,4 @@
= render('clusters/clusters/namespace', platform_field: platform_kubernetes_field)
.form-group
- = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), pajamas_button: true, data: { qa_selector: 'add_kubernetes_cluster_button' }
+ = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), pajamas_button: true
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index e20fccc218a..1cd8015934e 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -48,6 +48,7 @@
= first_line_in_markdown(todo, :body, 125, is_todo: true, project: todo.project, group: todo.group)
= render_if_exists "dashboard/todos/diff_summary", local_assigns: { todo: todo }
+ = render_if_exists "dashboard/todos/review_summary", local_assigns: { todo: todo }
.todo-timestamp.gl-white-space-nowrap.gl-sm-ml-3.gl-mt-2.gl-mb-2.gl-sm-my-0.gl-px-2.gl-sm-px-0
%span.todo-timestamp.gl-font-sm.gl-text-secondary
diff --git a/db/migrate/20230714020854_add_name_and_description_to_member_roles.rb b/db/migrate/20230714020854_add_name_and_description_to_member_roles.rb
new file mode 100644
index 00000000000..eda87babea1
--- /dev/null
+++ b/db/migrate/20230714020854_add_name_and_description_to_member_roles.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AddNameAndDescriptionToMemberRoles < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ def up
+ with_lock_retries do
+ add_column :member_roles, :name, :text, null: false, default: 'Custom', if_not_exists: true
+ add_column :member_roles, :description, :text, if_not_exists: true
+ end
+
+ add_text_limit :member_roles, :name, 255
+ add_text_limit :member_roles, :description, 255
+ end
+
+ def down
+ with_lock_retries do
+ remove_column :member_roles, :name, :text, if_exists: true
+ remove_column :member_roles, :description, :text, if_exists: true
+ end
+ end
+end
diff --git a/db/schema_migrations/20230714020854 b/db/schema_migrations/20230714020854
new file mode 100644
index 00000000000..17efaee93da
--- /dev/null
+++ b/db/schema_migrations/20230714020854
@@ -0,0 +1 @@
+1c6d77c77c42a47bdc885e5cc8e0aef11a47d1a1e27a405cd1ccc8d938bf13b6 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 93e3ad40756..6842d465ce2 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -17968,7 +17968,11 @@ CREATE TABLE member_roles (
read_code boolean DEFAULT false,
read_vulnerability boolean DEFAULT false NOT NULL,
admin_vulnerability boolean DEFAULT false NOT NULL,
- read_dependency boolean DEFAULT false NOT NULL
+ read_dependency boolean DEFAULT false NOT NULL,
+ name text DEFAULT 'Custom'::text NOT NULL,
+ description text,
+ CONSTRAINT check_4364846f58 CHECK ((char_length(description) <= 255)),
+ CONSTRAINT check_9907916995 CHECK ((char_length(name) <= 255))
);
CREATE SEQUENCE member_roles_id_seq
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 62b8907e99f..21c984a135f 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -7335,6 +7335,30 @@ Input type: `WorkItemExportInput`
| <a id="mutationworkitemexporterrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationworkitemexportmessage"></a>`message` | [`String`](#string) | Export request result message. |
+### `Mutation.workItemSubscribe`
+
+WARNING:
+**Introduced** in 16.3.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Input type: `WorkItemSubscribeInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationworkitemsubscribeclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationworkitemsubscribeid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
+| <a id="mutationworkitemsubscribesubscribed"></a>`subscribed` | [`Boolean!`](#boolean) | Desired state of the subscription. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationworkitemsubscribeclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationworkitemsubscribeerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+| <a id="mutationworkitemsubscribeworkitem"></a>`workItem` | [`WorkItem`](#workitem) | Work item after mutation. |
+
### `Mutation.workItemUpdate`
Updates a work item by Global ID.
diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md
index 448bd72df54..8c23c6b2fc9 100644
--- a/doc/ci/environments/index.md
+++ b/doc/ci/environments/index.md
@@ -343,7 +343,7 @@ the job's `script`.
If there is a problem with a deployment, you can retry it or roll it back.
-To retry or rollback a deployment:
+To retry or roll back a deployment:
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
1. Select **Operate > Environments**.
diff --git a/doc/security/rate_limits.md b/doc/security/rate_limits.md
index 5353c11d2f1..ee10d66a8ad 100644
--- a/doc/security/rate_limits.md
+++ b/doc/security/rate_limits.md
@@ -133,6 +133,14 @@ There is a rate limit for the GraphQL `aiAction` mutation, which is enforced to
The **rate limit** is 160 calls per 8 hours per authenticated user.
+### Delete a member using the API
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118296) in GitLab 16.0.
+
+There is a rate limit for [removing project or group members using the API endpoints](../api/members.md#remove-a-member-from-a-group-or-project) `/groups/:id/members` or `/project/:id/members`.
+
+The **rate limit** is 60 deletions per minute.
+
## Troubleshooting
### Rack Attack is denylisting the load balancer
diff --git a/generator_templates/gitlab_internal_events/event_definition.yml b/generator_templates/gitlab_internal_events/event_definition.yml
index 26adc55726e..0c5399a3d1b 100644
--- a/generator_templates/gitlab_internal_events/event_definition.yml
+++ b/generator_templates/gitlab_internal_events/event_definition.yml
@@ -1,6 +1,6 @@
---
description: <%= args.last %>
-category: GitlabInternalEvents
+category: InternalEventTracking
action: <%= event %>
label_description:
property_description:
diff --git a/lib/gitlab/authorized_keys.rb b/lib/gitlab/authorized_keys.rb
index 3e529a0d2f3..00d9480334e 100644
--- a/lib/gitlab/authorized_keys.rb
+++ b/lib/gitlab/authorized_keys.rb
@@ -153,7 +153,7 @@ module Gitlab
end
def command(id)
- unless /\A[a-z0-9-]+\z/ =~ id
+ unless /\A[a-z0-9-]+\z/.match?(id)
raise KeyError, "Invalid ID: #{id.inspect}"
end
diff --git a/lib/gitlab/checks/branch_check.rb b/lib/gitlab/checks/branch_check.rb
index ba7662cd13d..3bc880ae8f7 100644
--- a/lib/gitlab/checks/branch_check.rb
+++ b/lib/gitlab/checks/branch_check.rb
@@ -43,7 +43,7 @@ module Gitlab
def prohibited_branch_checks
return if deletion?
- if branch_name =~ %r{\A#{Gitlab::Git::Commit::RAW_FULL_SHA_PATTERN}(/|\z)}o
+ if %r{\A#{Gitlab::Git::Commit::RAW_FULL_SHA_PATTERN}(/|\z)}o.match?(branch_name)
raise GitAccess::ForbiddenError, ERROR_MESSAGES[:prohibited_hex_branch_name]
end
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
index 88d624503df..d0ab4916c90 100644
--- a/lib/gitlab/ci/build/artifacts/metadata.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -70,8 +70,8 @@ module Gitlab
# abort if we don't get any data.
next unless path && meta
next unless path.valid_encoding? && meta.valid_encoding?
- next unless path =~ match_pattern
- next if path =~ INVALID_PATH_PATTERN
+ next unless match_pattern.match?(path)
+ next if INVALID_PATH_PATTERN.match?(path)
entries[path] = Gitlab::Json.parse(meta, symbolize_names: true)
rescue JSON::ParserError, Encoding::CompatibilityError
@@ -90,7 +90,7 @@ module Gitlab
raise ParserError, 'Artifacts metadata file empty!'
end
- unless version_string =~ VERSION_PATTERN
+ unless VERSION_PATTERN.match?(version_string)
raise ParserError, 'Invalid version!'
end
diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
index 355fffbf9c6..23db15d58b6 100644
--- a/lib/gitlab/ci/build/artifacts/metadata/entry.rb
+++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
@@ -102,7 +102,7 @@ module Gitlab
def total_size
descendant_pattern = /^#{Regexp.escape(@path.to_s)}/
entries.sum do |path, entry|
- (entry[:size] if path =~ descendant_pattern).to_i
+ (entry[:size] if descendant_pattern.match?(path)).to_i
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/gitlab/ci/project_config/remote.rb b/lib/gitlab/ci/project_config/remote.rb
index 19cbf8e9c1e..23ed510e378 100644
--- a/lib/gitlab/ci/project_config/remote.rb
+++ b/lib/gitlab/ci/project_config/remote.rb
@@ -6,7 +6,7 @@ module Gitlab
class Remote < Source
def content
strong_memoize(:content) do
- next unless ci_config_path =~ URI::DEFAULT_PARSER.make_regexp(%w[http https])
+ next unless URI::DEFAULT_PARSER.make_regexp(%w[http https]).match?(ci_config_path)
YAML.dump('include' => [{ 'remote' => ci_config_path }])
end
diff --git a/lib/gitlab/ci/reports/sbom/component.rb b/lib/gitlab/ci/reports/sbom/component.rb
index a516f4d02f1..51fd6af7bc4 100644
--- a/lib/gitlab/ci/reports/sbom/component.rb
+++ b/lib/gitlab/ci/reports/sbom/component.rb
@@ -5,12 +5,14 @@ module Gitlab
module Reports
module Sbom
class Component
- attr_reader :component_type, :name, :version
+ include Gitlab::Utils::StrongMemoize
+
+ attr_reader :component_type, :version
def initialize(type:, name:, purl:, version:)
@component_type = type
@name = name
- @purl = purl
+ @raw_purl = purl
@version = version
end
@@ -23,9 +25,16 @@ module Gitlab
end
def purl
- return unless @purl
+ return unless @raw_purl
+
+ ::Sbom::PackageUrl.parse(@raw_purl)
+ end
+ strong_memoize_attr :purl
+
+ def name
+ return @name unless purl
- ::Sbom::PackageUrl.parse(@purl)
+ [purl.namespace, purl.name].compact.join('/')
end
private
diff --git a/lib/gitlab/content_security_policy/config_loader.rb b/lib/gitlab/content_security_policy/config_loader.rb
index 8d1fcf4f916..9fb3c7d362f 100644
--- a/lib/gitlab/content_security_policy/config_loader.rb
+++ b/lib/gitlab/content_security_policy/config_loader.rb
@@ -11,194 +11,213 @@ module Gitlab
DEFAULT_FALLBACK_VALUE = '<default_value>'
HTTP_PORTS = [80, 443].freeze
- def self.default_enabled
- Rails.env.development? || Rails.env.test?
- end
+ class << self
+ def default_enabled
+ Rails.env.development? || Rails.env.test?
+ end
- def self.default_directives
- directives = default_directives_defaults
+ def default_directives
+ directives = default_directives_defaults
+
+ allow_development_tooling(directives)
+ allow_websocket_connections(directives)
+ allow_lfs(directives)
+ allow_cdn(directives)
+ allow_zuora(directives)
+ allow_sentry(directives)
+ allow_framed_gitlab_paths(directives)
+ allow_customersdot(directives)
+ allow_review_apps(directives)
+ csp_level_3_backport(directives)
+
+ directives
+ end
- allow_development_tooling(directives)
- allow_websocket_connections(directives)
- allow_cdn(directives)
- allow_zuora(directives)
- allow_sentry(directives)
- allow_framed_gitlab_paths(directives)
- allow_customersdot(directives)
- allow_review_apps(directives)
- csp_level_3_backport(directives)
+ def default_directives_defaults
+ {
+ 'default_src' => "'self'",
+ 'base_uri' => "'self'",
+ 'connect_src' => ContentSecurityPolicy::Directives.connect_src,
+ 'font_src' => "'self'",
+ 'form_action' => "'self' https: http:",
+ 'frame_ancestors' => "'self'",
+ 'frame_src' => ContentSecurityPolicy::Directives.frame_src,
+ 'img_src' => "'self' data: blob: http: https:",
+ 'manifest_src' => "'self'",
+ 'media_src' => "'self' data: blob: http: https:",
+ 'script_src' => ContentSecurityPolicy::Directives.script_src,
+ 'style_src' => ContentSecurityPolicy::Directives.style_src,
+ 'worker_src' => "#{Gitlab::Utils.append_path(Gitlab.config.gitlab.url, 'assets/')} blob: data:",
+ 'object_src' => "'none'",
+ 'report_uri' => nil
+ }
+ end
- directives
- end
+ # connect_src with 'self' includes https/wss variations of the origin,
+ # however, safari hasn't covered this yet and we need to explicitly add
+ # support for websocket origins until Safari catches up with the specs
+ def allow_development_tooling(directives)
+ return unless Rails.env.development?
- def initialize(csp_directives)
- # Using <default_value> falls back to the default values.
- directives = csp_directives.reject { |_, value| value == DEFAULT_FALLBACK_VALUE }
- @merged_csp_directives =
- HashWithIndifferentAccess.new(directives)
- .reverse_merge(::Gitlab::ContentSecurityPolicy::ConfigLoader.default_directives)
- end
+ allow_webpack_dev_server(directives)
+ allow_letter_opener(directives)
+ allow_snowplow_micro(directives) if Gitlab::Tracking.snowplow_micro_enabled?
+ end
- def load(policy)
- DIRECTIVES.each do |directive|
- arguments = arguments_for(directive)
+ def allow_webpack_dev_server(directives)
+ secure = Settings.webpack.dev_server['https']
+ host_and_port = "#{Settings.webpack.dev_server['host']}:#{Settings.webpack.dev_server['port']}"
+ http_url = "#{secure ? 'https' : 'http'}://#{host_and_port}"
+ ws_url = "#{secure ? 'wss' : 'ws'}://#{host_and_port}"
- next unless arguments.present?
+ append_to_directive(directives, 'connect_src', "#{http_url} #{ws_url}")
+ end
- policy.public_send(directive, *arguments) # rubocop:disable GitlabSecurity/PublicSend
+ def allow_letter_opener(directives)
+ url = Gitlab::Utils.append_path(Gitlab.config.gitlab.url, '/rails/letter_opener/')
+ append_to_directive(directives, 'frame_src', url)
end
- end
- private
+ def allow_snowplow_micro(directives)
+ url = URI.join(Gitlab::Tracking::Destinations::SnowplowMicro.new.uri, '/').to_s
+ append_to_directive(directives, 'connect_src', url)
+ end
- def self.default_directives_defaults
- {
- 'default_src' => "'self'",
- 'base_uri' => "'self'",
- 'connect_src' => ContentSecurityPolicy::Directives.connect_src,
- 'font_src' => "'self'",
- 'form_action' => "'self' https: http:",
- 'frame_ancestors' => "'self'",
- 'frame_src' => ContentSecurityPolicy::Directives.frame_src,
- 'img_src' => "'self' data: blob: http: https:",
- 'manifest_src' => "'self'",
- 'media_src' => "'self' data: blob: http: https:",
- 'script_src' => ContentSecurityPolicy::Directives.script_src,
- 'style_src' => ContentSecurityPolicy::Directives.style_src,
- 'worker_src' => "#{Gitlab::Utils.append_path(Gitlab.config.gitlab.url, 'assets/')} blob: data:",
- 'object_src' => "'none'",
- 'report_uri' => nil
- }
- end
+ def allow_lfs(directives)
+ return unless Gitlab.config.lfs.enabled && LfsObjectUploader.direct_download_enabled?
- # connect_src with 'self' includes https/wss variations of the origin,
- # however, safari hasn't covered this yet and we need to explicitly add
- # support for websocket origins until Safari catches up with the specs
- def self.allow_development_tooling(directives)
- return unless Rails.env.development?
+ lfs_url = build_lfs_url
+ return unless lfs_url.present?
- allow_webpack_dev_server(directives)
- allow_letter_opener(directives)
- allow_snowplow_micro(directives) if Gitlab::Tracking.snowplow_micro_enabled?
- end
+ append_to_directive(directives, 'connect_src', lfs_url)
+ end
- def self.allow_webpack_dev_server(directives)
- secure = Settings.webpack.dev_server['https']
- host_and_port = "#{Settings.webpack.dev_server['host']}:#{Settings.webpack.dev_server['port']}"
- http_url = "#{secure ? 'https' : 'http'}://#{host_and_port}"
- ws_url = "#{secure ? 'wss' : 'ws'}://#{host_and_port}"
+ def allow_websocket_connections(directives)
+ host = Gitlab.config.gitlab.host
+ port = Gitlab.config.gitlab.port
+ secure = Gitlab.config.gitlab.https
+ protocol = secure ? 'wss' : 'ws'
- append_to_directive(directives, 'connect_src', "#{http_url} #{ws_url}")
- end
+ ws_url = "#{protocol}://#{host}"
+ ws_url = "#{ws_url}:#{port}" unless HTTP_PORTS.include?(port)
- def self.allow_letter_opener(directives)
- url = Gitlab::Utils.append_path(Gitlab.config.gitlab.url, '/rails/letter_opener/')
- append_to_directive(directives, 'frame_src', url)
- end
+ append_to_directive(directives, 'connect_src', ws_url)
+ end
- def self.allow_snowplow_micro(directives)
- url = URI.join(Gitlab::Tracking::Destinations::SnowplowMicro.new.uri, '/').to_s
- append_to_directive(directives, 'connect_src', url)
- end
+ def allow_cdn(directives)
+ cdn_host = Settings.gitlab.cdn_host.presence
+ return unless cdn_host
- def self.allow_websocket_connections(directives)
- host = Gitlab.config.gitlab.host
- port = Gitlab.config.gitlab.port
- secure = Gitlab.config.gitlab.https
- protocol = secure ? 'wss' : 'ws'
+ append_to_directive(directives, 'script_src', cdn_host)
+ append_to_directive(directives, 'style_src', cdn_host)
+ append_to_directive(directives, 'font_src', cdn_host)
+ append_to_directive(directives, 'worker_src', cdn_host)
+ append_to_directive(directives, 'frame_src', cdn_host)
+ end
- ws_url = "#{protocol}://#{host}"
- ws_url = "#{ws_url}:#{port}" unless HTTP_PORTS.include?(port)
+ def allow_zuora(directives)
+ return unless Gitlab.com?
- append_to_directive(directives, 'connect_src', ws_url)
- end
+ append_to_directive(directives, 'frame_src', zuora_host)
+ end
- def self.allow_cdn(directives)
- cdn_host = Settings.gitlab.cdn_host.presence
- return unless cdn_host
+ def allow_sentry(directives)
+ allow_legacy_sentry(directives) if legacy_sentry_configured?
+ return unless sentry_client_side_dsn_enabled?
- append_to_directive(directives, 'script_src', cdn_host)
- append_to_directive(directives, 'style_src', cdn_host)
- append_to_directive(directives, 'font_src', cdn_host)
- append_to_directive(directives, 'worker_src', cdn_host)
- append_to_directive(directives, 'frame_src', cdn_host)
- end
+ sentry_uri = URI(Gitlab::CurrentSettings.sentry_clientside_dsn)
- def self.allow_zuora(directives)
- return unless Gitlab.com?
+ append_to_directive(directives, 'connect_src', "#{sentry_uri.scheme}://#{sentry_uri.host}")
+ end
- append_to_directive(directives, 'frame_src', zuora_host)
- end
+ def allow_legacy_sentry(directives)
+ # Support for Sentry setup via configuration files will be removed in 16.0
+ # in favor of Gitlab::CurrentSettings.
+ sentry_uri = URI(Gitlab.config.sentry.clientside_dsn)
- def self.allow_sentry(directives)
- allow_legacy_sentry(directives) if legacy_sentry_configured?
- return unless sentry_client_side_dsn_enabled?
+ append_to_directive(directives, 'connect_src', "#{sentry_uri.scheme}://#{sentry_uri.host}")
+ end
- sentry_uri = URI(Gitlab::CurrentSettings.sentry_clientside_dsn)
+ def legacy_sentry_configured?
+ Gitlab.config.sentry&.enabled && Gitlab.config.sentry&.clientside_dsn
+ end
- append_to_directive(directives, 'connect_src', "#{sentry_uri.scheme}://#{sentry_uri.host}")
- end
+ def sentry_client_side_dsn_enabled?
+ Gitlab::CurrentSettings.try(:sentry_enabled) && Gitlab::CurrentSettings.try(:sentry_clientside_dsn)
+ end
- def self.allow_legacy_sentry(directives)
- # Support for Sentry setup via configuration files will be removed in 16.0
- # in favor of Gitlab::CurrentSettings.
- sentry_uri = URI(Gitlab.config.sentry.clientside_dsn)
+ # Using 'self' in the CSP introduces several CSP bypass opportunities
+ # for this reason we list the URLs where GitLab frames itself instead
+ def allow_framed_gitlab_paths(directives)
+ ['/admin/', '/assets/', '/-/speedscope/index.html', '/-/sandbox/'].map do |path|
+ append_to_directive(directives, 'frame_src', Gitlab::Utils.append_path(Gitlab.config.gitlab.url, path))
+ end
+ end
- append_to_directive(directives, 'connect_src', "#{sentry_uri.scheme}://#{sentry_uri.host}")
- end
+ def allow_customersdot(directives)
+ customersdot_host = ENV['CUSTOMER_PORTAL_URL'].presence
+ return unless customersdot_host
- def self.legacy_sentry_configured?
- Gitlab.config.sentry&.enabled && Gitlab.config.sentry&.clientside_dsn
- end
+ append_to_directive(directives, 'frame_src', customersdot_host)
+ end
- def self.sentry_client_side_dsn_enabled?
- Gitlab::CurrentSettings.try(:sentry_enabled) && Gitlab::CurrentSettings.try(:sentry_clientside_dsn)
- end
+ def allow_review_apps(directives)
+ return unless ENV['REVIEW_APPS_ENABLED'].presence
- # Using 'self' in the CSP introduces several CSP bypass opportunities
- # for this reason we list the URLs where GitLab frames itself instead
- def self.allow_framed_gitlab_paths(directives)
- ['/admin/', '/assets/', '/-/speedscope/index.html', '/-/sandbox/'].map do |path|
- append_to_directive(directives, 'frame_src', Gitlab::Utils.append_path(Gitlab.config.gitlab.url, path))
+ # Allow-listed to allow POSTs to https://gitlab.com/api/v4/projects/278964/merge_requests/:merge_request_iid/visual_review_discussions
+ append_to_directive(directives, 'connect_src', 'https://gitlab.com/api/v4/projects/278964/merge_requests/')
end
- end
- def self.allow_customersdot(directives)
- customersdot_host = ENV['CUSTOMER_PORTAL_URL'].presence
- return unless customersdot_host
+ # The follow contains workarounds to patch Safari's lack of support for CSP Level 3
+ def csp_level_3_backport(directives)
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/343579
+ # frame-src was deprecated in CSP level 2 in favor of child-src
+ # CSP level 3 "undeprecated" frame-src and browsers fall back on child-src if it's missing
+ # However Safari seems to read child-src first so we'll just keep both equal
+ append_to_directive(directives, 'child_src', directives['frame_src'])
+
+ # Safari also doesn't support worker-src and only checks child-src
+ # So for compatibility until it catches up to other browsers we need to
+ # append worker-src's content to child-src
+ append_to_directive(directives, 'child_src', directives['worker_src'])
+ end
- append_to_directive(directives, 'frame_src', customersdot_host)
- end
+ def append_to_directive(directives, directive, text)
+ directives[directive] = "#{directives[directive]} #{text}".strip
+ end
- def self.allow_review_apps(directives)
- return unless ENV['REVIEW_APPS_ENABLED'].presence
+ def zuora_host
+ "https://*.zuora.com/apps/PublicHostedPageLite.do"
+ end
- # Allow-listed to allow POSTs to https://gitlab.com/api/v4/projects/278964/merge_requests/:merge_request_iid/visual_review_discussions
- append_to_directive(directives, 'connect_src', 'https://gitlab.com/api/v4/projects/278964/merge_requests/')
+ def build_lfs_url
+ uploader = LfsObjectUploader.new(nil)
+ fog = CarrierWave::Storage::Fog.new(uploader)
+ fog_file = CarrierWave::Storage::Fog::File.new(uploader, fog, nil)
+ fog_file.public_url || fog_file.url
+ end
end
- # The follow contains workarounds to patch Safari's lack of support for CSP Level 3
- def self.csp_level_3_backport(directives)
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/343579
- # frame-src was deprecated in CSP level 2 in favor of child-src
- # CSP level 3 "undeprecated" frame-src and browsers fall back on child-src if it's missing
- # However Safari seems to read child-src first so we'll just keep both equal
- append_to_directive(directives, 'child_src', directives['frame_src'])
-
- # Safari also doesn't support worker-src and only checks child-src
- # So for compatibility until it catches up to other browsers we need to
- # append worker-src's content to child-src
- append_to_directive(directives, 'child_src', directives['worker_src'])
+ def initialize(csp_directives)
+ # Using <default_value> falls back to the default values.
+ @merged_csp_directives = csp_directives
+ .reject { |_, value| value == DEFAULT_FALLBACK_VALUE }
+ .with_indifferent_access
+ .reverse_merge(ConfigLoader.default_directives)
end
- def self.append_to_directive(directives, directive, text)
- directives[directive] = "#{directives[directive]} #{text}".strip
- end
+ def load(policy)
+ DIRECTIVES.each do |directive|
+ arguments = arguments_for(directive)
+
+ next unless arguments.present?
- def self.zuora_host
- "https://*.zuora.com/apps/PublicHostedPageLite.do"
+ policy.public_send(directive, *arguments) # rubocop:disable GitlabSecurity/PublicSend
+ end
end
+ private
+
def arguments_for(directive)
# In order to disable a directive, the user can explicitly
# set a falsy value like nil, false or empty string
diff --git a/lib/gitlab/database/postgres_constraint.rb b/lib/gitlab/database/postgres_constraint.rb
index fa3870cb9c7..9c49251664b 100644
--- a/lib/gitlab/database/postgres_constraint.rb
+++ b/lib/gitlab/database/postgres_constraint.rb
@@ -17,7 +17,7 @@ module Gitlab
scope :valid, -> { where(constraint_valid: true) }
scope :by_table_identifier, ->(identifier) do
- unless identifier =~ Gitlab::Database::FULLY_QUALIFIED_IDENTIFIER
+ unless Gitlab::Database::FULLY_QUALIFIED_IDENTIFIER.match?(identifier)
raise ArgumentError, "Table name is not fully qualified with a schema: #{identifier}"
end
diff --git a/lib/gitlab/database/postgres_foreign_key.rb b/lib/gitlab/database/postgres_foreign_key.rb
index bb3e1d45f15..9fb3098efe0 100644
--- a/lib/gitlab/database/postgres_foreign_key.rb
+++ b/lib/gitlab/database/postgres_foreign_key.rb
@@ -21,7 +21,7 @@ module Gitlab
enum on_update_action: ACTION_TYPES, _prefix: :on_update
scope :by_referenced_table_identifier, ->(identifier) do
- unless identifier =~ Database::FULLY_QUALIFIED_IDENTIFIER
+ unless Database::FULLY_QUALIFIED_IDENTIFIER.match?(identifier)
raise ArgumentError, "Referenced table name is not fully qualified with a schema: #{identifier}"
end
@@ -31,7 +31,7 @@ module Gitlab
scope :by_referenced_table_name, ->(name) { where(referenced_table_name: name) }
scope :by_constrained_table_identifier, ->(identifier) do
- unless identifier =~ Database::FULLY_QUALIFIED_IDENTIFIER
+ unless Database::FULLY_QUALIFIED_IDENTIFIER.match?(identifier)
raise ArgumentError, "Constrained table name is not fully qualified with a schema: #{identifier}"
end
@@ -41,7 +41,7 @@ module Gitlab
scope :by_constrained_table_name, ->(name) { where(constrained_table_name: name) }
scope :by_constrained_table_name_or_identifier, ->(name) do
- if name =~ Database::FULLY_QUALIFIED_IDENTIFIER
+ if Database::FULLY_QUALIFIED_IDENTIFIER.match?(name)
by_constrained_table_identifier(name)
else
by_constrained_table_name(name)
diff --git a/lib/gitlab/database/postgres_index.rb b/lib/gitlab/database/postgres_index.rb
index 50009cadf5d..1c775482e7e 100644
--- a/lib/gitlab/database/postgres_index.rb
+++ b/lib/gitlab/database/postgres_index.rb
@@ -14,7 +14,7 @@ module Gitlab
has_many :queued_reindexing_actions, class_name: 'Gitlab::Database::Reindexing::QueuedAction', foreign_key: :index_identifier
scope :by_identifier, ->(identifier) do
- unless identifier =~ Gitlab::Database::FULLY_QUALIFIED_IDENTIFIER
+ unless Gitlab::Database::FULLY_QUALIFIED_IDENTIFIER.match?(identifier)
raise ArgumentError, "Index name is not fully qualified with a schema: #{identifier}"
end
diff --git a/lib/gitlab/database/postgres_partition.rb b/lib/gitlab/database/postgres_partition.rb
index e63c6fc86ea..f79b8b5e32c 100644
--- a/lib/gitlab/database/postgres_partition.rb
+++ b/lib/gitlab/database/postgres_partition.rb
@@ -10,7 +10,7 @@ module Gitlab
# identifier includes the partition schema.
# For example 'gitlab_partitions_static.events_03', or 'gitlab_partitions_dynamic.logs_03'
scope :for_identifier, ->(identifier) do
- unless identifier =~ Gitlab::Database::FULLY_QUALIFIED_IDENTIFIER
+ unless Gitlab::Database::FULLY_QUALIFIED_IDENTIFIER.match?(identifier)
raise ArgumentError, "Partition name is not fully qualified with a schema: #{identifier}"
end
@@ -22,7 +22,7 @@ module Gitlab
end
scope :for_parent_table, ->(parent_table) do
- if parent_table =~ Database::FULLY_QUALIFIED_IDENTIFIER
+ if Database::FULLY_QUALIFIED_IDENTIFIER.match?(parent_table)
where(parent_identifier: parent_table).order(:name)
else
where("parent_identifier = concat(current_schema(), '.', ?)", parent_table).order(:name)
diff --git a/lib/gitlab/database/postgres_partitioned_table.rb b/lib/gitlab/database/postgres_partitioned_table.rb
index fead7379e43..76e2cd48f80 100644
--- a/lib/gitlab/database/postgres_partitioned_table.rb
+++ b/lib/gitlab/database/postgres_partitioned_table.rb
@@ -10,7 +10,7 @@ module Gitlab
has_many :postgres_partitions, foreign_key: 'parent_identifier', primary_key: 'identifier'
scope :by_identifier, ->(identifier) do
- unless identifier =~ Gitlab::Database::FULLY_QUALIFIED_IDENTIFIER
+ unless Gitlab::Database::FULLY_QUALIFIED_IDENTIFIER.match?(identifier)
raise ArgumentError, "Table name is not fully qualified with a schema: #{identifier}"
end
diff --git a/lib/gitlab/database/reindexing/reindex_concurrently.rb b/lib/gitlab/database/reindexing/reindex_concurrently.rb
index 60fa4deda39..aa445082aa9 100644
--- a/lib/gitlab/database/reindexing/reindex_concurrently.rb
+++ b/lib/gitlab/database/reindexing/reindex_concurrently.rb
@@ -20,7 +20,7 @@ module Gitlab
def perform
raise ReindexError, 'indexes serving an exclusion constraint are currently not supported' if index.exclusion?
- raise ReindexError, 'index is a left-over temporary index from a previous reindexing run' if index.name =~ /#{TEMPORARY_INDEX_PATTERN}/o
+ raise ReindexError, 'index is a left-over temporary index from a previous reindexing run' if /#{TEMPORARY_INDEX_PATTERN}/o.match?(index.name)
# Expression indexes require additional statistics in `pg_statistic`:
# select * from pg_statistic where starelid = (select oid from pg_class where relname = 'some_index');
diff --git a/lib/gitlab/dependency_linker/base_linker.rb b/lib/gitlab/dependency_linker/base_linker.rb
index 203cee1fd5e..aba6e0f033a 100644
--- a/lib/gitlab/dependency_linker/base_linker.rb
+++ b/lib/gitlab/dependency_linker/base_linker.rb
@@ -31,7 +31,7 @@ module Gitlab
end
def external_url(name, external_ref)
- return if external_ref =~ GIT_INVALID_URL_REGEX
+ return if GIT_INVALID_URL_REGEX.match?(external_ref)
case external_ref
when /\A#{URL_REGEX}\z/o
diff --git a/lib/gitlab/dependency_linker/composer_json_linker.rb b/lib/gitlab/dependency_linker/composer_json_linker.rb
index 965ed8bb95e..762d7f3e73e 100644
--- a/lib/gitlab/dependency_linker/composer_json_linker.rb
+++ b/lib/gitlab/dependency_linker/composer_json_linker.rb
@@ -13,7 +13,7 @@ module Gitlab
end
def package_url(name)
- "https://packagist.org/packages/#{name}" if name =~ /\A#{REPO_REGEX}\z/o
+ "https://packagist.org/packages/#{name}" if /\A#{REPO_REGEX}\z/o.match?(name)
end
end
end
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index a506bc3aaa2..ad901dc958b 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -25,7 +25,7 @@ module Gitlab
full_line = line.delete("\n")
- if line =~ /^@@ -/
+ if /^@@ -/.match?(line)
type = "match"
diff_hunk = Gitlab::WordDiff::Segments::DiffHunk.new(line)
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index e7462b711f1..ecacd02996d 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -54,7 +54,7 @@ module Gitlab
return "" unless decoded
# Certain trigger phrases that means we didn't parse correctly
- if decoded =~ %r{(Content\-Type\:|multipart/alternative|text/plain)}
+ if %r{(Content\-Type\:|multipart/alternative|text/plain)}.match?(decoded)
return ""
end
diff --git a/lib/gitlab/git/gitmodules_parser.rb b/lib/gitlab/git/gitmodules_parser.rb
index 92940c352d3..a5a84d79720 100644
--- a/lib/gitlab/git/gitmodules_parser.rb
+++ b/lib/gitlab/git/gitmodules_parser.rb
@@ -50,7 +50,7 @@ module Gitlab
@content.split("\n").each_with_object(iterator) do |text, iterator|
text.chomp!
- next if text =~ /^\s*#/
+ next if /^\s*#/.match?(text)
if text =~ /\A\[submodule "(?<name>[^"]+)"\]\z/
iterator.start_section($~[:name])
diff --git a/lib/gitlab/metrics/samplers/threads_sampler.rb b/lib/gitlab/metrics/samplers/threads_sampler.rb
index a460594fb59..1357e0a5d9b 100644
--- a/lib/gitlab/metrics/samplers/threads_sampler.rb
+++ b/lib/gitlab/metrics/samplers/threads_sampler.rb
@@ -54,7 +54,7 @@ module Gitlab
if thread_name.presence.nil?
'unnamed'
- elsif thread_name =~ /puma threadpool \d+/
+ elsif /puma threadpool \d+/.match?(thread_name)
# These are the puma workers processing requests
'puma threadpool'
elsif use_thread_name?(thread_name)
diff --git a/lib/gitlab/middleware/sidekiq_web_static.rb b/lib/gitlab/middleware/sidekiq_web_static.rb
index 61b5fb9e0c6..c5d2ecbe00e 100644
--- a/lib/gitlab/middleware/sidekiq_web_static.rb
+++ b/lib/gitlab/middleware/sidekiq_web_static.rb
@@ -15,7 +15,7 @@ module Gitlab
end
def call(env)
- env.delete('HTTP_X_SENDFILE_TYPE') if env['PATH_INFO'] =~ SIDEKIQ_REGEX
+ env.delete('HTTP_X_SENDFILE_TYPE') if SIDEKIQ_REGEX.match?(env['PATH_INFO'])
@app.call(env)
end
diff --git a/lib/gitlab/middleware/static.rb b/lib/gitlab/middleware/static.rb
index 972fed2134c..324d929a93d 100644
--- a/lib/gitlab/middleware/static.rb
+++ b/lib/gitlab/middleware/static.rb
@@ -6,7 +6,7 @@ module Gitlab
UPLOADS_REGEX = %r{\A/uploads(/|\z)}.freeze
def call(env)
- return @app.call(env) if env['PATH_INFO'] =~ UPLOADS_REGEX
+ return @app.call(env) if UPLOADS_REGEX.match?(env['PATH_INFO'])
super
end
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
index ba50a42cd37..31256101bd2 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -258,7 +258,7 @@ module Gitlab
def multiline_blocked?(parsed_url)
url = parsed_url.to_s
- return true if url =~ /\n|\r/
+ return true if /\n|\r/.match?(url)
# Google Cloud Storage uses a multi-line, encoded Signature query string
return false if %w(http https).include?(parsed_url.scheme&.downcase)
@@ -282,7 +282,7 @@ module Gitlab
def validate_user(value)
return if value.blank?
- return if value =~ /\A\p{Alnum}/
+ return if /\A\p{Alnum}/.match?(value)
raise BlockedUrlError, "Username needs to start with an alphanumeric character"
end
@@ -290,7 +290,7 @@ module Gitlab
def validate_hostname(value)
return if value.blank?
return if IPAddress.valid?(value)
- return if value =~ /\A\p{Alnum}/
+ return if /\A\p{Alnum}/.match?(value)
raise BlockedUrlError, "Hostname or IP address invalid"
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6f39e75156d..4914b254618 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -15528,6 +15528,9 @@ msgstr ""
msgid "Dependencies|Vulnerable components"
msgstr ""
+msgid "Dependencies|unknown"
+msgstr ""
+
msgid "Dependency List has no entries"
msgstr ""
@@ -19146,9 +19149,6 @@ msgstr ""
msgid "Failed to deploy to"
msgstr ""
-msgid "Failed to enable tracing."
-msgstr ""
-
msgid "Failed to enqueue the rebase operation, possibly due to a long-lived transaction. Try again later."
msgstr ""
@@ -19227,9 +19227,6 @@ msgstr ""
msgid "Failed to load milestones. Please try again."
msgstr ""
-msgid "Failed to load page."
-msgstr ""
-
msgid "Failed to load projects"
msgstr ""
@@ -19239,9 +19236,6 @@ msgstr ""
msgid "Failed to load stacktrace."
msgstr ""
-msgid "Failed to load traces."
-msgstr ""
-
msgid "Failed to make repository read-only. %{reason}"
msgstr ""
@@ -20840,9 +20834,6 @@ msgstr ""
msgid "Get started with GitLab"
msgstr ""
-msgid "Get started with Tracing"
-msgstr ""
-
msgid "Get started with error tracking"
msgstr ""
@@ -29924,9 +29915,6 @@ msgstr ""
msgid "Monitor Settings"
msgstr ""
-msgid "Monitor your applications with GitLab Distributed Tracing."
-msgstr ""
-
msgid "Monitor your errors directly in GitLab."
msgstr ""
@@ -31098,9 +31086,6 @@ msgstr ""
msgid "No test coverage"
msgstr ""
-msgid "No traces to display."
-msgstr ""
-
msgid "No user provided"
msgstr ""
@@ -32459,9 +32444,6 @@ msgstr ""
msgid "Opens new window"
msgstr ""
-msgid "Operation"
-msgstr ""
-
msgid "Operation not allowed"
msgstr ""
@@ -46474,6 +46456,9 @@ msgstr ""
msgid "The current user is not authorized to create the pipeline schedule"
msgstr ""
+msgid "The current user is not authorized to set pipeline schedule variables"
+msgstr ""
+
msgid "The current user is not authorized to update the pipeline schedule"
msgstr ""
@@ -48880,10 +48865,49 @@ msgstr ""
msgid "Trace Details"
msgstr ""
-msgid "Traces"
+msgid "Tracing"
msgstr ""
-msgid "Tracing"
+msgid "Tracing|Check again"
+msgstr ""
+
+msgid "Tracing|Date"
+msgstr ""
+
+msgid "Tracing|Duration"
+msgstr ""
+
+msgid "Tracing|Enable"
+msgstr ""
+
+msgid "Tracing|Failed to enable tracing."
+msgstr ""
+
+msgid "Tracing|Failed to load page."
+msgstr ""
+
+msgid "Tracing|Failed to load trace details."
+msgstr ""
+
+msgid "Tracing|Failed to load traces."
+msgstr ""
+
+msgid "Tracing|Get started with Tracing"
+msgstr ""
+
+msgid "Tracing|Monitor your applications with GitLab Distributed Tracing."
+msgstr ""
+
+msgid "Tracing|No traces to display."
+msgstr ""
+
+msgid "Tracing|Operation"
+msgstr ""
+
+msgid "Tracing|Service"
+msgstr ""
+
+msgid "Tracing|Traces"
msgstr ""
msgid "Track groups of issues that share a theme, across projects and milestones"
diff --git a/qa/qa/page/project/infrastructure/kubernetes/add_existing.rb b/qa/qa/page/project/infrastructure/kubernetes/add_existing.rb
deleted file mode 100644
index 2fc65cf0afe..00000000000
--- a/qa/qa/page/project/infrastructure/kubernetes/add_existing.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Page
- module Project
- module Infrastructure
- module Kubernetes
- class AddExisting < Page::Base
- view 'app/views/clusters/clusters/user/_form.html.haml' do
- element :cluster_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
- element :api_url, 'url_field :api_url' # rubocop:disable QA/ElementWithPattern
- element :ca_certificate, 'text_area :ca_cert' # rubocop:disable QA/ElementWithPattern
- element :token, 'text_field :token' # rubocop:disable QA/ElementWithPattern
- element :add_kubernetes_cluster_button
- element :rbac_checkbox
- end
-
- def set_cluster_name(name)
- fill_in 'cluster_name', with: name
- end
-
- def set_api_url(api_url)
- fill_in 'cluster_platform_kubernetes_attributes_api_url', with: api_url
- end
-
- def set_ca_certificate(ca_certificate)
- fill_in 'cluster_platform_kubernetes_attributes_ca_cert', with: ca_certificate
- end
-
- def set_token(token)
- fill_in 'cluster_platform_kubernetes_attributes_token', with: token
- end
-
- def add_cluster!
- click_element :add_kubernetes_cluster_button, Page::Project::Infrastructure::Kubernetes::Show
- end
-
- def uncheck_rbac!
- uncheck_element(:rbac_checkbox)
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/page/project/infrastructure/kubernetes/index.rb b/qa/qa/page/project/infrastructure/kubernetes/index.rb
deleted file mode 100644
index 4c759a049e1..00000000000
--- a/qa/qa/page/project/infrastructure/kubernetes/index.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Page
- module Project
- module Infrastructure
- module Kubernetes
- class Index < Page::Base
- view 'app/assets/javascripts/clusters_list/components/clusters_actions.vue' do
- element :clusters_actions_button
- end
-
- def connect_cluster
- click_element(:clusters_actions_button)
- end
-
- def has_cluster?(cluster)
- has_element?(:cluster, cluster_name: cluster.to_s)
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/page/project/infrastructure/kubernetes/show.rb b/qa/qa/page/project/infrastructure/kubernetes/show.rb
deleted file mode 100644
index 8725f64fe32..00000000000
--- a/qa/qa/page/project/infrastructure/kubernetes/show.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Page
- module Project
- module Infrastructure
- module Kubernetes
- class Show < Page::Base
- view 'app/assets/javascripts/clusters/forms/components/integration_form.vue' do
- element :integration_status_toggle
- element :base_domain_field
- end
-
- view 'app/assets/javascripts/integrations/edit/components/integration_form_actions.vue' do
- element :save_changes_button
- end
-
- def set_domain(domain)
- fill_element :base_domain_field, domain
- end
-
- def save_domain
- click_element :save_changes_button, Page::Project::Infrastructure::Kubernetes::Show
- end
- end
- end
- end
- end
- end
-end
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
index 6d810fdcd51..e4d87daa8d7 100644
--- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb
+++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu
include AccessMatchersForController
let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project, :public, :repository) }
- let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
+ let_it_be_with_reload(:project) { create(:project, :public, :repository) }
+ let_it_be_with_reload(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project) }
before do
project.add_developer(user)
@@ -137,6 +137,20 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu
expect(v.variable_type).to eq("file")
end
end
+
+ context 'when the user is not allowed to create a pipeline schedule with variables' do
+ before do
+ project.update!(restrict_user_defined_variables: true)
+ end
+
+ it 'does not create a new schedule' do
+ expect { go }
+ .to not_change { Ci::PipelineSchedule.count }
+ .and not_change { Ci::PipelineScheduleVariable.count }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
end
context 'when variables_attributes has two variables and duplicated' do
@@ -149,8 +163,8 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu
it 'returns an error that the keys of variable are duplicated' do
expect { go }
- .to change { Ci::PipelineSchedule.count }.by(0)
- .and change { Ci::PipelineScheduleVariable.count }.by(0)
+ .to not_change { Ci::PipelineSchedule.count }
+ .and not_change { Ci::PipelineScheduleVariable.count }
expect(assigns(:schedule).errors['variables']).not_to be_empty
end
@@ -213,6 +227,22 @@ RSpec.describe Projects::PipelineSchedulesController, feature_category: :continu
expect(pipeline_schedule.variables.last.key).to eq('AAA')
expect(pipeline_schedule.variables.last.value).to eq('AAA123')
end
+
+ context 'when the user is not allowed to update pipeline schedule variables' do
+ before do
+ project.update!(restrict_user_defined_variables: true)
+ end
+
+ it 'does not update the schedule' do
+ expect { go }
+ .to not_change { Ci::PipelineScheduleVariable.count }
+
+ expect(response).to have_gitlab_http_status(:ok)
+
+ pipeline_schedule.reload
+ expect(pipeline_schedule.variables).to be_empty
+ end
+ end
end
context 'when params include two duplicated variables' do
diff --git a/spec/factories/ci/reports/sbom/components.rb b/spec/factories/ci/reports/sbom/components.rb
index 8f2c00b695a..76bfbe13acb 100644
--- a/spec/factories/ci/reports/sbom/components.rb
+++ b/spec/factories/ci/reports/sbom/components.rb
@@ -9,12 +9,14 @@ FactoryBot.define do
transient do
purl_type { 'npm' }
+ namespace { nil }
end
purl do
::Sbom::PackageUrl.new(
type: purl_type,
name: name,
+ namespace: namespace,
version: version
).to_s
end
diff --git a/spec/frontend/tracing/components/tracing_details_spec.js b/spec/frontend/tracing/components/tracing_details_spec.js
new file mode 100644
index 00000000000..c5efa2a7eb5
--- /dev/null
+++ b/spec/frontend/tracing/components/tracing_details_spec.js
@@ -0,0 +1,103 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import TracingDetails from '~/tracing/components/tracing_details.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import { visitUrl, isSafeURL } from '~/lib/utils/url_utility';
+
+jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility');
+
+describe('TracingDetails', () => {
+ let wrapper;
+ let observabilityClientMock;
+
+ const TRACE_ID = 'test-trace-id';
+ const TRACING_INDEX_URL = 'https://www.gitlab.com/flightjs/Flight/-/tracing';
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findTraceDetails = () => wrapper.findComponentByTestId('trace-details');
+
+ const props = {
+ traceId: TRACE_ID,
+ tracingIndexUrl: TRACING_INDEX_URL,
+ };
+
+ const mountComponent = async () => {
+ wrapper = shallowMountExtended(TracingDetails, {
+ propsData: {
+ ...props,
+ observabilityClient: observabilityClientMock,
+ },
+ });
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ isSafeURL.mockReturnValue(true);
+
+ observabilityClientMock = {
+ isTracingEnabled: jest.fn(),
+ fetchTrace: jest.fn(),
+ };
+ });
+
+ it('renders the loading indicator while checking if tracing is enabled', () => {
+ mountComponent();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(observabilityClientMock.isTracingEnabled).toHaveBeenCalled();
+ });
+
+ describe('when tracing is enabled', () => {
+ const mockTrace = { traceId: 'test-trace-id', foo: 'bar' };
+ beforeEach(async () => {
+ observabilityClientMock.isTracingEnabled.mockResolvedValueOnce(true);
+ observabilityClientMock.fetchTrace.mockResolvedValueOnce(mockTrace);
+
+ await mountComponent();
+ });
+
+ it('fetches the trace and renders the trace details', () => {
+ expect(observabilityClientMock.isTracingEnabled).toHaveBeenCalled();
+ expect(observabilityClientMock.fetchTrace).toHaveBeenCalled();
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findTraceDetails().exists()).toBe(true);
+ });
+ });
+
+ describe('when tracing is not enabled', () => {
+ beforeEach(async () => {
+ observabilityClientMock.isTracingEnabled.mockResolvedValueOnce(false);
+
+ await mountComponent();
+ });
+
+ it('redirects to tracingIndexUrl', () => {
+ expect(visitUrl).toHaveBeenCalledWith(props.tracingIndexUrl);
+ });
+ });
+
+ describe('error handling', () => {
+ it('if isTracingEnabled fails, it renders an alert and empty page', async () => {
+ observabilityClientMock.isTracingEnabled.mockRejectedValueOnce('error');
+
+ await mountComponent();
+
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Failed to load trace details.' });
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findTraceDetails().exists()).toBe(false);
+ });
+
+ it('if fetchTrace fails, it renders an alert and empty page', async () => {
+ observabilityClientMock.isTracingEnabled.mockReturnValueOnce(true);
+ observabilityClientMock.fetchTrace.mockRejectedValueOnce('error');
+
+ await mountComponent();
+
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Failed to load trace details.' });
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findTraceDetails().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/tracing/components/tracing_list_spec.js b/spec/frontend/tracing/components/tracing_list_spec.js
index 183578cff31..f02b238f7a7 100644
--- a/spec/frontend/tracing/components/tracing_list_spec.js
+++ b/spec/frontend/tracing/components/tracing_list_spec.js
@@ -5,6 +5,8 @@ import TracingEmptyState from '~/tracing/components/tracing_empty_state.vue';
import TracingTableList from '~/tracing/components/tracing_table_list.vue';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
+import * as urlUtility from '~/lib/utils/url_utility';
+import setWindowLocation from 'helpers/set_window_location_helper';
jest.mock('~/alert');
@@ -69,6 +71,16 @@ describe('TracingList', () => {
expect(observabilityClientMock.fetchTraces).toHaveBeenCalledTimes(1);
});
+
+ it('on trace selection it redirects to the details url', () => {
+ setWindowLocation('base_path');
+ const visitUrlMock = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({});
+
+ findTableList().vm.$emit('trace-selected', { trace_id: 'test-trace-id' });
+
+ expect(visitUrlMock).toHaveBeenCalledTimes(1);
+ expect(visitUrlMock).toHaveBeenCalledWith('/base_path/test-trace-id');
+ });
});
describe('when tracing is not enabled', () => {
diff --git a/spec/frontend/tracing/components/tracing_table_list_spec.js b/spec/frontend/tracing/components/tracing_table_list_spec.js
index 773b3eb8ed2..aa96b9b370f 100644
--- a/spec/frontend/tracing/components/tracing_table_list_spec.js
+++ b/spec/frontend/tracing/components/tracing_table_list_spec.js
@@ -1,3 +1,4 @@
+import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import TracingTableList from '~/tracing/components/tracing_table_list.vue';
@@ -27,13 +28,18 @@ describe('TracingTableList', () => {
};
const getRows = () => wrapper.findComponent({ name: 'GlTable' }).find('tbody').findAll('tr');
-
+ const getRow = (idx) => getRows().at(idx);
const getCells = (trIdx) => getRows().at(trIdx).findAll('td');
const getCell = (trIdx, tdIdx) => {
return getCells(trIdx).at(tdIdx);
};
+ const selectRow = async (idx) => {
+ getRow(idx).trigger('click');
+ await nextTick();
+ };
+
it('renders traces as table', () => {
mountComponent();
@@ -50,6 +56,14 @@ describe('TracingTableList', () => {
});
});
+ it('emits trace-selected on row selection', async () => {
+ mountComponent();
+
+ await selectRow(0);
+ expect(wrapper.emitted('trace-selected')).toHaveLength(1);
+ expect(wrapper.emitted('trace-selected')[0][0]).toBe(mockTraces[0]);
+ });
+
it('renders the empty state when no traces are provided', () => {
mountComponent({ traces: [] });
diff --git a/spec/frontend/tracing/details_index_spec.js b/spec/frontend/tracing/details_index_spec.js
new file mode 100644
index 00000000000..e0d368b6cb7
--- /dev/null
+++ b/spec/frontend/tracing/details_index_spec.js
@@ -0,0 +1,42 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import DetailsIndex from '~/tracing/details_index.vue';
+import TracingDetails from '~/tracing/components/tracing_details.vue';
+import ObservabilityContainer from '~/observability/components/observability_container.vue';
+
+describe('DetailsIndex', () => {
+ const props = {
+ traceId: 'test-trace-id',
+ tracingIndexUrl: 'https://example.com/tracing/index',
+ oauthUrl: 'https://example.com/oauth',
+ tracingUrl: 'https://example.com/tracing',
+ provisioningUrl: 'https://example.com/provisioning',
+ };
+
+ let wrapper;
+
+ const mountComponent = () => {
+ wrapper = shallowMountExtended(DetailsIndex, {
+ propsData: props,
+ });
+ };
+
+ it('renders ObservabilityContainer component', () => {
+ mountComponent();
+
+ const observabilityContainer = wrapper.findComponent(ObservabilityContainer);
+ expect(observabilityContainer.exists()).toBe(true);
+ expect(observabilityContainer.props('oauthUrl')).toBe(props.oauthUrl);
+ expect(observabilityContainer.props('tracingUrl')).toBe(props.tracingUrl);
+ expect(observabilityContainer.props('provisioningUrl')).toBe(props.provisioningUrl);
+ });
+
+ it('renders TracingList component inside ObservabilityContainer', () => {
+ mountComponent();
+
+ const observabilityContainer = wrapper.findComponent(ObservabilityContainer);
+ const detailsCmp = observabilityContainer.findComponent(TracingDetails);
+ expect(detailsCmp.exists()).toBe(true);
+ expect(detailsCmp.props('traceId')).toBe(props.traceId);
+ expect(detailsCmp.props('tracingIndexUrl')).toBe(props.tracingIndexUrl);
+ });
+});
diff --git a/spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb b/spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb
index d9acd59aa71..ef09ad7aed4 100644
--- a/spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb
+++ b/spec/lib/generators/gitlab/analytics/internal_events_generator_spec.rb
@@ -83,7 +83,7 @@ RSpec.describe Gitlab::Analytics::InternalEventsGenerator, :silence_stdout, feat
let(:identifiers) { %w[project user namespace] }
let(:event_definition) do
{
- "category" => "GitlabInternalEvents",
+ "category" => "InternalEventTracking",
"action" => event,
"description" => description,
"product_section" => section,
diff --git a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb
index b36924d42a5..d62d25aeefe 100644
--- a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb
+++ b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb
@@ -27,6 +27,28 @@ RSpec.describe Gitlab::Ci::Reports::Sbom::Component, feature_category: :dependen
)
end
+ describe '#name' do
+ subject { component.name }
+
+ it { is_expected.to eq(name) }
+
+ context 'with namespace' do
+ let(:purl) do
+ 'pkg:maven/org.NameSpace/Name@v0.0.1'
+ end
+
+ it { is_expected.to eq('org.NameSpace/Name') }
+
+ context 'when needing normalization' do
+ let(:purl) do
+ 'pkg:pypi/org.NameSpace/Name@v0.0.1'
+ end
+
+ it { is_expected.to eq('org.namespace/name') }
+ end
+ end
+ end
+
describe '#<=>' do
where do
{
@@ -47,7 +69,7 @@ RSpec.describe Gitlab::Ci::Reports::Sbom::Component, feature_category: :dependen
a_type: 'library',
b_type: 'library',
a_purl: 'pkg:npm/component-a@1.0.0',
- b_purl: 'pkg:npm/component-a@1.0.0',
+ b_purl: 'pkg:npm/component-b@1.0.0',
a_version: '1.0.0',
b_version: '1.0.0',
expected: -1
@@ -57,7 +79,7 @@ RSpec.describe Gitlab::Ci::Reports::Sbom::Component, feature_category: :dependen
b_name: 'component-a',
a_type: 'library',
b_type: 'library',
- a_purl: 'pkg:npm/component-a@1.0.0',
+ a_purl: 'pkg:npm/component-b@1.0.0',
b_purl: 'pkg:npm/component-a@1.0.0',
a_version: '1.0.0',
b_version: '1.0.0',
diff --git a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
index 44887a86aff..dd633820ad9 100644
--- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
+++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
@@ -4,6 +4,9 @@ require 'spec_helper'
RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader, feature_category: :shared do
let(:policy) { ActionDispatch::ContentSecurityPolicy.new }
+ let(:lfs_enabled) { false }
+ let(:proxy_download) { false }
+
let(:csp_config) do
{
enabled: true,
@@ -20,6 +23,32 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader, feature_category: :s
}
end
+ let(:lfs_config) do
+ {
+ enabled: lfs_enabled,
+ remote_directory: 'lfs-objects',
+ connection: object_store_connection_config,
+ direct_upload: false,
+ proxy_download: proxy_download,
+ storage_options: {}
+ }
+ end
+
+ let(:object_store_connection_config) do
+ {
+ provider: 'AWS',
+ aws_access_key_id: 'AWS_ACCESS_KEY_ID',
+ aws_secret_access_key: 'AWS_SECRET_ACCESS_KEY'
+ }
+ end
+
+ before do
+ stub_lfs_setting(enabled: lfs_enabled)
+ allow(LfsObjectUploader)
+ .to receive(:object_store_options)
+ .and_return(GitlabSettings::Options.build(lfs_config))
+ end
+
describe '.default_enabled' do
let(:enabled) { described_class.default_enabled }
@@ -170,6 +199,70 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader, feature_category: :s
end
end
+ describe 'LFS connect-src headers' do
+ let(:url_for_provider) { described_class.send(:build_lfs_url) }
+
+ context 'when LFS is enabled' do
+ let(:lfs_enabled) { true }
+
+ context 'and direct downloads are enabled' do
+ let(:provider) { LfsObjectUploader.object_store_options.connection.provider }
+
+ context 'when provider is AWS' do
+ it { expect(provider).to eq('AWS') }
+
+ it { expect(url_for_provider).to be_present }
+
+ it { expect(directives['connect_src']).to include(url_for_provider) }
+ end
+
+ context 'when provider is AzureRM' do
+ let(:object_store_connection_config) do
+ {
+ provider: 'AzureRM',
+ azure_storage_account_name: 'azuretest',
+ azure_storage_access_key: 'ABCD1234'
+ }
+ end
+
+ it { expect(provider).to eq('AzureRM') }
+
+ it { expect(url_for_provider).to be_present }
+
+ it { expect(directives['connect_src']).to include(url_for_provider) }
+ end
+
+ context 'when provider is Google' do
+ let(:object_store_connection_config) do
+ {
+ provider: 'Google',
+ google_project: 'GOOGLE_PROJECT',
+ google_application_default: true
+ }
+ end
+
+ it { expect(provider).to eq('Google') }
+
+ it { expect(url_for_provider).to be_present }
+
+ it { expect(directives['connect_src']).to include(url_for_provider) }
+ end
+ end
+
+ context 'but direct downloads are disabled' do
+ let(:proxy_download) { true }
+
+ it { expect(directives['connect_src']).not_to include(url_for_provider) }
+ end
+ end
+
+ context 'when LFS is disabled' do
+ let(:proxy_download) { true }
+
+ it { expect(directives['connect_src']).not_to include(url_for_provider) }
+ end
+ end
+
describe 'CDN connections' do
before do
allow(described_class).to receive(:allow_letter_opener)
diff --git a/spec/requests/api/graphql/mutations/work_items/subscribe_spec.rb b/spec/requests/api/graphql/mutations/work_items/subscribe_spec.rb
new file mode 100644
index 00000000000..00672332082
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/work_items/subscribe_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Subscribe to a work item', feature_category: :team_planning do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:guest) { create(:user).tap { |user| project.add_guest(user) } }
+ let_it_be(:work_item) { create(:work_item, project: project) }
+
+ let(:subscribed_state) { true }
+ let(:mutation_input) { { 'id' => work_item.to_global_id.to_s, 'subscribed' => subscribed_state } }
+ let(:mutation) { graphql_mutation(:workItemSubscribe, mutation_input, fields) }
+ let(:mutation_response) { graphql_mutation_response(:work_item_subscribe) }
+ let(:fields) do
+ <<~FIELDS
+ workItem {
+ widgets {
+ type
+ ... on WorkItemWidgetNotifications {
+ subscribed
+ }
+ }
+ }
+ errors
+ FIELDS
+ end
+
+ context 'when user is not allowed to update subscription work items' do
+ let(:current_user) { create(:user) }
+
+ it_behaves_like 'a mutation that returns a top-level access error'
+ end
+
+ context
+
+ context 'when user has permissions to update its subscription to the work items' do
+ let(:current_user) { guest }
+
+ it "subscribe the user to the work item's notifications" do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change { work_item.subscribed?(current_user, project) }.to(true)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include({
+ 'type' => 'NOTIFICATIONS',
+ 'subscribed' => true
+ })
+ end
+
+ context 'when unsunscribing' do
+ let(:subscribed_state) { false }
+
+ before do
+ create(:subscription, project: project, user: current_user, subscribable: work_item, subscribed: true)
+ end
+
+ it "unsubscribe the user from the work item's notifications" do
+ expect do
+ post_graphql_mutation(mutation, current_user: current_user)
+ end.to change { work_item.subscribed?(current_user, project) }.to(false)
+
+ expect(response).to have_gitlab_http_status(:success)
+ expect(mutation_response['workItem']['widgets']).to include({
+ 'type' => 'NOTIFICATIONS',
+ 'subscribed' => false
+ })
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/pipeline_schedules/create_service_spec.rb b/spec/services/ci/pipeline_schedules/create_service_spec.rb
index a01c71432c3..3fc093c13da 100644
--- a/spec/services/ci/pipeline_schedules/create_service_spec.rb
+++ b/spec/services/ci/pipeline_schedules/create_service_spec.rb
@@ -3,9 +3,11 @@
require 'spec_helper'
RSpec.describe Ci::PipelineSchedules::CreateService, feature_category: :continuous_integration do
- let_it_be(:user) { create(:user) }
let_it_be(:reporter) { create(:user) }
- let_it_be(:project) { create(:project, :public, :repository) }
+ let_it_be_with_reload(:user) { create(:user) }
+ let_it_be_with_reload(:project) { create(:project, :public, :repository) }
+
+ subject(:service) { described_class.new(project, user, params) }
before_all do
project.add_maintainer(user)
@@ -82,5 +84,7 @@ RSpec.describe Ci::PipelineSchedules::CreateService, feature_category: :continuo
end
end
end
+
+ it_behaves_like 'pipeline schedules checking variables permission'
end
end
diff --git a/spec/services/ci/pipeline_schedules/update_service_spec.rb b/spec/services/ci/pipeline_schedules/update_service_spec.rb
index c31a652ed93..834bbcfcfeb 100644
--- a/spec/services/ci/pipeline_schedules/update_service_spec.rb
+++ b/spec/services/ci/pipeline_schedules/update_service_spec.rb
@@ -3,16 +3,18 @@
require 'spec_helper'
RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuous_integration do
- let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:user) { create(:user) }
+ let_it_be_with_reload(:project) { create(:project, :public, :repository) }
+ let_it_be_with_reload(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
let_it_be(:reporter) { create(:user) }
- let_it_be(:project) { create(:project, :public, :repository) }
- let_it_be(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
let_it_be(:pipeline_schedule_variable) do
create(:ci_pipeline_schedule_variable,
key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule)
end
+ subject(:service) { described_class.new(pipeline_schedule, user, params) }
+
before_all do
project.add_maintainer(user)
project.add_reporter(reporter)
@@ -123,5 +125,7 @@ RSpec.describe Ci::PipelineSchedules::UpdateService, feature_category: :continuo
end
end
end
+
+ it_behaves_like 'pipeline schedules checking variables permission'
end
end
diff --git a/spec/services/todos/destroy/group_private_service_spec.rb b/spec/services/todos/destroy/group_private_service_spec.rb
index be470688084..b3185bc72ff 100644
--- a/spec/services/todos/destroy/group_private_service_spec.rb
+++ b/spec/services/todos/destroy/group_private_service_spec.rb
@@ -14,7 +14,7 @@ RSpec.describe Todos::Destroy::GroupPrivateService, feature_category: :team_plan
let!(:todo_group_member) { create(:todo, user: group_member, group: group) }
let!(:todo_project_member) { create(:todo, user: project_member, group: group) }
- describe '#execute' do
+ describe '#execute', :aggregate_failures do
before do
group.add_developer(group_member)
project.add_developer(project_member)
@@ -57,7 +57,37 @@ RSpec.describe Todos::Destroy::GroupPrivateService, feature_category: :team_plan
end
it 'removes todos only for users who are not group users' do
- expect { subject }.to change { Todo.count }.from(7).to(5)
+ expect { subject }.to change { Todo.count }.from(7).to(4)
+
+ expect(parent_member.todos).to contain_exactly(todo_parent_member)
+ expect(subgroup_member.todos).to be_empty
+ expect(subgproject_member.todos).to contain_exactly(todo_subproject_member)
+ end
+ end
+
+ context 'with member via group share' do
+ let(:invited_group) { create(:group) }
+ let(:invited_group_member) { create(:user).tap { |u| invited_group.add_guest(u) } }
+
+ let!(:todo_invited_group_member) { create(:todo, user: invited_group_member, group: group) }
+
+ it 'does not remove todos for users invited to the group' do
+ create(:group_group_link, shared_group: group, shared_with_group: invited_group)
+
+ expect { subject }.to change { Todo.count }.from(5).to(3)
+
+ expect(invited_group_member.todos).to contain_exactly(todo_invited_group_member)
+ end
+
+ it 'does not remove todos for users invited to an ancestor group' do
+ parent_group = create(:group)
+ group.update!(parent: parent_group)
+
+ create(:group_group_link, shared_group: parent_group, shared_with_group: invited_group)
+
+ expect { subject }.to change { Todo.count }.from(5).to(3)
+
+ expect(invited_group_member.todos).to contain_exactly(todo_invited_group_member)
end
end
end
diff --git a/spec/support/shared_examples/ci/pipeline_schedules_create_or_update_shared_examples.rb b/spec/support/shared_examples/ci/pipeline_schedules_create_or_update_shared_examples.rb
new file mode 100644
index 00000000000..399225c13b2
--- /dev/null
+++ b/spec/support/shared_examples/ci/pipeline_schedules_create_or_update_shared_examples.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'pipeline schedules checking variables permission' do
+ let(:params) do
+ {
+ description: 'desc',
+ ref: 'patch-x',
+ active: false,
+ cron: '*/1 * * * *',
+ cron_timezone: 'UTC',
+ variables_attributes: variables_attributes
+ }
+ end
+
+ shared_examples 'success response' do
+ it 'saves values with passed params' do
+ result = service.execute
+
+ expect(result.status).to eq(:success)
+ expect(result.payload).to have_attributes(
+ description: 'desc',
+ ref: 'patch-x',
+ active: false,
+ cron: '*/1 * * * *',
+ cron_timezone: 'UTC'
+ )
+ end
+ end
+
+ shared_examples 'failure response' do
+ it 'does not save' do
+ result = service.execute
+
+ expect(result.status).to eq(:error)
+ expect(result.reason).to eq(:forbidden)
+ expect(result.message).to match_array(
+ ['The current user is not authorized to set pipeline schedule variables']
+ )
+ end
+ end
+
+ context 'when sending variables' do
+ let(:variables_attributes) do
+ [{ key: 'VAR2', secret_value: 'secret 2' }]
+ end
+
+ shared_examples 'success response with variables' do
+ it_behaves_like 'success response'
+
+ it 'saves variables' do
+ result = service.execute
+
+ variables = result.payload.variables.map { |v| [v.key, v.value] }
+
+ expect(variables).to include(
+ ['VAR2', 'secret 2']
+ )
+ end
+ end
+
+ context 'when user is maintainer' do
+ it_behaves_like 'success response with variables'
+ end
+
+ context 'when user is developer' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'success response with variables'
+ end
+
+ context 'when restrict_user_defined_variables is true' do
+ before_all do
+ project.update!(restrict_user_defined_variables: true)
+ end
+
+ it_behaves_like 'success response with variables'
+
+ context 'when user is developer' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'failure response'
+ end
+ end
+ end
+
+ context 'when not sending variables' do
+ let(:variables_attributes) { [] }
+
+ context 'when user is maintainer' do
+ it_behaves_like 'success response'
+ end
+
+ context 'when user is developer' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'success response'
+ end
+
+ context 'when restrict_user_defined_variables is true' do
+ before_all do
+ project.update!(restrict_user_defined_variables: true)
+ end
+
+ it_behaves_like 'success response'
+
+ context 'when user is developer' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'success response'
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb b/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb
index e2a4fb31361..67f53fdf32d 100644
--- a/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb
+++ b/spec/support/shared_examples/controllers/internal_event_tracking_examples.rb
@@ -10,8 +10,6 @@
RSpec.shared_examples 'internal event tracking' do
let(:fake_tracker) { instance_spy(Gitlab::Tracking::Destinations::Snowplow) }
- let(:namespace) { nil }
- let(:proejct) { nil }
before do
allow(Gitlab::Tracking).to receive(:tracker).and_return(fake_tracker)
@@ -23,6 +21,9 @@ RSpec.shared_examples 'internal event tracking' do
it 'logs to Snowplow', :aggregate_failures do
subject
+ project = try(:project)
+ namespace = try(:namespace)
+
expect(Gitlab::Tracking::StandardContext)
.to have_received(:new)
.with(
@@ -30,11 +31,12 @@ RSpec.shared_examples 'internal event tracking' do
user_id: user.id,
namespace_id: namespace&.id,
plan_name: namespace&.actual_plan_name
- )
+ ).at_least(:once)
expect(Gitlab::Tracking::ServicePingContext)
.to have_received(:new)
.with(data_source: :redis_hll, event: action)
+ .at_least(:once)
expect(fake_tracker).to have_received(:event)
.with(
@@ -45,6 +47,5 @@ RSpec.shared_examples 'internal event tracking' do
an_instance_of(SnowplowTracker::SelfDescribingJson)
]
)
- .exactly(:once)
end
end