diff options
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 |