From 3b328dfc1dd52cbe4227cd083c2bdec61d9dca4d Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 14 Apr 2025 18:12:09 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../user_type/access_summary_section.vue | 29 +++ .../user_type/user_type_selector.vue | 118 ++++++++++- .../components/pipeline_schedules_form.vue | 10 +- .../mount_pipeline_schedules_form_app.js | 4 +- .../runner/components/runner_status_badge.vue | 1 + .../docker_hub_rate_limits/index.js | 17 -- .../components/details_page/tags_list_row.vue | 21 ++ ...et_container_repository_tags.query.graphql | 1 + .../explorer/pages/list.vue | 4 - .../components/registry_settings_app.vue | 3 - .../projects/settings/ci_cd/show/index.js | 2 - .../projects/pipelines/charts/index.js | 2 - .../branch_rules/components/view/index.vue | 6 +- .../branch_rules/components/branch_rule.vue | 5 +- .../docker_hub_rate_limits_alert.vue | 45 ----- .../registry/repositories_controller.rb | 4 + .../settings/branch_rules_controller.rb | 1 - .../settings/repository_controller.rb | 1 - .../branch_rules/squash_options/update.rb | 10 - .../projects/branch_rules_resolver.rb | 6 - .../types/projects/branch_rule_type.rb | 5 +- .../projects/namespace_project_sort_enum.rb | 3 + app/helpers/ci/pipeline_schedules_helper.rb | 2 +- .../concerns/gitlab/encrypted_attribute.rb | 27 ++- app/models/project.rb | 18 +- app/models/project_ci_cd_setting.rb | 4 +- app/models/users/callout.rb | 1 - app/policies/project_policy.rb | 2 +- app/views/projects/pipelines/charts.html.haml | 1 - .../projects/settings/ci_cd/show.html.haml | 2 - app/views/shared/issuable/_form.html.haml | 2 +- .../issuable/form/_merge_params.html.haml | 6 +- .../inactive_projects_deletion_cron_worker.rb | 33 ++-- .../beta/branch_rule_squash_settings.yml | 9 - .../ops/emit_db_transaction_sli_metrics.yml | 2 +- .../ops/emit_sidekiq_histogram_metrics.yml | 2 +- ...enable_sidekiq_resource_usage_tracking.yml | 2 +- .../ops/enable_sidekiq_shard_router.yml | 2 +- .../ops/omit_aggregated_db_log_fields.yml | 2 +- .../ops/remote_mirror_no_delay.yml | 2 +- .../ops/sample_pg_stat_activity.yml | 2 +- ...ation-rename-skip-gitguardian-scanning.yml | 21 +- doc/api/graphql/reference/_index.md | 5 +- .../ai_features/ai_development_license.md | 2 +- .../documentation/styleguide/_index.md | 48 ++--- doc/development/rubocop_development_guide.md | 8 + doc/security/asset_proxy.md | 4 +- doc/update/deprecations.md | 36 ++-- .../dast/browser/checks/79.1.md | 83 ++++++-- .../dast/browser/checks/_index.md | 2 +- .../dast/on-demand_scan.md | 1 - .../application_security/detect/_index.md | 47 ++++- doc/user/compliance/audit_event_types.md | 2 + doc/user/profile/service_accounts.md | 39 ++-- .../pages_new_project_template.md | 29 ++- .../img/pages_project_templates_v13_1.png | Bin 56290 -> 0 bytes .../repository/branches/branch_rules.md | 8 +- .../pipeline_creation/inputs/string_input.rb | 2 +- locale/gitlab.pot | 56 ++++-- .../user_type/user_type_selector_spec.js | 183 +++++++++++++++++- spec/frontend/ci/job_details/mock_data.js | 11 -- .../pipeline_schedules_form_spec.js | 16 +- .../details_page/tags_list_row_spec.js | 64 +++++- .../container_registry/explorer/mock_data.js | 20 +- .../components/view/index_spec.js | 18 +- .../components/branch_rule_spec.js | 7 +- .../namespace_projects_resolver_spec.rb | 46 +++++ .../types/namespace_project_sort_enum_spec.rb | 2 + .../ci/pipeline_schedules_helper_spec.rb | 26 +-- .../inputs/spec_inputs_spec.rb | 1 + .../ci/config/interpolation/inputs_spec.rb | 11 ++ .../gitlab/encrypted_attribute_spec.rb | 10 +- spec/models/project_spec.rb | 12 ++ spec/policies/project_policy_spec.rb | 132 +++++++------ spec/requests/api/ci/pipelines_spec.rb | 6 +- spec/requests/api/ci/triggers_spec.rb | 6 +- .../squash_options/update_spec.rb | 11 -- .../merge_access_levels_spec.rb | 8 - .../push_access_levels_spec.rb | 8 - .../branch_rules/branch_protection_spec.rb | 44 ++--- .../api/graphql/project/branch_rules_spec.rb | 106 ++++------ .../registry/repositories_controller_spec.rb | 4 + ...tive_projects_deletion_cron_worker_spec.rb | 20 ++ .../gems/attr_encrypted/lib/attr_encrypted.rb | 37 ++-- .../test/attr_encrypted_test.rb | 22 --- .../gems/attr_encrypted/test/test_helper.rb | 1 - 86 files changed, 1007 insertions(+), 637 deletions(-) create mode 100644 app/assets/javascripts/admin/users/components/user_type/access_summary_section.vue delete mode 100644 app/assets/javascripts/docker_hub_rate_limits/index.js delete mode 100644 app/assets/javascripts/vue_shared/components/docker_hub_rate_limits_alert.vue delete mode 100644 config/feature_flags/beta/branch_rule_squash_settings.yml delete mode 100644 doc/user/project/pages/img/pages_project_templates_v13_1.png diff --git a/app/assets/javascripts/admin/users/components/user_type/access_summary_section.vue b/app/assets/javascripts/admin/users/components/user_type/access_summary_section.vue new file mode 100644 index 00000000000..2a934f65665 --- /dev/null +++ b/app/assets/javascripts/admin/users/components/user_type/access_summary_section.vue @@ -0,0 +1,29 @@ + + + diff --git a/app/assets/javascripts/admin/users/components/user_type/user_type_selector.vue b/app/assets/javascripts/admin/users/components/user_type/user_type_selector.vue index 7621d4a395f..ba053ad0f2e 100644 --- a/app/assets/javascripts/admin/users/components/user_type/user_type_selector.vue +++ b/app/assets/javascripts/admin/users/components/user_type/user_type_selector.vue @@ -1,7 +1,10 @@ @@ -74,7 +117,7 @@ export default { - +
+ +

+ {{ s__('AdminUsers|Review and set Admin area access with a custom admin role.') }} +

+
+ + + + + + + + + + + + + + + + + + + + + +
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue index 371fb760573..50d90485763 100644 --- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue +++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue @@ -16,7 +16,6 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue'; import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue'; import PipelineInputsForm from '~/ci/common/pipeline_inputs/pipeline_inputs_form.vue'; -import PipelineVariablesPermissionsMixin from '~/ci/mixins/pipeline_variables_permissions_mixin'; import createPipelineScheduleMutation from '../graphql/mutations/create_pipeline_schedule.mutation.graphql'; import updatePipelineScheduleMutation from '../graphql/mutations/update_pipeline_schedule.mutation.graphql'; import getPipelineSchedulesQuery from '../graphql/queries/get_pipeline_schedules.query.graphql'; @@ -38,7 +37,7 @@ export default { RefSelector, TimezoneDropdown, }, - mixins: [glFeatureFlagsMixin(), PipelineVariablesPermissionsMixin], + mixins: [glFeatureFlagsMixin()], inject: [ 'projectPath', 'projectId', @@ -46,7 +45,6 @@ export default { 'dailyLimit', 'settingsLink', 'schedulesPath', - 'userRole', ], props: { timezoneData: { @@ -62,6 +60,10 @@ export default { type: Boolean, required: true, }, + canSetPipelineVariables: { + type: Boolean, + required: true, + }, }, apollo: { schedule: { @@ -333,7 +335,7 @@ export default { /> { projectPath, schedulesPath, settingsLink, - userRole, + canSetPipelineVariables, timezoneData, } = containerEl.dataset; @@ -43,7 +43,6 @@ export default (selector, editing = false) => { projectPath, schedulesPath, settingsLink, - userRole, }, render(createElement) { return createElement(PipelineSchedulesForm, { @@ -51,6 +50,7 @@ export default (selector, editing = false) => { timezoneData: JSON.parse(timezoneData), refParam: defaultBranch, editing, + canSetPipelineVariables: parseBoolean(canSetPipelineVariables), }, }); }, diff --git a/app/assets/javascripts/ci/runner/components/runner_status_badge.vue b/app/assets/javascripts/ci/runner/components/runner_status_badge.vue index f0e8b7b6f22..2791daaf349 100644 --- a/app/assets/javascripts/ci/runner/components/runner_status_badge.vue +++ b/app/assets/javascripts/ci/runner/components/runner_status_badge.vue @@ -120,6 +120,7 @@ export default { v-gl-tooltip="badge.tooltip" :variant="badge.variant" :icon="badge.icon" + icon-optically-aligned v-bind="$attrs" > diff --git a/app/assets/javascripts/docker_hub_rate_limits/index.js b/app/assets/javascripts/docker_hub_rate_limits/index.js deleted file mode 100644 index e5b7e67df67..00000000000 --- a/app/assets/javascripts/docker_hub_rate_limits/index.js +++ /dev/null @@ -1,17 +0,0 @@ -import Vue from 'vue'; -import DockerHubRateLimitsAlert from '~/vue_shared/components/docker_hub_rate_limits_alert.vue'; - -export default (selector = '#js-docker-hub-rate-limits-alert') => { - const containerEl = document.querySelector(selector); - - if (!containerEl) { - return false; - } - - return new Vue({ - el: containerEl, - render(createElement) { - return createElement(DockerHubRateLimitsAlert); - }, - }); -}; diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue index 3a8657c204f..377f7d25920 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue @@ -12,6 +12,7 @@ import { import { localeDateFormat, newDate } from '~/lib/utils/datetime_utility'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import { n__ } from '~/locale'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; import ListItem from '~/vue_shared/components/registry/list_item.vue'; @@ -54,6 +55,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + mixins: [glFeatureFlagsMixin()], props: { tag: { type: Object, @@ -166,6 +168,9 @@ export default { this.tag.protection?.minimumAccessLevelForPush != null ); }, + isImmutable() { + return this.glFeatures.containerRegistryImmutableTags && this.tag.protection?.immutable; + }, tagRowId() { return `${this.tag.name}_badge`; }, @@ -227,6 +232,22 @@ export default { + +
- - {{ $options.i18n.UPDATE_SETTINGS_SUCCESS_MESSAGE }} - { coverageChartPath, defaultBranch, testRunsEmptyStateImagePath, - projectQualitySummaryFeedbackImagePath, } = el.dataset; const shouldRenderDoraCharts = parseBoolean(el.dataset.shouldRenderDoraCharts); @@ -46,7 +45,6 @@ const mountPipelineChartsApp = (el) => { defaultBranch, projectBranchCount, testRunsEmptyStateImagePath, - projectQualitySummaryFeedbackImagePath, contextId, }, render: (createElement) => createElement(ProjectPipelinesCharts, {}), diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue index e87ef0a11a4..7f13ce598d3 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue @@ -254,11 +254,7 @@ export default { return this.showApprovers || this.showSquashSetting; }, showSquashSetting() { - return ( - this.glFeatures.branchRuleSquashSettings && - !this.branch?.includes('*') && - !this.isAllProtectedBranchesRule - ); // Squash settings are not available for wildcards or All protected branches + return !this.branch?.includes('*') && !this.isAllProtectedBranchesRule; // Squash settings are not available for wildcards or All protected branches }, showEditSquashSetting() { return ( diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue index af008538046..7cf99419779 100644 --- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue +++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue @@ -41,9 +41,6 @@ export default { const squashOptions = project?.branchRules?.nodes || []; return squashOptions.find((option) => option.name === this.name)?.squashOption; }, - skip() { - return !this.glFeatures.branchRuleSquashSettings; - }, error(error) { createAlert({ message: error }); }, @@ -170,7 +167,7 @@ export default { if (this.pushAccessLevels.total > 0) { approvalDetails.push(this.pushAccessLevelsText); } - if (this.glFeatures.branchRuleSquashSettings && this.squashOption) { + if (this.squashOption) { approvalDetails.push(this.squashSettingText); } return approvalDetails; diff --git a/app/assets/javascripts/vue_shared/components/docker_hub_rate_limits_alert.vue b/app/assets/javascripts/vue_shared/components/docker_hub_rate_limits_alert.vue deleted file mode 100644 index 2ebd4622690..00000000000 --- a/app/assets/javascripts/vue_shared/components/docker_hub_rate_limits_alert.vue +++ /dev/null @@ -1,45 +0,0 @@ - - diff --git a/app/controllers/projects/registry/repositories_controller.rb b/app/controllers/projects/registry/repositories_controller.rb index 9194690ce82..6f8f77f0143 100644 --- a/app/controllers/projects/registry/repositories_controller.rb +++ b/app/controllers/projects/registry/repositories_controller.rb @@ -10,6 +10,10 @@ module Projects push_frontend_feature_flag(:show_container_registry_tag_signatures, project) end + before_action only: [:index, :show] do + push_frontend_feature_flag(:container_registry_immutable_tags, project) + end + before_action :authorize_update_container_image!, only: [:destroy] def index diff --git a/app/controllers/projects/settings/branch_rules_controller.rb b/app/controllers/projects/settings/branch_rules_controller.rb index 2ff58d9da18..ab537ea42b7 100644 --- a/app/controllers/projects/settings/branch_rules_controller.rb +++ b/app/controllers/projects/settings/branch_rules_controller.rb @@ -6,7 +6,6 @@ module Projects before_action :authorize_admin_project! before_action do push_frontend_feature_flag(:edit_branch_rules, @project) - push_frontend_feature_flag(:branch_rule_squash_settings, @project) end feature_category :source_code_management diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb index a81023f0484..9bb71de9f83 100644 --- a/app/controllers/projects/settings/repository_controller.rb +++ b/app/controllers/projects/settings/repository_controller.rb @@ -9,7 +9,6 @@ module Projects before_action do push_frontend_feature_flag(:edit_branch_rules, @project) - push_frontend_feature_flag(:branch_rule_squash_settings, @project) push_frontend_ability(ability: :admin_project, resource: @project, user: current_user) push_frontend_ability(ability: :admin_protected_branch, resource: @project, user: current_user) end diff --git a/app/graphql/mutations/projects/branch_rules/squash_options/update.rb b/app/graphql/mutations/projects/branch_rules/squash_options/update.rb index a1c3ed15f8d..8752f50ac41 100644 --- a/app/graphql/mutations/projects/branch_rules/squash_options/update.rb +++ b/app/graphql/mutations/projects/branch_rules/squash_options/update.rb @@ -25,10 +25,6 @@ module Mutations def resolve(branch_rule_id:, squash_option:) branch_rule = authorized_find!(id: branch_rule_id) - if feature_disabled?(branch_rule.project) - raise_resource_not_available_error! 'Squash options feature disabled' - end - service_response = ::Projects::BranchRules::SquashOptions::UpdateService.new( branch_rule, squash_option: squash_option, @@ -40,12 +36,6 @@ module Mutations errors: service_response.errors } end - - private - - def feature_disabled?(project) - Feature.disabled?(:branch_rule_squash_settings, project) - end end end end diff --git a/app/graphql/resolvers/projects/branch_rules_resolver.rb b/app/graphql/resolvers/projects/branch_rules_resolver.rb index 7af4f550579..d73b1b7ef01 100644 --- a/app/graphql/resolvers/projects/branch_rules_resolver.rb +++ b/app/graphql/resolvers/projects/branch_rules_resolver.rb @@ -18,8 +18,6 @@ module Resolvers # BranchRules for 'All branches' i.e. no associated ProtectedBranch def custom_branch_rules(args) - return [] unless squash_settings_enabled? - [all_branches_rule] end @@ -37,10 +35,6 @@ module Resolvers def protected_branches apply_lookahead(project.all_protected_branches.sorted_by_name) end - - def squash_settings_enabled? - Feature.enabled?(:branch_rule_squash_settings, project) - end end end end diff --git a/app/graphql/types/projects/branch_rule_type.rb b/app/graphql/types/projects/branch_rule_type.rb index cf97472d9fd..e0fde255844 100644 --- a/app/graphql/types/projects/branch_rule_type.rb +++ b/app/graphql/types/projects/branch_rule_type.rb @@ -49,8 +49,7 @@ module Types field :squash_option, type: ::Types::Projects::BranchRules::SquashOptionType, null: true, - description: 'The default behavior for squashing in merge requests. ' \ - 'Returns null if `branch_rule_squash_settings` feature flag is disabled.', + description: 'Default behavior for squashing in merge requests. ', experiment: { milestone: '17.9' } field :updated_at, Types::TimeType, @@ -58,8 +57,6 @@ module Types description: 'Timestamp of when the branch rule was last updated.' def squash_option - return unless ::Feature.enabled?(:branch_rule_squash_settings, branch_rule.project) - branch_rule.squash_option end end diff --git a/app/graphql/types/projects/namespace_project_sort_enum.rb b/app/graphql/types/projects/namespace_project_sort_enum.rb index 6209b0cc87d..7feb9d64ebd 100644 --- a/app/graphql/types/projects/namespace_project_sort_enum.rb +++ b/app/graphql/types/projects/namespace_project_sort_enum.rb @@ -14,6 +14,9 @@ module Types value 'PATH_ASC', 'Sort by path, ascending order.', value: :path_asc value 'PATH_DESC', 'Sort by path, descending order.', value: :path_desc + value 'FULL_PATH_ASC', 'Sort by full path, ascending order.', value: :full_path_asc + value 'FULL_PATH_DESC', 'Sort by full path, descending order.', value: :full_path_desc + value 'REPOSITORY_SIZE_ASC', 'Sort by total repository size, ascending order.', value: :repository_size_asc value 'REPOSITORY_SIZE_DESC', 'Sort by total repository size, descending order.', value: :repository_size_desc diff --git a/app/helpers/ci/pipeline_schedules_helper.rb b/app/helpers/ci/pipeline_schedules_helper.rb index 0a050f13913..1821e0976c6 100644 --- a/app/helpers/ci/pipeline_schedules_helper.rb +++ b/app/helpers/ci/pipeline_schedules_helper.rb @@ -13,7 +13,7 @@ module Ci schedules_path: pipeline_schedules_path(project), settings_link: project_settings_ci_cd_path(project), timezone_data: timezone_data.to_json, - user_role: current_user ? project.team.human_max_access(current_user.id) : nil + can_set_pipeline_variables: Ability.allowed?(current_user, :set_pipeline_variables, project).to_s } end end diff --git a/app/models/concerns/gitlab/encrypted_attribute.rb b/app/models/concerns/gitlab/encrypted_attribute.rb index 2103a89ef87..331d092c672 100644 --- a/app/models/concerns/gitlab/encrypted_attribute.rb +++ b/app/models/concerns/gitlab/encrypted_attribute.rb @@ -6,28 +6,27 @@ module Gitlab private - def db_key_base(attribute) - dynamic_encryption_key(:db_key_base, attribute) + def db_key_base + dynamic_encryption_key(:db_key_base) end - def db_key_base_32(attribute) - dynamic_encryption_key(:db_key_base_32, attribute) + def db_key_base_32 + dynamic_encryption_key(:db_key_base_32) end - def db_key_base_truncated(attribute) - dynamic_encryption_key(:db_key_base_truncated, attribute) + def db_key_base_truncated + dynamic_encryption_key(:db_key_base_truncated) end - def dynamic_encryption_key(key_type, attribute) - dynamic_encryption_key_for_operation(key_type, attr_encrypted_attributes[attribute][:operation]) + def dynamic_encryption_key(key_type) + dynamic_encryption_key_for_operation(key_type) end - def dynamic_encryption_key_for_operation(key_type, operation) - if operation == :encrypting - Gitlab::Encryption::KeyProvider[key_type].encryption_key.secret - else - Gitlab::Encryption::KeyProvider[key_type].decryption_keys.map(&:secret) - end + def dynamic_encryption_key_for_operation(key_type) + # We always use the encryption key, which is the only key defined since + # we don't support multiple keys in attr_encrypted but only with + # Active Record Encryption. + Gitlab::Encryption::KeyProvider[key_type].encryption_key.secret end end end diff --git a/app/models/project.rb b/app/models/project.rb index 3ca346d11e7..b33d6e75b35 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -685,6 +685,18 @@ class Project < ApplicationRecord scope :sorted_by_stars_asc, -> { reorder(self.arel_table['star_count'].asc) } scope :sorted_by_path_asc, -> { reorder(self.arel_table['path'].asc) } scope :sorted_by_path_desc, -> { reorder(self.arel_table['path'].desc) } + scope :sorted_by_full_path_asc, -> { order_by_full_path(:asc) } + scope :sorted_by_full_path_desc, -> { order_by_full_path(:desc) } + scope :order_by_full_path, ->(direction) do + build_keyset_order_on_joined_column( + scope: joins(:route), + attribute_name: 'project_full_path', + column: Route.arel_table[:path], + direction: direction, + nullable: :nulls_first + ) + end + # Sometimes queries (e.g. using CTEs) require explicit disambiguation with table name scope :projects_order_id_asc, -> { reorder(self.arel_table['id'].asc) } scope :projects_order_id_desc, -> { reorder(self.arel_table['id'].desc) } @@ -1118,6 +1130,8 @@ class Project < ApplicationRecord when 'latest_activity_asc' then sorted_by_updated_asc when 'path_desc'then sorted_by_path_desc when 'path_asc' then sorted_by_path_asc + when 'full_path_desc'then sorted_by_full_path_desc + when 'full_path_asc' then sorted_by_full_path_asc when 'stars_desc' then sorted_by_stars_desc when 'stars_asc' then sorted_by_stars_asc else @@ -3257,10 +3271,10 @@ class Project < ApplicationRecord ci_cd_settings.restrict_user_defined_variables? end - def override_pipeline_variables_allowed?(access_level) + def override_pipeline_variables_allowed?(access_level, user) return false unless ci_cd_settings - ci_cd_settings.override_pipeline_variables_allowed?(access_level) + ci_cd_settings.override_pipeline_variables_allowed?(access_level, user) end def ci_push_repository_for_job_token_allowed? diff --git a/app/models/project_ci_cd_setting.rb b/app/models/project_ci_cd_setting.rb index b17f29403bc..726feae71a9 100644 --- a/app/models/project_ci_cd_setting.rb +++ b/app/models/project_ci_cd_setting.rb @@ -68,7 +68,7 @@ class ProjectCiCdSetting < ApplicationRecord Gitlab::CurrentSettings.current_application_settings.keep_latest_artifact? && keep_latest_artifact? end - def override_pipeline_variables_allowed?(role_access_level) + def override_pipeline_variables_allowed?(role_access_level, user) return true unless restrict_user_defined_variables? project_minimum_access_level = pipeline_variables_minimum_override_role_for_database @@ -77,7 +77,7 @@ class ProjectCiCdSetting < ApplicationRecord role_project_minimum_access_level = role_map_pipeline_variables_minimum_override_role[project_minimum_access_level] - role_access_level >= role_project_minimum_access_level + role_access_level >= role_project_minimum_access_level || user&.can_admin_all_resources? end def pipeline_variables_minimum_override_role=(value) diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb index a10a9023b5b..48bba2637f3 100644 --- a/app/models/users/callout.rb +++ b/app/models/users/callout.rb @@ -60,7 +60,6 @@ module Users namespace_storage_limit_alert_warning_threshold: 56, # EE-only namespace_storage_limit_alert_alert_threshold: 57, # EE-only namespace_storage_limit_alert_error_threshold: 58, # EE-only - project_quality_summary_feedback: 59, # EE-only # 60 removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/154140 new_top_level_group_alert: 61, # 62, removed in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131314 diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 74177fa035c..f8c8f030326 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -250,7 +250,7 @@ class ProjectPolicy < BasePolicy end condition(:user_defined_variables_allowed) do - @subject.override_pipeline_variables_allowed?(team_access_level) + @subject.override_pipeline_variables_allowed?(team_access_level, @user) end condition(:push_repository_for_job_token_allowed) do diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml index e4fd39e8991..17a2826458b 100644 --- a/app/views/projects/pipelines/charts.html.haml +++ b/app/views/projects/pipelines/charts.html.haml @@ -10,6 +10,5 @@ failed_pipelines_link: project_pipelines_path(@project, page: '1', scope: 'all', status: 'failed'), coverage_chart_path: charts_project_graph_path(@project, @project.default_branch), test_runs_empty_state_image_path: image_path('illustrations/empty-state/empty-pipeline-md.svg'), - project_quality_summary_feedback_image_path: image_path('illustrations/chat-sm.svg'), project_branch_count: @project.repository_exists? ? @project.repository.branch_count : 0, default_branch: @project.default_branch } } diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index dfd1f7361bb..7cde6d2ed2a 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -7,8 +7,6 @@ %h1.gl-sr-only= @breadcrumb_title -#js-docker-hub-rate-limits-alert - - if can?(current_user, :admin_pipeline, @project) = render ::Layouts::SettingsBlockComponent.new(_("General pipelines"), id: 'js-general-pipeline-settings', diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 7d072823dde..8b0f29da0fd 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -24,7 +24,7 @@ = render 'shared/issuable/form/metadata', issuable: issuable, form: form, project: project, presenter: presenter -= render 'shared/issuable/form/merge_params', issuable: issuable, project: project += render 'shared/issuable/form/merge_params', issuable: issuable = render 'shared/issuable/form/contribution', issuable: issuable, form: form diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml index 2d96838e8b2..cdacf8c1982 100644 --- a/app/views/shared/issuable/form/_merge_params.html.haml +++ b/app/views/shared/issuable/form/_merge_params.html.haml @@ -22,11 +22,7 @@ = _("Squash commits when merge request is accepted.") = link_to sprite_icon('question-o'), help_page_path('user/project/merge_requests/squash_and_merge.md'), target: '_blank', rel: 'noopener noreferrer' - c.with_help_text do - -# When we remove the feature flag we should also remove the local_assigns[:project] - - if Feature.enabled?(:branch_rule_squash_settings, local_assigns.fetch(:project)) - = _('Required in this branch.') - - else - = _('Required in this project.') + = _('Required in this branch.') - else = hidden_field_tag 'merge_request[squash]', '0', id: nil = render Pajamas::CheckboxTagComponent.new(name: 'merge_request[squash]', checked: merge_request_squash_option?(merge_request), value: '1', checkbox_options: { class: 'js-form-update' }) do |c| diff --git a/app/workers/projects/inactive_projects_deletion_cron_worker.rb b/app/workers/projects/inactive_projects_deletion_cron_worker.rb index 075427bb27f..9aff5c346a1 100644 --- a/app/workers/projects/inactive_projects_deletion_cron_worker.rb +++ b/app/workers/projects/inactive_projects_deletion_cron_worker.rb @@ -31,7 +31,7 @@ module Projects project_id = last_processed_project_id Project.where('projects.id > ?', project_id).each_batch(of: 100) do |batch| # rubocop: disable CodeReuse/ActiveRecord - inactive_projects = batch.inactive.without_deleted + inactive_projects = batch.inactive.not_aimed_for_deletion inactive_projects.each do |project| if over_time? @@ -42,9 +42,10 @@ module Projects with_context(project: project, user: admin_bot) do deletion_warning_email_sent_on = notified_inactive_projects["project:#{project.id}"] - if send_deletion_warning_email?(deletion_warning_email_sent_on, project) - send_notification(project, admin_bot) - elsif deletion_warning_email_sent_on && delete_due_to_inactivity?(deletion_warning_email_sent_on) + if deletion_warning_email_sent_on.blank? + send_notification(project) + log_audit_event(project, admin_bot) + elsif grace_period_is_over?(deletion_warning_email_sent_on) Gitlab::InactiveProjectsDeletionWarningTracker.new(project.id).reset delete_project(project, admin_bot) end @@ -58,6 +59,10 @@ module Projects private + def grace_period_is_over?(deletion_warning_email_sent_on) + deletion_warning_email_sent_on < grace_months_after_deletion_notification.ago + end + def grace_months_after_deletion_notification strong_memoize(:grace_months_after_deletion_notification) do (::Gitlab::CurrentSettings.inactive_projects_delete_after_months - @@ -65,26 +70,26 @@ module Projects end end - def send_deletion_warning_email?(deletion_warning_email_sent_on, project) - deletion_warning_email_sent_on.blank? - end - - def delete_due_to_inactivity?(deletion_warning_email_sent_on) - deletion_warning_email_sent_on < grace_months_after_deletion_notification.ago - end - def deletion_date grace_months_after_deletion_notification.from_now.to_date.to_s end def delete_project(project, user) - ::Projects::DestroyService.new(project, user, {}).async_execute + if project.adjourned_deletion? + ::Projects::MarkForDeletionService.new(project, user, {}).execute + else + ::Projects::DestroyService.new(project, user, {}).async_execute + end end - def send_notification(project, user) + def send_notification(project) ::Projects::InactiveProjectsDeletionNotificationWorker.perform_async(project.id, deletion_date) end + def log_audit_event(_project, _user) + # Defined in EE + end + def over_time? (::Gitlab::Metrics::System.monotonic_time - @start_time) > MAX_RUN_TIME end diff --git a/config/feature_flags/beta/branch_rule_squash_settings.yml b/config/feature_flags/beta/branch_rule_squash_settings.yml deleted file mode 100644 index 4d67ebc5f10..00000000000 --- a/config/feature_flags/beta/branch_rule_squash_settings.yml +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: branch_rule_squash_settings -feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/498701 -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/173991 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/506542 -milestone: '17.7' -group: group::source code -type: beta -default_enabled: true diff --git a/config/feature_flags/ops/emit_db_transaction_sli_metrics.yml b/config/feature_flags/ops/emit_db_transaction_sli_metrics.yml index d2372243057..1bda62dd7dc 100644 --- a/config/feature_flags/ops/emit_db_transaction_sli_metrics.yml +++ b/config/feature_flags/ops/emit_db_transaction_sli_metrics.yml @@ -4,6 +4,6 @@ feature_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/3 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/147101 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/456667 milestone: '17.0' -group: group::scalability +group: group::durability type: ops default_enabled: false diff --git a/config/feature_flags/ops/emit_sidekiq_histogram_metrics.yml b/config/feature_flags/ops/emit_sidekiq_histogram_metrics.yml index 3433dc263cc..6120400b6ba 100644 --- a/config/feature_flags/ops/emit_sidekiq_histogram_metrics.yml +++ b/config/feature_flags/ops/emit_sidekiq_histogram_metrics.yml @@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/128706 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/421499 milestone: '16.3' type: ops -group: group::scalability +group: group::durability default_enabled: true diff --git a/config/feature_flags/ops/enable_sidekiq_resource_usage_tracking.yml b/config/feature_flags/ops/enable_sidekiq_resource_usage_tracking.yml index f54adf3ad93..0bc924bc30b 100644 --- a/config/feature_flags/ops/enable_sidekiq_resource_usage_tracking.yml +++ b/config/feature_flags/ops/enable_sidekiq_resource_usage_tracking.yml @@ -4,6 +4,6 @@ feature_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/work_ite introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/170895 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/501502 milestone: '17.6' -group: group::scalability +group: group::durability type: ops default_enabled: false diff --git a/config/feature_flags/ops/enable_sidekiq_shard_router.yml b/config/feature_flags/ops/enable_sidekiq_shard_router.yml index e2addadd950..9344e8900ac 100644 --- a/config/feature_flags/ops/enable_sidekiq_shard_router.yml +++ b/config/feature_flags/ops/enable_sidekiq_shard_router.yml @@ -4,6 +4,6 @@ feature_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/2 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/145495 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/444293 milestone: '16.10' -group: group::scalability +group: group::durability type: ops default_enabled: false diff --git a/config/feature_flags/ops/omit_aggregated_db_log_fields.yml b/config/feature_flags/ops/omit_aggregated_db_log_fields.yml index 7c17e4d2436..c9a0fcae44b 100644 --- a/config/feature_flags/ops/omit_aggregated_db_log_fields.yml +++ b/config/feature_flags/ops/omit_aggregated_db_log_fields.yml @@ -4,6 +4,6 @@ feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/500768#note_217 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/170751 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/501156 milestone: '17.6' -group: group::scalability +group: group::durability type: ops default_enabled: false diff --git a/config/feature_flags/ops/remote_mirror_no_delay.yml b/config/feature_flags/ops/remote_mirror_no_delay.yml index 17937b35cf0..79b02a92328 100644 --- a/config/feature_flags/ops/remote_mirror_no_delay.yml +++ b/config/feature_flags/ops/remote_mirror_no_delay.yml @@ -3,5 +3,5 @@ name: remote_mirror_no_delay introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92093 milestone: '15.2' type: ops -group: group::scalability +group: group::durability default_enabled: false diff --git a/config/feature_flags/ops/sample_pg_stat_activity.yml b/config/feature_flags/ops/sample_pg_stat_activity.yml index a3406ef15e0..55e3a2d63d7 100644 --- a/config/feature_flags/ops/sample_pg_stat_activity.yml +++ b/config/feature_flags/ops/sample_pg_stat_activity.yml @@ -4,6 +4,6 @@ feature_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/3 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/168335 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/503486 milestone: '17.6' -group: group::scalability +group: group::durability type: ops default_enabled: false diff --git a/data/deprecations/17-2-deprecation-rename-skip-gitguardian-scanning.yml b/data/deprecations/17-2-deprecation-rename-skip-gitguardian-scanning.yml index 1a451a4c69d..bc89148878a 100644 --- a/data/deprecations/17-2-deprecation-rename-skip-gitguardian-scanning.yml +++ b/data/deprecations/17-2-deprecation-rename-skip-gitguardian-scanning.yml @@ -1,13 +1,10 @@ -- title: "Rename options to skip GitGuardian secret detection" # The name of the feature to be deprecated - announcement_milestone: "17.3" # The milestone when this feature was first announced as deprecated. - removal_milestone: "Pending" # The milestone when this feature is planned to be removed - breaking_change: true +- title: "Rename options to skip GitGuardian secret detection" + announcement_milestone: "17.3" + removal_milestone: "18.0" + breaking_change: false body: | # Do not modify this line, instead modify the lines below. - The options to skip GitGuardian secret detection, `[skip secret detection]` and `secret_detection.skip_all`, are deprecated and will be removed in GitLab 18.0. You should use `[skip secret push protection]` and `secret_push_protection.skip_all` instead. -# The following items are not published on the docs page, but may be used in the future. - stage: secure # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth - tiers: premium, ultimate # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate] - issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/470119 # (optional) This is a link to the deprecation issue in GitLab - documentation_url: https://docs.gitlab.com/user/project/integrations/git_guardian/#skip-secret-detection # (optional) This is a link to the current documentation page - image_url: # (optional) This is a link to a thumbnail image depicting the feature - video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg + The options to skip GitGuardian secret detection, `[skip secret detection]` and `secret_detection.skip_all`, are deprecated. You should use `[skip secret push protection]` and `secret_push_protection.skip_all` instead. + stage: secure + tiers: premium, ultimate + issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/470119 + documentation_url: https://docs.gitlab.com/user/project/integrations/git_guardian/#skip-secret-detection diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index eb4616f5894..5f5bf542638 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -22202,7 +22202,7 @@ Branch rules configured for a rule target. | `isProtected` | [`Boolean!`](#boolean) | Check if the branch rule protects access for the branch. | | `matchingBranchesCount` | [`Int!`](#int) | Number of existing branches that match the branch rule. | | `name` | [`String!`](#string) | Name of the branch rule target. Includes wildcards. | -| `squashOption` {{< icon name="warning-solid" >}} | [`SquashOption`](#squashoption) | **Introduced** in GitLab 17.9. **Status**: Experiment. The default behavior for squashing in merge requests. Returns null if `branch_rule_squash_settings` feature flag is disabled. | +| `squashOption` {{< icon name="warning-solid" >}} | [`SquashOption`](#squashoption) | **Introduced** in GitLab 17.9. **Status**: Experiment. Default behavior for squashing in merge requests. | | `updatedAt` | [`Time`](#time) | Timestamp of when the branch rule was last updated. | ### `BurnupChartDailyTotals` @@ -43985,6 +43985,8 @@ Values for sorting projects. | `CONTAINER_REGISTRY_SIZE_ASC` | Sort by total container registry size, ascending order. | | `CONTAINER_REGISTRY_SIZE_DESC` | Sort by total container registry size, descending order. | | `EXCESS_REPO_STORAGE_SIZE_DESC` | Sort by excess repository storage size, descending order. | +| `FULL_PATH_ASC` | Sort by full path, ascending order. | +| `FULL_PATH_DESC` | Sort by full path, descending order. | | `LFS_OBJECTS_SIZE_ASC` | Sort by total LFS object size, ascending order. | | `LFS_OBJECTS_SIZE_DESC` | Sort by total LFS object size, descending order. | | `PACKAGES_SIZE_ASC` | Sort by total package size, ascending order. | @@ -45031,7 +45033,6 @@ Name of the feature that the callout is for. | `PRODUCT_ANALYTICS_DASHBOARD_FEEDBACK` | Callout feature name for product_analytics_dashboard_feedback. | | `PRODUCT_USAGE_DATA_COLLECTION_CHANGES` | Callout feature name for product_usage_data_collection_changes. | | `PROFILE_PERSONAL_ACCESS_TOKEN_EXPIRY` | Callout feature name for profile_personal_access_token_expiry. | -| `PROJECT_QUALITY_SUMMARY_FEEDBACK` | Callout feature name for project_quality_summary_feedback. | | `PROJECT_REPOSITORY_LIMIT_ALERT_WARNING_THRESHOLD` | Callout feature name for project_repository_limit_alert_warning_threshold. | | `REGISTRATION_ENABLED_CALLOUT` | Callout feature name for registration_enabled_callout. | | `SECURITY_CONFIGURATION_DEVOPS_ALERT` | Callout feature name for security_configuration_devops_alert. | diff --git a/doc/development/ai_features/ai_development_license.md b/doc/development/ai_features/ai_development_license.md index 83f688cd72f..ce559061dc8 100644 --- a/doc/development/ai_features/ai_development_license.md +++ b/doc/development/ai_features/ai_development_license.md @@ -101,7 +101,7 @@ gdk restart - If you need to set up a Duo Pro add-on instead, run this Rake task: ```shell -GITLAB_SIMULATE_SAAS=1 bundle exec 'rake gitlab:duo:setup[pro]' +GITLAB_SIMULATE_SAAS=1 bundle exec 'rake gitlab:duo:setup[duo_pro]' ``` ### Pros diff --git a/doc/development/documentation/styleguide/_index.md b/doc/development/documentation/styleguide/_index.md index f59fb7fe71f..69255b9901b 100644 --- a/doc/development/documentation/styleguide/_index.md +++ b/doc/development/documentation/styleguide/_index.md @@ -96,7 +96,7 @@ Also, keep the following guidance in mind: - For [UI text](#ui-text), allow for up to 30% expansion and contraction in translation. To see how much a string expands or contracts in another language, paste the string into [Google Translate](https://translate.google.com/) and review the results. - You can ask a colleague who speaks the language to verify if the translation is clear. + Ask a colleague who speaks the language to verify if the translation is clear. ## Markdown @@ -179,7 +179,7 @@ Instead of: - Application code is written by the developer. Sometimes, using `GitLab` as the subject can be awkward. For example, `GitLab exports the report`. -In this case, you can use passive voice instead. For example, `The report is exported`. +In this case, use passive voice instead. For example, `The report is exported`. ### Customer perspective @@ -211,7 +211,7 @@ without the addition of sales or marketing text. Instead, focus on facts and achievable goals. Be specific. For example: - The build time can decrease when you use this feature. -- You can use this feature to save time when you create a project. The API creates the file and you +- Use this feature to save time when you create a project. The API creates the file and you do not need to manually intervene. ### Self-referential writing @@ -303,8 +303,8 @@ For screenshots: 1. Close the dialog. All of the user data in the web page should now be replaced with the example data you entered. 1. Take the screenshot. -- Alternatively, you can create example accounts in a test environment, and take the screenshot there. -- If you can't easily reproduce the environment, you can blur the user data by using an image editing tool like Preview on macOS. +- Alternatively, create example accounts in a test environment, and take the screenshot there. +- If you can't easily reproduce the environment, blur the user data by using an image editing tool like Preview on macOS. ### Fake URLs @@ -320,7 +320,7 @@ There may be times where a token is needed to demonstrate an API call using cURL or a variable used in CI. It is strongly advised not to use real tokens in documentation even if the probability of a token being exploited is low. -You can use these fake tokens as examples: +Use these fake tokens as examples: | Token type | Token value | |:----------------------|:-------------------------------------------------------------------| @@ -436,7 +436,7 @@ When spacing content: - Use one space between sentences. (Use of more than one space is tested in [`SentenceSpacing.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab_base/SentenceSpacing.yml).) - Do not use non-breaking spaces. Use standard spaces instead. (Tested in [`lint-doc.sh`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/lint-doc.sh).) -- Do not use tabs for indentation. Use spaces instead. You can configure your code editor to output spaces instead of tabs when pressing the Tab key. +- Do not use tabs for indentation. Use spaces instead. Consider configuring your code editor to output spaces instead of tabs when pressing the Tab key. Do not use these punctuation characters: @@ -612,7 +612,7 @@ Use lists to present information in a format that is easier to scan. For example: ```markdown - You can: + To complete a task: - Do this thing. - Do this other thing. @@ -650,8 +650,8 @@ These things are imported: ### Nesting inside a list item -You can nest items under a list item, so they render with the same -indentation as the list item. You can do this with: +The following items can be nested under a list item, so they render with the same +indentation as the list item: - [Code blocks](#code-blocks) - [Blockquotes](#blockquotes) @@ -721,7 +721,7 @@ To keep tables accessible and scannable, tables should not have any empty cells. If there is no otherwise meaningful value for a cell, consider entering **N/A** for 'not applicable' or **None**. -To help keep tables easier to maintain, you can: +To make tables easier to maintain: - Add additional spaces to make the column widths consistent. For example: @@ -767,7 +767,8 @@ To enable the setting: To format a table with this extension, select the entire table, right-click the selection, and select **Format Selection With**. Select **Markdown Table Formatter** in the VS Code Command Palette. -Alternatively, if you use Sublime Text you can try the [Markdown Table Formatter](https://packagecontrol.io/packages/Markdown%20Table%20Formatter) +If you use Sublime Text, try the +[Markdown Table Formatter](https://packagecontrol.io/packages/Markdown%20Table%20Formatter) plugin, but it does not have a **Follow header row length** setting. ### Updates to existing tables @@ -848,7 +849,7 @@ The table and footnotes would render as follows: ##### Five or more footnotes If you have five or more footnotes that you cannot include in the table itself, -you can use consecutive numbers for the list items. +use consecutive numbers for the list items. If you use consecutive numbers, you must disable Markdown rule `029`: ```markdown @@ -909,7 +910,7 @@ To link to another documentation (`.md`) file in the same repository: - Put the entire link on a single line, even if the link is very long. ([Vale](../testing/vale.md) rule: [`MultiLineLinks.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab_base/MultiLineLinks.yml)). To link to a file outside of the documentation files, for example to link from development -documentation to a specific code file, you can: +documentation to a specific code file: - Use a full URL. For example: ``[`app/views/help/show.html.haml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/views/help/show.html.haml)`` - (Optional) Use a full URL with a specific ref. For example: ``[`app/views/help/show.html.haml`](https://gitlab.com/gitlab-org/gitlab/-/blob/6d01aa9f1cfcbdfa88edf9d003bd073f1a6fff1d/app/views/help/show.html.haml)`` @@ -954,7 +955,7 @@ For example: - `For more information, see [merge requests](link.md).` - `To create a review app, see [review apps](link.md).` -You can expand on this text by using phrases like +To expand on this text, use phrases like `For more information about this feature, see...` Do not use the following constructions: @@ -1330,7 +1331,7 @@ If you use macOS and want all screenshots to be compressed automatically, read [One simple trick to make your screenshots 80% smaller](https://about.gitlab.com/blog/2020/01/30/simple-trick-for-smaller-screenshots/). GitLab has a [Ruby script](https://gitlab.com/gitlab-org/gitlab/-/blob/master/bin/pngquant) -that you can use to simplify the manual process. In the root directory of your local +to simplify the manual process. In the root directory of your local copy of `https://gitlab.com/gitlab-org/gitlab`, run in a terminal: - Before compressing, if you want, check that all documentation PNG images have @@ -1556,8 +1557,8 @@ flowchart TD Use either the [Draw.io](https://draw.io) web application or the (unofficial) VS Code [Draw.io Integration](https://marketplace.visualstudio.com/items?itemName=hediet.vscode-drawio) -extension to create the diagram. Each tool provides the same diagram editing experience, however the web -application provides example diagrams that you can edit to suit your needs. +extension to create the diagram. Each tool provides the same diagram editing experience, but the web +application provides editable example diagrams. ##### Use the web application @@ -1623,7 +1624,7 @@ You can use icons from the [GitLab SVG library](https://gitlab-org.gitlab.io/git directly in the documentation. For example, `{{}}` renders as: {{< icon name="tanuki" >}}. In most cases, you should avoid using the icons in text. -However, you can use an icon when hover text is the only +However, use the icon when hover text is the only available way to describe a UI element. For example, **Delete** or **Edit** buttons often have hover text only. @@ -1637,7 +1638,7 @@ Do not use words to describe the icon: - Avoid: `Select **Erase job log** (the trash icon).` - Use instead: `Select **Erase job log** ({{}}).` This generates as: Select **Erase job log** ({{< icon name="remove" >}}). -When the button doesn't have any hover text, you can describe the icon. +When the button doesn't have any hover text, describe the icon. Follow up by creating a [UX bug issue](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Bug) to add hover text to the button to improve accessibility. @@ -1848,7 +1849,7 @@ It renders on the GitLab documentation site as: > This is a blockquote. -If the text spans multiple lines, you can split them. +If the text spans multiple lines, split them. For multiple paragraphs, use the symbol `>` before every line: @@ -1872,7 +1873,7 @@ It renders on the GitLab documentation site as: ## Tabs -On the documentation site, you can format text so it's displayed as tabs. +On the documentation site, you can format text to display as tabs. {{< alert type="warning" >}} @@ -2041,8 +2042,7 @@ GitLab, or restart GitLab. In this case: - The final step to reconfigure or restart GitLab can be used verbatim since it's the same every time. -When describing a configuration edit, you can use and edit to your liking the -following snippet: +When describing a configuration edit, use this snippet, editing it as needed: ````markdown {{}} diff --git a/doc/development/rubocop_development_guide.md b/doc/development/rubocop_development_guide.md index c0caaea42df..a5f9b643892 100644 --- a/doc/development/rubocop_development_guide.md +++ b/doc/development/rubocop_development_guide.md @@ -193,3 +193,11 @@ This allows you to reveal existing RuboCop exceptions during your daily work cyc Define `Include`s and permanent `Exclude`s in `.rubocop.yml` instead of `.rubocop_todo/**/*.yml`. {{< /alert >}} + +## RuboCop documentation + +When creating internal RuboCop rules, these should include RDoc style docs. + +These docs are used to generate a static site using Hugo, and are published to . + +The site includes all the internal cops from the `gitlab` and `gitlab-styles` projects, along with "good" and "bad" examples. diff --git a/doc/security/asset_proxy.md b/doc/security/asset_proxy.md index af255fd6ecb..51a02bbeefa 100644 --- a/doc/security/asset_proxy.md +++ b/doc/security/asset_proxy.md @@ -1,6 +1,6 @@ --- -stage: Software Supply Chain Security -group: Authentication +stage: Plan +group: Project Management info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments title: Proxying assets --- diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index 53d74b5127f..7f7544520dc 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -1722,6 +1722,22 @@ In 18.0 we are removing the `duoProAssignedUsersCount` GraphQL field. Users may
+
+ +### Rename options to skip GitGuardian secret detection + +
+ +- Announced in GitLab 17.3 +- Removal in GitLab 18.0 +- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/470119). + +
+ +The options to skip GitGuardian secret detection, `[skip secret detection]` and `secret_detection.skip_all`, are deprecated. You should use `[skip secret push protection]` and `secret_push_protection.skip_all` instead. + +
+
### Replace `add_on_purchase` GraphQL field with `add_on_purchases` @@ -7859,26 +7875,6 @@ Following [new guidance](https://docs.gitlab.com/development/api_styleguide/#wha
-### Rename options to skip GitGuardian secret detection - -
- -- Announced in GitLab 17.3 -- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/470119). - -
-{{< alert type="note" >}} - -This change has been removed from its original milestone and is being reassessed. - -{{< /alert >}} - -The options to skip GitGuardian secret detection, `[skip secret detection]` and `secret_detection.skip_all`, are deprecated and will be removed in GitLab 18.0. You should use `[skip secret push protection]` and `secret_push_protection.skip_all` instead. - -
- -
- ### SAST jobs no longer use global cache settings
diff --git a/doc/user/application_security/dast/browser/checks/79.1.md b/doc/user/application_security/dast/browser/checks/79.1.md index 039cc67785d..274fff0403c 100644 --- a/doc/user/application_security/dast/browser/checks/79.1.md +++ b/doc/user/application_security/dast/browser/checks/79.1.md @@ -2,35 +2,78 @@ stage: Application Security Testing group: Dynamic Analysis info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments -title: Cross-site Scripting +title: Cross Site Scripting --- ## Description -Reflected Cross-site Scripting (XSS) occurs when malicious scripts are injected into web applications -through request parameters that are immediately returned to a user without proper sanitization. -Unlike stored Cross-site Scripting attacks, reflected XSS are not persistent in the application's -database but are "reflected" back in the immediate response. +Cross Site Scripting (XSS) is an attack which exploits a web application or system to treat user input +as markup or script code. It is important to encode the data depending on the specific context it +is used in. There are at least six context types: + +- Inside HTML tags `
context 1
` +- Inside attributes: ```
``` +- Inside event attributes `````` +- Inside script blocks: `````` +- Unsafe element HTML assignment: ```element.innerHTML = "context 5"``` +- Inside URLs: ```link``` + +Script blocks alone can be encoded in multiple ways. Exercise caution if user +input must be written outside script tags. ## Remediation -Input validation: implement strict input validation for all user-controlled data. Use allowlist approaches -rather than denylists when validating input. Validate data type, length, and range as appropriate. +User input displayed in the application must be encoded, sanitized, or validated +so it isn't treated as HTML or executed as JavaScript code. Be careful not to +mix server-side templating with client-side templating, because the server-side doesn't encode +text like `{{ 7*7 }}`, which might execute client-side features. -Output encoding: apply context-specific output encoding when rendering user input. Use HTML entity -encoding for HTML contexts. Use JavaScript string escaping for JS context. CSS hex encoding should be -used for style attributes. +Do not encode user input before inserting it into a data store. The data must be +encoded based on its output context. It is much safer to force the displaying system to +handle the encoding. -Content Security Policy: implement a strict CSP that specifies trusted sources for scripts and other -resources. User `script-src 'self'` to restrict execution to same-origin source. Consider using nonces -or hashes for inline scripts when necessary. +Consider using built-in framework capabilities for automatically encoding user input. If you can't +automatically encode input, be careful to use the proper output encoding. The following recommendations +are a best effort, and might not work in all circumstances. -Framework Protections: leverage built-in XSS protections in modern frameworks. Use template engines -that automatically escape output. Avoid unsafe methods that bypass framework protections -(for example, `innerHTML`, `dangerouslySetInnerHTML`). - -HTTP Headers: set X-XSS-Protection headers for legacy browsers. Implement X-Content-Type-Options: nosniff. -Use Strict-Transport-Security to enforce HTTPS. +- Encode the following inside HTML tags, *excluding* `script`: + - `<` to `<` + - `>` to `>` + - `'` to `'` + - `"` to `"` + - `=` to `=` +- Encode the following inside attributes, *excluding* event attributes: + - `<` to `<` + - `>` to `>` + - `'` to `'` + - `"` to `"` + - `=` to `=` +- Encode the following inside event attributes, script blocks, and unsafe HTML assignment: + - Literal tab (`\t`) to `\\t` + - Literal new line (`\n`) to `\\n` + - Literal vertical tab (`\v`) to `\u000b` + - Literal form feed (`\f`) to `\\f` + - Literal carriage return (`\r`) to `\\r` + - Literal equal sign (`=`) to `\u0061` + - Literal back tick (`\`) to `\u0060` + - Literal double quote (`"`) to `\u0022` + - Literal ampersand (`&`) to `\u0026` + - Literal single quote (`'`) to `\u0027` + - Literal plus symbol (`+`) to `\u002b` + - Literal forward slash (`/`) to `\/` + - Literal less than symbol (`<`) to `\u003c` + - Literal greater than symbol (`>`) to `\u003e` + - Literal open parenthesis (`(`) to `\u0028` + - Literal close parenthesis (`)`) to `\u0029` + - Literal open bracket (`[`) to `\u005b` + - Literal close bracket (`]`) to `\u005d` + - Literal open brace (`{`) to `\u007b` + - Literal close brace (`}`) to `\u007d` + - Literal back slash (`\`) to `\\` + + This list is not exhaustive. You might need to encode additional characters depending on context. +- Inside URLs: + - Never allow user input to be printed in URLs. Attackers could inject `javascript:...` code or malicious links. ## Details @@ -40,5 +83,5 @@ Use Strict-Transport-Security to enforce HTTPS. ## Links +- [OWASP](https://owasp.org/www-community/attacks/xss/) - [CWE](https://cwe.mitre.org/data/definitions/79.html) -- [Cross-site Scripting - Wikipedia](https://en.wikipedia.org/wiki/Cross-site_scripting) diff --git a/doc/user/application_security/dast/browser/checks/_index.md b/doc/user/application_security/dast/browser/checks/_index.md index 9196152e440..74dba0544a8 100644 --- a/doc/user/application_security/dast/browser/checks/_index.md +++ b/doc/user/application_security/dast/browser/checks/_index.md @@ -186,7 +186,7 @@ scan for vulnerabilities in the site under test. | [611.1](611.1.md) | External XML Entity Injection (XXE) | High | Active | | [74.1](74.1.md) | XSLT Injection | High | Active | | [78.1](78.1.md) | OS Command Injection | High | Active | -| [79.1](79.1.md) | Cross-site Scripting | High | Active | +| [79.1](79.1.md) | Cross Site Scripting | High | Active | | [89.1](89.1.md) | SQL Injection | High | Active | | [917.1](917.1.md) | Expression Language Injection | High | Active | | [918.1](918.1.md) | Server-Side Request Forgery | High | Active | diff --git a/doc/user/application_security/dast/on-demand_scan.md b/doc/user/application_security/dast/on-demand_scan.md index 7cd0ff7ad66..045fea3f07b 100644 --- a/doc/user/application_security/dast/on-demand_scan.md +++ b/doc/user/application_security/dast/on-demand_scan.md @@ -296,7 +296,6 @@ Validating a site is required to run an active scan. Prerequisites: - A runner must be available in the project to run a validation job. -- The GitLab server's certificate must be trusted and must not use a self-signed certificate. To validate a site profile: diff --git a/doc/user/application_security/detect/_index.md b/doc/user/application_security/detect/_index.md index 8c1f9aa1e83..acc1d2be9ae 100644 --- a/doc/user/application_security/detect/_index.md +++ b/doc/user/application_security/detect/_index.md @@ -42,14 +42,43 @@ Behavioral testing tools include: - API security testing: Test your application's API for known attacks and vulnerabilities to input. - Coverage-guided fuzz testing: Test your application for unexpected behavior. -## Lifecycle coverage +## Early detection -You should enable vulnerability detection from before the first commit through to when your -application can be deployed and run. Early detection has many benefits, including easier and quicker -remediation. +Enable GitLab application security scanning tools from before the first commit. Early detection +provides benefits such as easier, quicker, and cheaper remediation, compared to detection later in +the software development lifecycle. GitLab provides developers immediate feedback of security +scanning, enabling them to address vulnerabilities early. -All GitLab application security scanning tools can be run in a CI/CD pipeline, triggered by code -changes. Security scans can also be run on a schedule, outside the context of code changes, and some -can be run manually. It's important to also perform detection outside the CI/CD pipeline because -risks can arise outside the context of code changes. For example, a newly-discovered vulnerability -in a dependency might be a risk to any application using it. +Security scans: + +- Run automatically in the CI/CD pipeline when developers commit changes. Vulnerabilities detected + in a feature branch are listed, enabling you to investigate and address them before they're merged + into the default branch. For more details, see + [Security scan results](security_scan_results.md). +- Can be scheduled or run manually to detect vulnerabilities. When a project is idle and no changes + are being made, security scans configured to run in a CI/CD pipeline are not run. Risks such as + newly-discovered vulnerabilities can go undetected in this situation. Running security scans + outside a CI/CD pipeline helps address this risk. For more details, see + [Scan execution policies](../policies/scan_execution_policies.md). + +## Prevention + +Security scanning in the pipeline can help minimize the risk of vulnerabilities in the default +branch: + +- Extra approval can be enforced on merge requests according to the results of pipeline + security scanning. For example, you can require that a member of the security team **also** + approve a merge request if one or more critical vulnerabilities are detected in the code + changes. For more details, see + [Merge request approval policies](../policies/merge_request_approval_policies.md). +- Secret push protection can prevent commits being pushed to GitLab if they contain secret + information - for example, a GitLab personal access token. + +## Vulnerability management workflow + +Vulnerabilities detected in the default branch are listed in the vulnerability report. To address +these vulnerabilities, follow the vulnerability management workflow: + +- Triage: Evaluate vulnerabilities to identify those that need immediate attention. +- Analyze: Examine details of a vulnerability to determine if it can and should be remediated. +- Remediate: Resolve the root cause of the vulnerability, reduce the associated risks, or both. diff --git a/doc/user/compliance/audit_event_types.md b/doc/user/compliance/audit_event_types.md index 7a465199006..947a463e41c 100644 --- a/doc/user/compliance/audit_event_types.md +++ b/doc/user/compliance/audit_event_types.md @@ -455,6 +455,8 @@ Audit event types belong to the following product categories. | Type name | Event triggered when | Saved to database | Introduced in | Scope | |:----------|:---------------------|:------------------|:--------------|:------| +| [`admin_role_assigned_to_user`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186570) | A custom admin role is assigned to a user | {{< icon name="check-circle" >}} Yes | GitLab [18.0](https://gitlab.com/gitlab-org/gitlab/-/issues/507958) | User | +| [`admin_role_unassigned_from_user`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/186570) | A custom admin role is unassigned from a user | {{< icon name="check-circle" >}} Yes | GitLab [18.0](https://gitlab.com/gitlab-org/gitlab/-/issues/507958) | User | | [`member_role_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137087) | A custom role is created | {{< icon name="check-circle" >}} Yes | GitLab [16.7](https://gitlab.com/gitlab-org/gitlab/-/issues/388934) | Group, Instance | | [`member_role_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/141630) | A custom role is deleted | {{< icon name="check-circle" >}} Yes | GitLab [16.9](https://gitlab.com/gitlab-org/gitlab/-/issues/437672) | Group, Instance | | [`member_role_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/141630) | A custom role is updated | {{< icon name="check-circle" >}} Yes | GitLab [16.9](https://gitlab.com/gitlab-org/gitlab/-/issues/437672) | Group, Instance | diff --git a/doc/user/profile/service_accounts.md b/doc/user/profile/service_accounts.md index 266df7ae5ae..20f394166d4 100644 --- a/doc/user/profile/service_accounts.md +++ b/doc/user/profile/service_accounts.md @@ -40,10 +40,10 @@ token for [Git operations](personal_access_tokens.md#clone-repository-using-pers - [Configurable rate limits](../../security/rate_limits.md#configurable-limits). - [Non-configurable rate limits](../../security/rate_limits.md#non-configurable-limits). -Service accounts were previously managed exclusively through the API. You can now use either the UI or the API. +You can also manage service accounts through the API. -- For instance-level service accounts, use the [Service account users API](../../api/user_service_accounts.md). -- For group-level service accounts, use the [Group service accounts API](../../api/group_service_accounts.md). +- For instance-level service accounts, use the [service account users API](../../api/user_service_accounts.md). +- For group-level service accounts, use the [group service accounts API](../../api/group_service_accounts.md). ## View and manage service accounts @@ -168,13 +168,18 @@ The personal access tokens page displays information about the personal access t - Rotate personal access tokens. - Revoke personal access tokens. +You can also manage personal access tokens for service accounts through the API. + +- For instance-level service accounts, use the [personal access tokens API](../../api/user_service_accounts.md). +- For group-level service accounts, use the [group service accounts API](../../api/group_service_accounts.md). + To view the personal access tokens page for a service account: 1. Go to the [Service Accounts](#view-and-manage-service-accounts) page. 1. Identify a service account. 1. Select the vertical ellipsis ({{< icon name="ellipsis_v" >}}) > **Manage Access Tokens**. -## Create a personal access token for a service account +### Create a personal access token for a service account To use a service account, you must create a personal access token to authenticate requests. @@ -198,25 +203,31 @@ To create a personal access token: 1. Select the [desired scopes](personal_access_tokens.md#personal-access-token-scopes). 1. Select **Create personal access token**. -## Rotate a personal access token +### Rotate a personal access token Prerequisites: -- For service accounts created by top-level group Owners, you must have the Owner role in the top-level group or be an administrator. -- For service accounts created by administrators, you must be an administrator for your GitLab Self-Managed instance. +- For instance-level service accounts, you must be an administrator for the instance. +- For group-level service accounts, you must have the Owner role in a top-level group. -Use the groups API to [rotate the personal access token](../../api/group_service_accounts.md#rotate-a-personal-access-token-for-a-service-account-user) for a service account user. +1. Go to the [Service Accounts](#view-and-manage-service-accounts) page. +1. Identify a service account. +1. Select the vertical ellipsis ({{< icon name="ellipsis_v" >}}) > **Manage Access Tokens**. +1. Select **Rotate**. +1. On the confirmation dialog, select **Rotate**. -## Revoke a personal access token +### Revoke a personal access token Prerequisites: -- You must be signed in as the service account user. +- For instance-level service accounts, you must be an administrator for the instance. +- For group-level service accounts, you must have the Owner role in a top-level group. -To revoke a personal access token, use the [personal access tokens API](../../api/personal_access_tokens.md#revoke-a-personal-access-token). You can use either of the following methods: - -- Use a [personal access token ID](../../api/personal_access_tokens.md#revoke-a-personal-access-token). The token used to perform the revocation must have the [`admin_mode`](personal_access_tokens.md#personal-access-token-scopes) scope. -- Use a [request header](../../api/personal_access_tokens.md#self-revoke). The token used to perform the request is revoked. +1. Go to the [Service Accounts](#view-and-manage-service-accounts) page. +1. Identify a service account. +1. Select the vertical ellipsis ({{< icon name="ellipsis_v" >}}) > **Manage Access Tokens**. +1. Select **Revoke**. +1. On the confirmation dialog, select **Revoke**. ## Delete a service account via API diff --git a/doc/user/project/pages/getting_started/pages_new_project_template.md b/doc/user/project/pages/getting_started/pages_new_project_template.md index c69165864e6..eb360de4178 100644 --- a/doc/user/project/pages/getting_started/pages_new_project_template.md +++ b/doc/user/project/pages/getting_started/pages_new_project_template.md @@ -21,9 +21,6 @@ configured to generate a Pages site. 1. On the left sidebar, at the top, select **Create new** ({{< icon name="plus" >}}) and **New project/repository**. 1. Select **Create from Template**. 1. Next to one of the templates starting with **Pages**, select **Use template**. - - ![Project templates for Pages](../img/pages_project_templates_v13_1.png) - 1. Complete the form and select **Create project**. 1. On the left sidebar, select **Build > Pipelines** and select **New pipeline** to trigger GitLab CI/CD to build and deploy your @@ -37,3 +34,29 @@ that immediately publishes your changes to the Pages site. To view the HTML and other assets that were created for the site, [download the job artifacts](../../../../ci/jobs/job_artifacts.md#download-job-artifacts). + +## Project templates + +{{< history >}} + +- [Removed](https://gitlab.com/groups/gitlab-org/-/epics/13847) the following templates from + project templates in GitLab 18.0: + [`Bridgetown`](https://gitlab.com/pages/bridgetown), [`Gatsby`](https://gitlab.com/pages/gatsby), + [`Hexo`](https://gitlab.com/pages/hexo), [`Middleman`](https://gitlab.com/pages/middleman), + `Netlify/GitBook`, [`Netlify/Hexo`](https://gitlab.com/pages/nfhexo), + [`Netlify/Hugo`](https://gitlab.com/pages/nfhugo), [`Netlify/Jekyll`](https://gitlab.com/pages/nfjekyll), + [`Netlify/Plain HTML`](https://gitlab.com/pages/nfplain-html), and [`Pelican`](https://gitlab.com/pages/pelican). + +{{< /history >}} + +GitLab maintains template projects for these frameworks: + +| Realm | Framework | Available project templates | +|----------------|-----------------------------------------------------|-----------------------------| +| **Go** | [`hugo`](https://gitlab.com/pages/hugo) | Pages/Hugo | +| **Markdown** | [`astro`](https://gitlab.com/pages/astro) | Pages/Astro | +| **Markdown** | [`docusaurus`](https://gitlab.com/pages/docusaurus) | Pages/Docusaurus | +| **Plain HTML** | [`plain-html`](https://gitlab.com/pages/plain-html) | Pages/Plain HTML | +| **React** | [`next.js`](https://gitlab.com/pages/nextjs) | Pages/Next.js | +| **Ruby** | [`jekyll`](https://gitlab.com/pages/jekyll) | Pages/Jekyll | +| **Vue.js** | [`nuxt`](https://gitlab.com/pages/nuxt) | Pages/Nuxt | diff --git a/doc/user/project/pages/img/pages_project_templates_v13_1.png b/doc/user/project/pages/img/pages_project_templates_v13_1.png deleted file mode 100644 index d67a82f06230e40d572c7958c7c2224c2658d416..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 56290 zcmZ^}WkB3L^EOJMxE80lOL3Os?y|+*wM%h#cXtX46km$FySqzqEyaqJ;`T20{ru&e z^B(p?^2=nB%w*=8WJsj)CmD2<_b4zhFz9l!lBzH;Z^1Ayu#HG?&=wA4gg$8T_OlpJ z3q-{W^c=CVs3A0!Rle_0PO_>Bj~{o zt=d|+m{54w+Sobsdk9he(}N#ce{E)?qWGtai?tAyHc**D!rsY(f}0h<%1$MWLP0?x z=wxomuPQ11A9m=M5EaP9#etuV&E4Id)t!sg-sv+N2Ol3F8#^Z(CnpQE2aB_(or{SF zi=8v|zk~eOIFc65W=>WPE>`w-6tCl&nA*F#2vJeJPW0b@|E|-*!|H!#vUC2ASWp4k zUMXxGtn6(6H#Qe5%l{v?SIWO^|H$?4bb_yg@oP9+I7!&s+FIDT2&!Lrh$)RLZ* z71gC)US3vLSGnS4US95=pPyM-SrZcz@9*!0OEvcP_U>O^&d<-gH}3ZyUQShbxj#2P zJ#RR%vb;RLBqina4-A|>y{vR)fD;q{?lfP$yzE@Pgw*V&fV=bCwj3NBo*tKLeMq>O ze22F0FSpwqDpa2CHxm=n3kP{+?YcK_o?l)rU!EV9&Yo>$v!0)i0UzA-SrcxrZ)c94 zmfIXJ)@vWHSDv5tnlcks+JmfFS=w_Fpmv^KJbHK3pPX9C!;pM@epI)7>&MDm{Jpg6 z$Hn8zi$le(7Aq?Uvr$P_LUm)~_Gqc4W&hRI=)>JET57;nh=dFI0hd;NpgFNr^G6 zY(o_())`Zgt-pl5ro@UJYMjWu;tHIdqNna(7VDhnMmlE}_ka=oI|HfL`!n9jEh#NK z-BZ7SWtx?Z!^6K`TAJ&o`>MZws}XljsvX)7cXap^KT(wGTozdG4;~DL%xAbzte-!$0v-PR*VEpy@a z?&Vun@_3=@beT;=tNvh)J-=qLgied3V-c8@DaBJeBh)F%iSo$C=;7=@vBtHxI6gQk zB5&aQ@O)tatlm6nU>sdu|IMvB%r-oy%Gs{zdVjk*L8Dc%Q|?4z z@90umN58_OFj0KNcF$f?!~pQtdKroUE8UzmHjt?ys1-x0pDlm{hCJ0{Fmd zEuM3ot8G?QyFW^Oh${?e31NcsqX{%r-S~BMrIcwPt_pN2kv)Z>jF`OSCJsU@7k z7TCHa2gwUK78x9+f|fpBa2iphRXzh*G3Z`)tw_&} zS3Pp4fsgoDswPr~Uk1mWP-c#5CdFKz1AvT~dVkIhyLwL@?-ECM${q)i%9j%);tHi( z<%`}^AaV6ta(+&yNLe|l&OJKhQ<}i>{%ukECm1oFBEYXY$2~=$s(X#czxJ=8WGV|) zJ}Og(HW zE`EFs<|ru@P~3z_ftYG-?mZiKM+uNm;&b_Oy%3_u^*arc2t0ApZ!u8{#aw)XvwYxY zsD${z5DXaU_87>HzBD$)FuF=RHAOClBr;yUw!%cgfZTE}gM34B9I#%#li4fUIn>)<#OVNd+!n z$fyK>*;B=jTmthL(QQFTF8WJy`&Qvg3dH|CQl6DFlXB5VygIrpE@^2LcQN87AW1|? zujGE*5$fJR5D08i^3KFd66cpFdsi?wo|Fo}H%u|AAJNI{Hvc1h`kGaR$?8s{td!dA;l$sNs$?lhcIaTG;Xw?-o^0hEmxWEs~4=9kN_3L zlUt+mkxQuXr3HhGs7W{R*+S?j0g*S^BHN(mAUYrCL;GtPTwPos9{cM13(}{z2nqw5 z5AoA2Z-xevy~Z}SW&J^Chlh;88PAJaD~1JNO7w4yO)(H4@z=ktHkaE$7CD!=`vXnC zs#o2HYNt+a>~{6UlTWhhZjeXa8=+%g8n(#UIL`LVO@%c3#dtCN|4|{1L8W=2Bk)ohsU@-rH<9TEt)a8Y?#7$ z`0?E`Dg)4&UuN$fiVh?akxNQR5z@MpcqqqAxu+=s1!8t6oYbJ&)SK#(GdD%#y>ID*e}sHPS@yvJ_Dn()Bi zMvA%8eWffl2zQRD8|As9b?=vHJejOpmE)>h2}Fn2G!`d@2>L7FIb3{D@)ol`;Sp*^ za%+Du97xyPFRt3II?#jxu5VOUhD*>q0H+H%Iv^rll!_qL&u@!V{3~fJIy!yNDwbK| ziy+6SE-Ul~nVZH{IxE=XYWfc91fRk)2wa^eF84_wMU~Z@pxV1(zoC=&v!}8b=G6XR zh;=C!&qqXA?gqsR01K5J0Y6?@5JZc1wtj?W{EA#S0D}3p$2=;lh{sCNMYjr#-h{r& zr5W-<1fs2lAO_@sh=6xwUdKlwPe{e;t|{uXnd;P%pR!ewIbFPlZJW_gTrH}VD{4q- z`%&{O1>RGfMuYvD?@o{~!Rqlvfed0-1?nzG%a=GJDCp&h4lUoj}`_tN6N~z zrv$2cbVE6F{Y2`D#;hPs+58! zFO1woe?FQbYj+qu&{YU@*Iu+GCQ4@;ry1ob;YwDW8JUGOC1B$9KnN?}OKVmd7Ky`}jh zin=Y zmwvyN!d>9#(T{Kgd}&s|18T!CCDH0(fZhS8*^xWaRgw#AcmzSPi1T}5c;?6n9xh=6 za4pjC7kXi`#9mfYsedR{wT5dk7tx;oc4-Zgs7D@3*MyTuJ2!%bUlmqjNH1zUyLJSiBMiiqyHJCpY%u3H zu2KQq+IrY7nOAkFMf3VLv$-~ULS%7EpT_inkZ&I9p3+Mv*)l!pES+##ts8kIE@u!M zTCNZV9|qxi>KBW;U8H)*F}v0W7}Z+iL*DW_DLwdZ6L+{@MV0 z&@zpBH*9{%oeL+S%}m-`nb@G(b#@CN+4(S6*Skp(Ubxv9Ro+YCrgtOeb9z@SR+j=I!oLq~J`Rp6KCu`Up&C+i>VcsYwRbC)R*(buhK>2toldUFTG8Nk zAGlw8grEO75{`e*O+=<2xc)F4<@1-928-Ncl5`b$g|7mRwj-!{bG}z{? zmE!1|EIBrTyjrdpIrnbd#S;m@o84+eNi|dFpdYK&Gqc1mM3m0SWoYY?XC}gXt*PA~-K^gc%aXuD`uExo0h<#JoXc-!i559v*Xd>H z-W4j|C5TG%hc;lyqfn8jqT3>cNNa}hqL^^M35o(~qA|VElH(Y(!Do0YNde@LRyZ;O zm%o5A5y}sxfs6iNzrsU0p_@+cAtFND#GamS$p;Rb9XB198$vfc`)e&+#h?0kzVp7D zPQAFLdReg_ZHDW-`mwM14mcd?Pg@(}&ny4_xmT{wj6+imYoM>OoG%i>A2YHvGI`Po z>g~#hU-?zQ7*Ze?Vs!|=FirqG5XYN921Fv9tXc2GWwxF&q8v;T#?r?hulsdP%u(ioqULU?cskc3rOs5;M;>I;9l$PV7qp7; ztti0J=@_D0KTqvExh_m{z?C(lscSC*&`ZK%U^D<3C`LNn;MZV%=2`JI{@3@#B~L>y zpam?D_vn7I!4LTGh3%OI4aWD&h64^;%oi>~Ov9cZajwZAt|wiy1*SYk>&flg%)agK z+(-Zu5;xc0?OU~&-HRO3)*nB4gCs{zoO#`D^CO2m*~88Cwz%`gxHaP_SN>-0B67|W ztAS>Co7^opDK#=^Gf#Wg)^_d_~Ia8;NlDa za^_cPGrHw=qEsT*zCrTETA-|c0I$?9_SX4uyq_`0d4Mw{;}fYnP8M!pWwBE!eQ<({ z2GsLb(7zD1>aR`C``+e`R7~J1PF{l5|4Rd1XpJ4?dr>_qlyHPaOIo6mR^kN+oE7 zy0sRkO6aTF-h;lRS_6CcMF9=hhix#|+kH8F=Hpn_gN=5+k;)p;7k|;Pa%#CBFYlO6 zMM1hlluF?N`Bn@s&URl`n5Nrv}7Jp173EoHQ0 zZeK+j-YWk&1L-Q<6#cnE0j^Odqo?A1fTIJzbAb)p!e{G3Qb>;06Ph0Hg4K5Vj5@TO zK!4;*YSe>~kTvuc8V>}1NHWrh+d87&5j}HFu3pexr&LGeQC210kO!HLNQ^hZz6j70e@?Ee*VO3kW3OSIKBI$()TAG%cAX3!)0n{qMe5;AatLj_s8) z=i8Wm*88?pIf7{Frynub%&YlHtc#WDuJ5lW`2C~gTZhWc4@2n0=T>SZZ_4~szg{pa zfBr=-pEK=0HkPHf<3CCa=3#qKQ2sbJx;HZ(RLRzyR8n*CUp)Qt0V!G-7rplA>3Bj^ z_;=EQl$MvZETc9$Ek4Rpg2Z@C8SjZdrF{ZX0V)_3n4Q<%<)#Zo%KT8S`cq=O7+axAMW z=+o~8v?nB*z2chCbmDbG3WbysOTBcX%0^Q9h3M3i3Lt*S#9F}R9HJ;?V5?wYwr(q^j@4myYN3xgE?ZBf`sk}4WgOPwCklfn-5c^}GD`!*lGN+}~qs>PPV-oYmd6_8KPsi$H1nad! z#ZI{Oe0_7yaiz@@0DbI9rqY2g9u{)u0-}V{=3#`N1vxFJZTNZ!e6a8*9Uv%Pg|0Y6 z$sK`TXDLekgFcWcTS)ndF?M z^Q6W%Zp${wWJ4?iuHQx*gpw{aPD3X_%h-cLCG1lp7a|(;y#Pr(E|Sc*40aJCd!HFo z3}@V276eO2)1q?^KVCo3Bx?N5hF_9|I>S3~5E1YYc(s=|;(0$CyR1qQDbufL`Ed+6 znW^3p5t2VTKe)Z!X|6$@f1&dNOpQpEydYcJjfU}M52eerz{2!*kWJALb766PLcB{D zoJ$}u5gl^;c`cP(ij#9~(URX}tOO^WI*}FsSlW9x8d^Z6L2Rzj1(M&rN&nk3O&-sM zzpp#^9D*q)D+|lGF~>rKZWqO!Y3h53U`L=YNflu$+LQ~As&!keS7glg_&#eFyiotnQ^Z3FhYMD0%H5sw@53X-ugz7fz#gR1nMGfkv+7A; z9(o=yoSreS``$p?fkE9oSeJp-F=N3dn5lz(7@*h?Y34Us|erFQFzVvR*^N z%M~NWn&F{J3g#DZf?LWK3X z=Lf^!z1Uwa^>6GN#sm*y4Y3EfFX!mQ>Qu+oX|#v*+y0yy(?uA~`x+d99UT1%&SIy9 zWR@A3pByzRJ&s15U?3Oc+}|Y$TJ0asnMs|>KI_nhm#S(s&R0RMxgBX&FT$8uqvmLZ z1{&L-0F0evEIk)A5jQy1eytltno4a-DM!llr(aOG#VdX0;Ke0hWf;Fei_=ME`h!ei zF|E6;G~v_l4Rry2jciAdpI#GLl)Q#fq$-?#GRV&_}3(0$_T!>+trK6P%nuY&=p%;*rdz=Dp}@bJ+7;m+k^HXSQ1 zc~?V)>`N8&vnMejVH&LfQAS*xD<*`D1Q~gP-OGW72GA#a0Hyth1f@YqBf8&A!rBxb zN{?lK@90IKxqCm67>qo^jbBoylT{E*Wo&(*Jh@u@(ZFl!b@@6Xwk%-e?4j!u8b?C! zT9>wYQrpwh=&<}Ra^r6~&VN81pve$;#of){-bIg3{CSZfG|kf~=OXl>naTZ)niZD1czB|kgXE7dy`J74~bHIEM z){`u?MpO%yxO|A0<>Zde($6VjK{3#`DKZj(LIT9Y*_2|c=y0MN{+dr-fGxO}HC~&` z$#i?`;#rr_Ml8Vd9N$rg9(yFedz&bL=qSreXR{(F-IUSIJZU~$&4G+eLyS$Efw!X& zL&SoRB)c^6(aL5XI|(bAUs^$mMS5;QTq=sy4ZT74kI96*R3;mq3FX@^+{WsZ(BDn0 z4~{we?Q7nj!E*H2yOQ5TmOR_`H@FB+tM;?jJpJN5Y`9zy8%wfRphp1ZT2vsOLr+2F zR5zbwoPo<IZ9OgL93%_)APTVtW{Ylh*q(K7T>VGXA@Ymp?;K z=kxTNtsiRgW8p1DAlH?@CEu_M~xZyrsoj=vsoJm%##mndIx4m`7MrvZ?lr_J~F1Ltig(a6)y`mJncJ{&LG$zlHQ9VIdYC~RqP51AiTsb#!7WAuB z^f$~~0cL?ns4uukdD3qh??4Q?JAYU96sIwTv+P@$Q3t;VEO-NdS~HVlp>3P~Nt9yIEptAqW$_f-wsj`0m)r3+g6IRI>NesA=DhLBG zwe`*Id2d3gYp8dPcHD!h;dKpS$z&@bXPcGL9XRNC^yM7-JzhNci7X~REuz#$4coGi z?|X7bwDDt9Ow7JnlRoOc6u+>Og$j0YlDXhOHNZ^F%_tE_Uge{)xY>;QY@-jeqF`Kr z{3GRqwdH(i{T~FHDisBQ#-jevhpt5OP0iq6DI~Z~dvl#fv{JU)LoDu<>&r{`r}tpF2PboR&jXqNj` z=)5L=+w95M+^_8_tW2G<4*bMf)FY1=eqT;>xJWZEHb>g&Sp84|klDH`b zKt}CzV_SDKO#N3Fg%JC_OsbQK8PbQnQMb6A1m?o%V_D56U6vrv`H3fXUF2VPt9Jti z@y)T2gx+8Pe)z2w`?bJZ1%%;`;MZyjdz*SqjS?C(7MB*EyV zVba>m%=??;*sq%}fm6144+9yyZ(D~8c2EJ9r5B$J%VtCwn7(UB3u637u>BeqPX!6Ub@Fu#04xqxY8~`cJGxzT{Jq@a# z*?_UOfeE9*yEnY>9~4(;(0OMj_ND4;ly2J8?;G9_sQD{Qr|^HN7B&h)?)HHDDa
-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 KQK?k23I8ue@a$gz diff --git a/doc/user/project/repository/branches/branch_rules.md b/doc/user/project/repository/branches/branch_rules.md index c9d44a338e1..b192e4d50e9 100644 --- a/doc/user/project/repository/branches/branch_rules.md +++ b/doc/user/project/repository/branches/branch_rules.md @@ -172,16 +172,10 @@ For additional information, see [Approval rules](../../merge_requests/approvals/ - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181370) in GitLab 17.9 with a flag named `branch_rule_squash_settings`. Disabled by default. - [Enabled on GitLab.com, GitLab Self-Managed, and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/issues/506542) in GitLab 17.10. +- [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/524860) in GitLab 17.11. Feature flag `branch_rule_squash_settings` removed. {{< /history >}} -{{< alert type="flag" >}} - -The availability of this feature is controlled by a feature flag. -For more information, see the history. - -{{< /alert >}} - Prerequisites: - You must have at least the Maintainer role for the project. diff --git a/lib/ci/pipeline_creation/inputs/string_input.rb b/lib/ci/pipeline_creation/inputs/string_input.rb index bb5ca29efc8..7ea9746ade5 100644 --- a/lib/ci/pipeline_creation/inputs/string_input.rb +++ b/lib/ci/pipeline_creation/inputs/string_input.rb @@ -54,7 +54,7 @@ module Ci override :coerced_value def coerced_value(value) - super.to_s + value.to_s end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 34c433d102a..1779817f9fb 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -96,6 +96,11 @@ msgid_plural "%d Approvals" msgstr[0] "" msgstr[1] "" +msgid "%d Artifact" +msgid_plural "%d Artifacts" +msgstr[0] "" +msgstr[1] "" + msgid "%d Module" msgid_plural "%d Modules" msgstr[0] "" @@ -4929,6 +4934,9 @@ msgstr "" msgid "AdminUsers|Access level" msgstr "" +msgid "AdminUsers|Access summary for %{userType} user" +msgstr "" + msgid "AdminUsers|Access the API" msgstr "" @@ -4989,6 +4997,9 @@ msgstr "" msgid "AdminUsers|Banned" msgstr "" +msgid "AdminUsers|Based on member role in groups and projects. %{linkStart}Learn more about member roles.%{linkEnd}" +msgstr "" + msgid "AdminUsers|Be added to groups and projects" msgstr "" @@ -5112,9 +5123,15 @@ msgstr "" msgid "AdminUsers|Full access to all groups, projects, users, features, and the Admin area. You cannot remove your own administrator access." msgstr "" +msgid "AdminUsers|Full read and write access." +msgstr "" + msgid "AdminUsers|Group Managed Account" msgstr "" +msgid "AdminUsers|Groups and project settings" +msgstr "" + msgid "AdminUsers|Here are some helpful links to help you manage your instance:" msgstr "" @@ -5154,12 +5171,18 @@ msgstr "" msgid "AdminUsers|Manage (accept/reject) pending user sign ups" msgstr "" +msgid "AdminUsers|May be directly added to groups and projects. %{linkStart}Learn more about auditor role.%{linkEnd}" +msgstr "" + msgid "AdminUsers|Name not matching" msgstr "" msgid "AdminUsers|New user" msgstr "" +msgid "AdminUsers|No access." +msgstr "" + msgid "AdminUsers|Owned groups will be left" msgstr "" @@ -5187,6 +5210,9 @@ msgstr "" msgid "AdminUsers|Reactivating a user will:" msgstr "" +msgid "AdminUsers|Read access to all groups and projects." +msgstr "" + msgid "AdminUsers|Read-only access to all groups and projects. No access to the Admin area by default." msgstr "" @@ -5205,12 +5231,18 @@ msgstr "" msgid "AdminUsers|Rejected users:" msgstr "" +msgid "AdminUsers|Requires at least Maintainer role in specific groups and projects." +msgstr "" + msgid "AdminUsers|Reset link will be generated and sent to the user. User will be forced to set the password on first sign in." msgstr "" msgid "AdminUsers|Restore user access to the account, including web, Git and API." msgstr "" +msgid "AdminUsers|Review and set Admin area access with a custom admin role." +msgstr "" + msgid "AdminUsers|Role Promotions" msgstr "" @@ -8940,9 +8972,6 @@ msgstr "" msgid "Authenticate user SSH keys without requiring additional configuration. Performance of GitLab can be improved by using the GitLab database instead. %{link_start}How do I configure authentication using the GitLab database? %{link_end}" msgstr "" -msgid "Authenticate with Docker Hub" -msgstr "" - msgid "Authenticate with GitHub" msgstr "" @@ -17353,6 +17382,9 @@ msgstr "" msgid "ContainerRegistry|There are no container images stored for this project" msgstr "" +msgid "ContainerRegistry|This container image tag cannot be overwritten or deleted." +msgstr "" + msgid "ContainerRegistry|This field is required." msgstr "" @@ -17410,6 +17442,9 @@ msgstr "" msgid "ContainerRegistry|You can add an image to this registry with the following commands:" msgstr "" +msgid "ContainerRegistry|immutable" +msgstr "" + msgid "ContainerRegistry|index" msgstr "" @@ -22685,9 +22720,6 @@ msgstr "" msgid "Do you want to remove this deploy key?" msgstr "" -msgid "Docker Hub pull rate limits begin April 1, 2025 and might affect CI/CD pipelines that pull Docker images. To prevent pipeline failures, configure the GitLab Dependency Proxy to authenticate with Docker Hub." -msgstr "" - msgid "Dockerfile" msgstr "" @@ -47351,9 +47383,6 @@ msgstr "" msgid "ProjectQualitySummary|Get insight into the overall percentage of tests in your project that succeed, fail and are skipped." msgstr "" -msgid "ProjectQualitySummary|Help us improve this page" -msgstr "" - msgid "ProjectQualitySummary|Latest pipeline results" msgstr "" @@ -47369,9 +47398,6 @@ msgstr "" msgid "ProjectQualitySummary|Measure of how much of your code is covered by tests." msgstr "" -msgid "ProjectQualitySummary|Provide feedback" -msgstr "" - msgid "ProjectQualitySummary|See full report" msgstr "" @@ -47399,9 +47425,6 @@ msgstr "" msgid "ProjectQualitySummary|The percentage of tests that succeed, fail, or are skipped." msgstr "" -msgid "ProjectQualitySummary|This page helps you understand the code testing trends for your project. Let us know how we can improve it!" -msgstr "" - msgid "ProjectQualitySummary|Violations" msgstr "" @@ -66405,6 +66428,9 @@ msgstr "" msgid "VirtualRegistries|Virtual Registries" msgstr "" +msgid "VirtualRegistry|Maven" +msgstr "" + msgid "VirtualRegistry|Virtual registries" msgstr "" diff --git a/spec/frontend/admin/users/components/user_type/user_type_selector_spec.js b/spec/frontend/admin/users/components/user_type/user_type_selector_spec.js index c6b8ff09b61..74f08e6fd7b 100644 --- a/spec/frontend/admin/users/components/user_type/user_type_selector_spec.js +++ b/spec/frontend/admin/users/components/user_type/user_type_selector_spec.js @@ -1,8 +1,18 @@ -import { GlFormRadioGroup, GlFormRadio, GlCard } from '@gitlab/ui'; +import { GlFormRadioGroup, GlFormRadio, GlCard, GlSprintf, GlLink, GlIcon } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import UserTypeSelector from '~/admin/users/components/user_type/user_type_selector.vue'; import { stubComponent } from 'helpers/stub_component'; import AdminRoleDropdown from 'ee_component/admin/users/components/user_type/admin_role_dropdown.vue'; +import AccessSummarySection from '~/admin/users/components/user_type/access_summary_section.vue'; +import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; + +const ADMIN_ROLE_ENABLED_FLAG = { customRoles: true, customAdminRoles: true }; +const ADMIN_ROLE_DISABLED_FLAGS = [ + { customRoles: false, customAdminRoles: false }, + { customRoles: true, customAdminRoles: false }, + { customRoles: false, customAdminRoles: true }, +]; +const ALL_FLAG_STATES = [ADMIN_ROLE_ENABLED_FLAG, ...ADMIN_ROLE_DISABLED_FLAGS]; describe('UserTypeSelector component', () => { let wrapper; @@ -12,10 +22,17 @@ describe('UserTypeSelector component', () => { isCurrentUser = false, licenseAllowsAuditorUser = true, adminRoleId = 1, + customRoles = true, + customAdminRoles = true, } = {}) => { wrapper = shallowMountExtended(UserTypeSelector, { propsData: { userType, isCurrentUser, licenseAllowsAuditorUser, adminRoleId }, + provide: { + glFeatures: { customRoles, customAdminRoles }, + }, stubs: { + AccessSummarySection, + GlSprintf, AdminRoleDropdown: stubComponent(AdminRoleDropdown, { props: ['roleId'] }), GlFormRadio: stubComponent(GlFormRadio, { template: `
@@ -32,6 +49,28 @@ describe('UserTypeSelector component', () => { const findRadioFor = (value) => wrapper.findByTestId(`user-type-${value}`); const findSummaryCard = () => wrapper.findComponent(GlCard); const findAdminRoleDropdown = () => wrapper.findComponent(AdminRoleDropdown); + const findSummarySectionAt = (index) => wrapper.findAllComponents(AccessSummarySection).at(index); + const findSummaryHeaderLabel = () => wrapper.findByTestId('summary-header').find('label'); + const findSummaryHeaderHelpText = () => wrapper.findByTestId('summary-header').find('p'); + + const expectRegularUserGroupSectionText = () => { + const listItems = findSummarySectionAt(1).findAll('li'); + + expect(listItems).toHaveLength(1); + expect(listItems.at(0).text()).toBe( + 'Based on member role in groups and projects. Learn more about member roles.', + ); + }; + + const expectAuditorGroupSectionText = () => { + const listItems = findSummarySectionAt(1).findAll('li'); + + expect(listItems).toHaveLength(2); + expect(listItems.at(0).text()).toBe('Read access to all groups and projects.'); + expect(listItems.at(1).text()).toMatchInterpolatedText( + 'May be directly added to groups and projects. Learn more about auditor role.', + ); + }; describe('user type radio group', () => { beforeEach(() => createWrapper()); @@ -73,15 +112,143 @@ describe('UserTypeSelector component', () => { }); }); - describe('access summary card', () => { - beforeEach(() => createWrapper()); + describe.each` + userType | helpLinkText | helpLinkUrl | expectGroupSectionTextFn + ${'regular'} | ${'Learn more about member roles.'} | ${'/help/user/permissions'} | ${expectRegularUserGroupSectionText} + ${'auditor'} | ${'Learn more about auditor role.'} | ${'/help/administration/auditor_users'} | ${expectAuditorGroupSectionText} + `( + 'access summary card for $userType user', + ({ userType, helpLinkText, helpLinkUrl, expectGroupSectionTextFn }) => { + describe('for all feature flag states', () => { + describe.each(ALL_FLAG_STATES)('for feature flag state %s', (state) => { + beforeEach(() => createWrapper({ ...state, userType })); - it('shows the card', () => { - expect(findSummaryCard().exists()).toBe(true); - }); + it('shows card', () => { + expect(findSummaryCard().exists()).toBe(true); + }); - it('shows admin role dropdown', () => { - expect(findAdminRoleDropdown().props('roleId')).toBe(1); + it('shows card header', () => { + expect(findSummaryHeaderLabel().text()).toBe( + `Access summary for ${capitalizeFirstCharacter(userType)} user`, + ); + }); + + it('shows admin section', () => { + expect(findSummarySectionAt(0).props()).toEqual({ + icon: 'admin', + headerText: 'Admin area', + }); + }); + + describe('group section', () => { + it('shows section', () => { + expect(findSummarySectionAt(1).props()).toEqual({ + icon: 'group', + headerText: 'Groups and projects', + }); + }); + + it('shows text', () => { + expectGroupSectionTextFn(); + }); + + it('shows docs link', () => { + const link = findSummarySectionAt(1).findComponent(GlLink); + + expect(link.text()).toBe(helpLinkText); + expect(link.props('href')).toBe(helpLinkUrl); + }); + }); + + describe('settings section', () => { + it('shows section', () => { + expect(findSummarySectionAt(2).props()).toEqual({ + icon: 'settings', + headerText: 'Groups and project settings', + }); + }); + + it('shows text', () => { + expect(findSummarySectionAt(2).text()).toContain( + 'Requires at least Maintainer role in specific groups and projects.', + ); + }); + }); + }); + }); + + describe('when admin role feature is enabled', () => { + beforeEach(() => createWrapper({ ...ADMIN_ROLE_ENABLED_FLAG, userType: 'regular' })); + + it('shows header help text', () => { + expect(findSummaryHeaderHelpText().text()).toBe( + 'Review and set Admin area access with a custom admin role.', + ); + }); + + it('shows admin role dropdown in admin section', () => { + expect(findSummarySectionAt(0).findComponent(AdminRoleDropdown).props('roleId')).toBe(1); + }); + }); + + describe('for disabled admin role feature', () => { + describe.each(ADMIN_ROLE_DISABLED_FLAGS)('for feature flag state %s', (state) => { + beforeEach(() => createWrapper({ ...state, userType: 'regular' })); + + it('does not show header help text', () => { + expect(findSummaryHeaderHelpText().exists()).toBe(false); + }); + + it('does not show admin role dropdown', () => { + expect(findAdminRoleDropdown().exists()).toBe(false); + }); + + it('shows no access text in admin section', () => { + const listItems = findSummarySectionAt(0).findAll('li'); + + expect(listItems).toHaveLength(1); + expect(listItems.at(0).text()).toBe('No access.'); + }); + }); + }); + }, + ); + + describe('access summary card for admin user', () => { + describe.each(ALL_FLAG_STATES)('for feature flags %s', (state) => { + beforeEach(() => createWrapper({ ...state, userType: 'admin' })); + + it('shows header', () => { + expect(findSummaryHeaderLabel().text()).toBe('Access summary for Administrator user'); + }); + + it('does not show help text', () => { + expect(findSummaryHeaderHelpText().exists()).toBe(false); + }); + + describe.each` + section | index + ${'admin'} | ${0} + ${'group'} | ${1} + ${'settings'} | ${2} + `('for $section section', ({ index }) => { + let sectionContents; + + beforeEach(() => { + sectionContents = findSummarySectionAt(index).find('div'); + }); + + it('shows check icon', () => { + expect(sectionContents.findComponent(GlIcon).props()).toMatchObject({ + name: 'check', + variant: 'success', + }); + }); + + it('shows full access text', () => { + expect(sectionContents.text()).toBe('Full read and write access.'); + }); + }); }); }); diff --git a/spec/frontend/ci/job_details/mock_data.js b/spec/frontend/ci/job_details/mock_data.js index 5bc517fe0f9..3e98940ad59 100644 --- a/spec/frontend/ci/job_details/mock_data.js +++ b/spec/frontend/ci/job_details/mock_data.js @@ -130,14 +130,3 @@ export const mockPipelineVariablesPermissions = (value) => ({ }, }, }); - -export const minimumRoleResponse = { - data: { - project: { - id: mockId, - ciCdSettings: { - pipelineVariablesMinimumOverrideRole: 'developer', - }, - }, - }, -}; diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js index 88fff395d2b..8dd7090df62 100644 --- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js +++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js @@ -18,10 +18,6 @@ import createPipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/muta import updatePipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql'; import getPipelineVariablesMinimumOverrideRoleQuery from '~/ci/pipeline_variables_minimum_override_role/graphql/queries/get_pipeline_variables_minimum_override_role_project_setting.query.graphql'; import getPipelineSchedulesQuery from '~/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql'; -import { - mockPipelineVariablesPermissions, - minimumRoleResponse, -} from 'jest/ci/job_details/mock_data'; import { timezoneDataFixture } from '../../../vue_shared/components/timezone_dropdown/helpers'; import { createScheduleMutationResponse, @@ -65,7 +61,6 @@ describe('Pipeline schedules form', () => { dailyLimit, settingsLink: '', schedulesPath: '/root/ci-project/-/pipeline_schedules', - userRole: 'maintainer', }; const querySuccessHandler = jest.fn().mockResolvedValue(mockSinglePipelineScheduleNode); @@ -76,11 +71,9 @@ describe('Pipeline schedules form', () => { const updateMutationHandlerSuccess = jest.fn().mockResolvedValue(updateScheduleMutationResponse); const updateMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error')); - const minimumRoleHandler = jest.fn().mockResolvedValue(minimumRoleResponse); - const createMockApolloProvider = ( requestHandlers = [ - [getPipelineVariablesMinimumOverrideRoleQuery, minimumRoleHandler], + [getPipelineVariablesMinimumOverrideRoleQuery], [createPipelineScheduleMutation, createMutationHandlerSuccess], ], ) => { @@ -89,15 +82,16 @@ describe('Pipeline schedules form', () => { const createComponent = ({ editing = false, - pipelineVariablesPermissionsMixin = mockPipelineVariablesPermissions(true), requestHandlers, ciInputsForPipelines = false, + canSetPipelineVariables = true, } = {}) => { wrapper = shallowMountExtended(PipelineSchedulesForm, { propsData: { timezoneData: timezoneDataFixture, refParam: 'master', editing, + canSetPipelineVariables, }, provide: { ...defaultProvide, @@ -105,7 +99,7 @@ describe('Pipeline schedules form', () => { ciInputsForPipelines, }, }, - mixins: [glFeatureFlagMixin(), pipelineVariablesPermissionsMixin], + mixins: [glFeatureFlagMixin()], apolloProvider: createMockApolloProvider(requestHandlers), }); }; @@ -205,7 +199,7 @@ describe('Pipeline schedules form', () => { it('does not display variable list when the user has no permissions', () => { createComponent({ - pipelineVariablesPermissionsMixin: mockPipelineVariablesPermissions(false), + canSetPipelineVariables: false, }); expect(findPipelineVariables().exists()).toBe(false); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js index 93d09871344..b3db2b55872 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js @@ -58,8 +58,13 @@ describe('tags list row', () => { const getTooltipFor = (component) => getBinding(component.element, 'gl-tooltip'); const findProtectedBadge = () => wrapper.findByTestId('protected-badge'); const findProtectedPopover = () => wrapper.findByTestId('protected-popover'); + const findImmutableBadge = () => wrapper.findByTestId('immutable-badge'); + const findImmutablePopover = () => wrapper.findByTestId('immutable-popover'); - const mountComponent = (propsData = defaultProps) => { + const mountComponent = ( + propsData = defaultProps, + { immutableTagsFeatureFlagState = false } = {}, + ) => { wrapper = shallowMountExtended(TagsListRow, { stubs: { GlSprintf, @@ -70,6 +75,11 @@ describe('tags list row', () => { }, propsData, directives: { GlTooltip: createMockDirective('gl-tooltip') }, + provide: { + glFeatures: { + containerRegistryImmutableTags: immutableTagsFeatureFlagState, + }, + }, }); }; @@ -191,7 +201,7 @@ describe('tags list row', () => { }, }); - expect(findProtectedBadge().exists()).toBe(true); + expect(findProtectedBadge().text()).toBe('protected'); }); it('has the correct text for the popover', () => { @@ -214,6 +224,56 @@ describe('tags list row', () => { }); }); + describe('immutable tag', () => { + const immutableProtection = { + minimumAccessLevelForDelete: null, + minimumAccessLevelForPush: null, + immutable: true, + }; + + it('hidden if tag.protection does not exists', () => { + mountComponent(defaultProps, { immutableTagsFeatureFlagState: true }); + + expect(findImmutableBadge().exists()).toBe(false); + }); + + it('displays if tag.protection.immutable exists', () => { + mountComponent( + { + ...defaultProps, + tag: { + ...tag, + protection: { + ...immutableProtection, + }, + }, + }, + { immutableTagsFeatureFlagState: true }, + ); + + expect(findImmutableBadge().text()).toBe('immutable'); + }); + + it('has the correct text for the popover', () => { + mountComponent( + { + ...defaultProps, + tag: { + ...tag, + protection: { + ...immutableProtection, + }, + }, + }, + { immutableTagsFeatureFlagState: true }, + ); + + const popoverText = findImmutablePopover().text(); + + expect(popoverText).toBe('This container image tag cannot be overwritten or deleted.'); + }); + }); + describe('warning icon', () => { it('is normally hidden', () => { mountComponent(); diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js index 5a0e05e9186..831d1e9e212 100644 --- a/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js +++ b/spec/frontend/packages_and_registries/container_registry/explorer/mock_data.js @@ -200,10 +200,7 @@ export const tagsMock = [ userPermissions: { destroyContainerRepositoryTag: true, }, - protection: { - minimumAccessLevelForPush: null, - minimumAccessLevelForDelete: null, - }, + protection: null, __typename: 'ContainerRepositoryTag', }, { @@ -221,10 +218,7 @@ export const tagsMock = [ userPermissions: { destroyContainerRepositoryTag: true, }, - protection: { - minimumAccessLevelForPush: null, - minimumAccessLevelForDelete: null, - }, + protection: null, __typename: 'ContainerRepositoryTag', }, { @@ -242,10 +236,7 @@ export const tagsMock = [ userPermissions: { destroyContainerRepositoryTag: true, }, - protection: { - minimumAccessLevelForPush: null, - minimumAccessLevelForDelete: null, - }, + protection: null, __typename: 'ContainerRepositoryTag', }, { @@ -263,10 +254,7 @@ export const tagsMock = [ userPermissions: { destroyContainerRepositoryTag: true, }, - protection: { - minimumAccessLevelForPush: null, - minimumAccessLevelForDelete: null, - }, + protection: null, __typename: 'ContainerRepositoryTag', }, ]; diff --git a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js index c2d4e340dd4..3221041d1a0 100644 --- a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js +++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js @@ -86,7 +86,7 @@ describe('View branch rules', () => { const { bindInternalEventDocument } = useMockInternalEventsTracking(); const createComponent = async ({ - glFeatures = { editBranchRules: true, branchRuleSquashSettings: true }, + glFeatures = { editBranchRules: true }, canAdminProtectedBranches = true, allowEditSquashSetting = true, branchRulesQueryHandler = branchRulesMockRequestHandler, @@ -171,12 +171,6 @@ describe('View branch rules', () => { ); describe('Squash settings', () => { - it('does not render squash settings section when feature flag is disabled', async () => { - await createComponent({ glFeatures: { branchRuleSquashSettings: false } }); - - expect(findSquashSettingSection().exists()).toBe(false); - }); - it.each` scenario | canAdminProtectedBranches | expectedIsEditAvailable | description ${'user has permission'} | ${true} | ${true} | ${'shows edit button'} @@ -185,7 +179,6 @@ describe('View branch rules', () => { '$description when $scenario', async ({ canAdminProtectedBranches, expectedIsEditAvailable }) => { await createComponent({ - glFeatures: { branchRuleSquashSettings: true }, canAdminProtectedBranches, }); @@ -204,7 +197,6 @@ describe('View branch rules', () => { jest.spyOn(util, 'getParameterByName').mockReturnValueOnce(branch); await createComponent({ - glFeatures: { branchRuleSquashSettings: true }, canAdminProtectedBranches: true, allowEditSquashSetting, }); @@ -329,9 +321,7 @@ describe('View branch rules', () => { `('$description', async ({ branch, expectedExists }) => { jest.spyOn(util, 'getParameterByName').mockReturnValueOnce(branch); - await createComponent({ - glFeatures: { branchRuleSquashSettings: true }, - }); + await createComponent(); expect(findSquashSettingSection().exists()).toBe(expectedExists); }); @@ -603,7 +593,9 @@ describe('View branch rules', () => { }); it('does not render Protect Branch section', () => { - expect(findSettingsSection().exists()).toBe(false); + const sections = wrapper.findAllComponents(SettingsSection); + expect(sections).toHaveLength(1); + expect(sections.at(0).attributes('heading')).toBe('Merge requests'); }); }); diff --git a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js index 906e751f0b8..e2c486708ff 100644 --- a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js +++ b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js @@ -19,14 +19,13 @@ describe('Branch rule', () => { let wrapper; const squashOptionMockRequestHandler = jest.fn().mockResolvedValue(squashOptionMockResponse); - const createComponent = async (props = {}, features = { branchRuleSquashSettings: false }) => { + const createComponent = async (props = {}) => { const fakeApollo = createMockApollo([[squashOptionQuery, squashOptionMockRequestHandler]]); wrapper = shallowMountExtended(BranchRule, { apolloProvider: fakeApollo, provide: { ...branchRuleProvideMock, - glFeatures: features, }, stubs: { ProtectedBadge, @@ -92,12 +91,12 @@ describe('Branch rule', () => { }); describe('squash settings', () => { - it('renders squash settings when branchRuleSquashSettings is true', async () => { + it('renders squash settings', async () => { const branchRuleProps = { ...branchRulePropsMock, }; - await createComponent(branchRuleProps, { branchRuleSquashSettings: true }); + await createComponent(branchRuleProps); expect(findProtectionDetailsListItems().at(2).text()).toBe('Squash commits: Encourage'); }); }); diff --git a/spec/graphql/resolvers/namespace_projects_resolver_spec.rb b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb index c05dc6eae33..f643753bdc8 100644 --- a/spec/graphql/resolvers/namespace_projects_resolver_spec.rb +++ b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb @@ -129,6 +129,52 @@ RSpec.describe Resolvers::NamespaceProjectsResolver, feature_category: :groups_a end end + context 'full path sorting' do + let_it_be(:parent_group) { create(:group) } + let_it_be(:nested_group_1) { create(:group, parent: parent_group, path: 'alpha') } + let_it_be(:nested_group_2) { create(:group, parent: parent_group, path: 'beta') } + let_it_be(:deeply_nested_group) { create(:group, parent: nested_group_1, path: 'gamma') } + + let_it_be(:projects_with_various_paths) do + [ + create(:project, path: 'zebra', namespace: parent_group), + create(:project, path: 'apple', namespace: nested_group_1), + create(:project, path: 'banana', namespace: nested_group_2), + create(:project, path: 'cherry', namespace: deeply_nested_group) + ] + end + + let(:namespace) { parent_group } + let(:args) { default_args.merge(include_subgroups: true, sort: :full_path_asc) } + let(:project_full_paths) { subject.map(&:full_path) } + + before_all do + projects_with_various_paths.each { |p| p.add_developer(current_user) } + end + + it 'returns projects sorted by full path in ascending order' do + expect(project_full_paths).to eq([ + "#{parent_group.path}/alpha/apple", + "#{parent_group.path}/alpha/gamma/cherry", + "#{parent_group.path}/beta/banana", + "#{parent_group.path}/zebra" + ]) + end + + context 'when sorting by full path in descending order' do + let(:args) { default_args.merge(include_subgroups: true, sort: :full_path_desc) } + + it 'returns projects sorted by full path in descending order' do + expect(project_full_paths).to eq([ + "#{parent_group.path}/zebra", + "#{parent_group.path}/beta/banana", + "#{parent_group.path}/alpha/gamma/cherry", + "#{parent_group.path}/alpha/apple" + ]) + end + end + end + context 'ids filtering' do let(:args) { default_args.merge(include_subgroups: false) } diff --git a/spec/graphql/types/namespace_project_sort_enum_spec.rb b/spec/graphql/types/namespace_project_sort_enum_spec.rb index c9c821a9fa6..a8ac907e8af 100644 --- a/spec/graphql/types/namespace_project_sort_enum_spec.rb +++ b/spec/graphql/types/namespace_project_sort_enum_spec.rb @@ -14,6 +14,8 @@ RSpec.describe GitlabSchema.types['NamespaceProjectSort'], feature_category: :gr STORAGE_SIZE_DESC PATH_ASC PATH_DESC + FULL_PATH_ASC + FULL_PATH_DESC REPOSITORY_SIZE_ASC REPOSITORY_SIZE_DESC SNIPPETS_SIZE_ASC diff --git a/spec/helpers/ci/pipeline_schedules_helper_spec.rb b/spec/helpers/ci/pipeline_schedules_helper_spec.rb index 6ff2b09e8ab..f0f9e0bc840 100644 --- a/spec/helpers/ci/pipeline_schedules_helper_spec.rb +++ b/spec/helpers/ci/pipeline_schedules_helper_spec.rb @@ -15,30 +15,6 @@ RSpec.describe Ci::PipelineSchedulesHelper, feature_category: :continuous_integr describe '#js_pipeline_schedules_form_data' do before do allow(helper).to receive_messages(timezone_data: timezones, current_user: user, can_view_pipeline_editor?: true) - allow(project.team).to receive(:human_max_access).with(user.id).and_return('Owner') - end - - describe 'user_role' do - context 'when there is no current user' do - before do - allow(helper).to receive(:current_user).and_return(nil) - end - - it 'is nil' do - expect(helper.js_pipeline_schedules_form_data(project, pipeline_schedule)[:user_role]).to be_nil - end - end - - context 'when there is a current_user' do - before do - allow(helper).to receive(:current_user).and_return(user) - allow(project.team).to receive(:human_max_access).with(user.id).and_return('Developer') - end - - it "returns the human readable access level that the current user has in the project" do - expect(helper.js_pipeline_schedules_form_data(project, pipeline_schedule)[:user_role]).to eq('Developer') - end - end end it 'returns pipeline schedule form data' do @@ -51,7 +27,7 @@ RSpec.describe Ci::PipelineSchedulesHelper, feature_category: :continuous_integr schedules_path: pipeline_schedules_path(project), settings_link: project_settings_ci_cd_path(project), timezone_data: timezones.to_json, - user_role: 'Owner' + can_set_pipeline_variables: 'false' }) end end diff --git a/spec/lib/ci/pipeline_creation/inputs/spec_inputs_spec.rb b/spec/lib/ci/pipeline_creation/inputs/spec_inputs_spec.rb index 6d6d97d9e43..18e0fd3a25f 100644 --- a/spec/lib/ci/pipeline_creation/inputs/spec_inputs_spec.rb +++ b/spec/lib/ci/pipeline_creation/inputs/spec_inputs_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'fast_spec_helper' +require 'oj' require_relative Rails.root.join('lib/ci/pipeline_creation/inputs/spec_inputs.rb') RSpec.describe Ci::PipelineCreation::Inputs::SpecInputs, feature_category: :pipeline_composition do diff --git a/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb b/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb index ba23a6c8df7..c8dd2ad00a8 100644 --- a/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/inputs_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'fast_spec_helper' +require 'oj' require_relative Rails.root.join('lib/gitlab/ci/config/interpolation/inputs.rb') RSpec.describe Gitlab::Ci::Config::Interpolation::Inputs, feature_category: :pipeline_composition do @@ -195,6 +196,16 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::Inputs, feature_category: :pip expect(inputs.to_hash).to eq(foo: 'bar') end end + + context 'when a hash value is passed as string' do + let(:specs) { { test_input: { type: 'string' } } } + let(:args) { { test_input: '{"key": "value"}' } } + + it 'is valid and behaves like an unparsed string' do + expect(inputs).to be_valid + expect(inputs.to_hash).to eq(test_input: '{"key": "value"}') + end + end end describe 'number validation' do diff --git a/spec/models/concerns/gitlab/encrypted_attribute_spec.rb b/spec/models/concerns/gitlab/encrypted_attribute_spec.rb index 133d8da7c0f..04770b85ccd 100644 --- a/spec/models/concerns/gitlab/encrypted_attribute_spec.rb +++ b/spec/models/concerns/gitlab/encrypted_attribute_spec.rb @@ -27,8 +27,8 @@ RSpec.describe Gitlab::EncryptedAttribute, feature_category: :shared do record.attr_encrypted_attributes[:token][:operation] = :encrypting end - it 'returns correct secret' do - expect(record.__send__(key_method, :token)) + it 'returns the encryption key secret' do + expect(record.__send__(key_method)) .to eq(Gitlab::Encryption::KeyProvider[key_method].encryption_key.secret) end end @@ -38,9 +38,9 @@ RSpec.describe Gitlab::EncryptedAttribute, feature_category: :shared do record.attr_encrypted_attributes[:token][:operation] = :decrypting end - it 'returns correct secrets' do - expect(record.__send__(key_method, :token)) - .to eq(Gitlab::Encryption::KeyProvider[key_method].decryption_keys.map(&:secret)) + it 'returns the encryption key secret' do + expect(record.__send__(key_method)) + .to eq(Gitlab::Encryption::KeyProvider[key_method].encryption_key.secret) end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1b53d3f0d31..a849420c004 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2355,6 +2355,18 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr expect(projects).to eq([project1, project2, project3].sort_by(&:path).reverse) end + it 'reorders the input relation by full path asc' do + projects = described_class.sort_by_attribute(:full_path_asc) + + expect(projects).to eq([project1, project2, project3].sort_by(&:full_path)) + end + + it 'reorders the input relation by full path desc' do + projects = described_class.sort_by_attribute(:full_path_desc) + + expect(projects).to eq([project1, project2, project3].sort_by(&:full_path).reverse) + end + context 'with project_statistics' do describe '.sort_by_attribute with project_statistics' do def create_project_statistics_with_size(project, size) diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 6885ed44339..5c6f7437fed 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -992,63 +992,79 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do context 'when `pipeline_variables_minimum_override_role` is defined' do using RSpec::Parameterized::TableSyntax - where(:user_role, :minimum_role, :restrict_variables, :allowed) do - :developer | :no_one_allowed | true | false - :maintainer | :no_one_allowed | true | false - :owner | :no_one_allowed | true | false - :guest | :no_one_allowed | true | false - :planner | :no_one_allowed | true | false - :reporter | :no_one_allowed | true | false - :anonymous | :no_one_allowed | true | false - :developer | :developer | true | true - :maintainer | :developer | true | true - :owner | :developer | true | true - :guest | :developer | true | true - :planner | :developer | true | true - :reporter | :developer | true | true - :anonymous | :developer | true | true - :developer | :maintainer | true | false - :maintainer | :maintainer | true | true - :owner | :maintainer | true | true - :guest | :maintainer | true | false - :planner | :maintainer | true | false - :reporter | :maintainer | true | false - :anonymous | :maintainer | true | false - :developer | :owner | true | false - :maintainer | :owner | true | false - :owner | :owner | true | true - :guest | :owner | true | false - :planner | :owner | true | false - :reporter | :owner | true | false - :anonymous | :owner | true | false - :developer | :no_one_allowed | false | true - :maintainer | :no_one_allowed | false | true - :owner | :no_one_allowed | false | true - :guest | :no_one_allowed | false | true - :planner | :no_one_allowed | false | true - :reporter | :no_one_allowed | false | true - :anonymous | :no_one_allowed | false | true - :developer | :developer | false | true - :maintainer | :developer | false | true - :owner | :developer | false | true - :guest | :developer | false | true - :planner | :developer | false | true - :reporter | :developer | false | true - :anonymous | :developer | false | true - :developer | :maintainer | false | true - :maintainer | :maintainer | false | true - :owner | :maintainer | false | true - :guest | :maintainer | false | true - :planner | :maintainer | false | true - :reporter | :maintainer | false | true - :anonymous | :maintainer | false | true - :developer | :owner | false | true - :maintainer | :owner | false | true - :owner | :owner | false | true - :guest | :owner | false | true - :planner | :owner | false | true - :reporter | :owner | false | true - :anonymous | :owner | false | true + where(:user_role, :admin_mode, :minimum_role, :restrict_variables, :allowed) do + :developer | false | :no_one_allowed | true | false + :maintainer | false | :no_one_allowed | true | false + :owner | false | :no_one_allowed | true | false + :guest | false | :no_one_allowed | true | false + :planner | false | :no_one_allowed | true | false + :reporter | false | :no_one_allowed | true | false + :anonymous | false | :no_one_allowed | true | false + :developer | false | :developer | true | true + :maintainer | false | :developer | true | true + :owner | false | :developer | true | true + :guest | false | :developer | true | true + :planner | false | :developer | true | true + :reporter | false | :developer | true | true + :anonymous | false | :developer | true | true + :developer | false | :maintainer | true | false + :maintainer | false | :maintainer | true | true + :owner | false | :maintainer | true | true + :guest | false | :maintainer | true | false + :planner | false | :maintainer | true | false + :reporter | false | :maintainer | true | false + :anonymous | false | :maintainer | true | false + :developer | false | :owner | true | false + :maintainer | false | :owner | true | false + :owner | false | :owner | true | true + :guest | false | :owner | true | false + :planner | false | :owner | true | false + :reporter | false | :owner | true | false + :anonymous | false | :owner | true | false + :developer | false | :no_one_allowed | false | true + :maintainer | false | :no_one_allowed | false | true + :owner | false | :no_one_allowed | false | true + :guest | false | :no_one_allowed | false | true + :planner | false | :no_one_allowed | false | true + :reporter | false | :no_one_allowed | false | true + :anonymous | false | :no_one_allowed | false | true + :developer | false | :developer | false | true + :maintainer | false | :developer | false | true + :owner | false | :developer | false | true + :guest | false | :developer | false | true + :planner | false | :developer | false | true + :reporter | false | :developer | false | true + :anonymous | false | :developer | false | true + :developer | false | :maintainer | false | true + :maintainer | false | :maintainer | false | true + :owner | false | :maintainer | false | true + :guest | false | :maintainer | false | true + :planner | false | :maintainer | false | true + :reporter | false | :maintainer | false | true + :anonymous | false | :maintainer | false | true + :developer | false | :owner | false | true + :maintainer | false | :owner | false | true + :owner | false | :owner | false | true + :guest | false | :owner | false | true + :planner | false | :owner | false | true + :reporter | false | :owner | false | true + :anonymous | false | :owner | false | true + :admin | false | :no_one_allowed | false | true + :admin | false | :owner | false | true + :admin | false | :maintainer | false | true + :admin | false | :developer | false | true + :admin | false | :no_one_allowed | true | false + :admin | false | :owner | true | false + :admin | false | :maintainer | true | false + :admin | false | :developer | true | true + :admin | true | :no_one_allowed | false | true + :admin | true | :developer | false | true + :admin | true | :maintainer | false | true + :admin | true | :owner | false | true + :admin | true | :no_one_allowed | true | false + :admin | true | :developer | true | true + :admin | true | :maintainer | true | true + :admin | true | :owner | true | true end with_them do let(:current_user) { public_send(user_role) } @@ -1058,6 +1074,8 @@ RSpec.describe ProjectPolicy, feature_category: :system_access do ci_cd_settings[:pipeline_variables_minimum_override_role] = minimum_role ci_cd_settings[:restrict_user_defined_variables] = restrict_variables ci_cd_settings.save! + + enable_admin_mode!(current_user) if admin_mode end it 'allows/disallows set pipeline variables based on project defined minimum role' do diff --git a/spec/requests/api/ci/pipelines_spec.rb b/spec/requests/api/ci/pipelines_spec.rb index c1025f34ca2..4fd0615d58f 100644 --- a/spec/requests/api/ci/pipelines_spec.rb +++ b/spec/requests/api/ci/pipelines_spec.rb @@ -889,10 +889,14 @@ RSpec.describe API::Ci::Pipelines, feature_category: :continuous_integration do { 'Content-Type' => 'application/x-www-form-urlencoded' } end + let(:transformed_values) do + inputs.transform_values { |value| value.is_a?(String) ? value : value.to_json } + end + subject(:post_request) do post api("/projects/#{project.id}/pipeline", user), headers: headers, - params: { ref: project.default_branch, inputs: inputs.transform_values(&:to_json) } + params: { ref: project.default_branch, inputs: transformed_values } end it_behaves_like 'creating a succesful pipeline' diff --git a/spec/requests/api/ci/triggers_spec.rb b/spec/requests/api/ci/triggers_spec.rb index 330e271d3a1..0311666f3ce 100644 --- a/spec/requests/api/ci/triggers_spec.rb +++ b/spec/requests/api/ci/triggers_spec.rb @@ -237,10 +237,14 @@ RSpec.describe API::Ci::Triggers, feature_category: :pipeline_composition do { 'Content-Type' => 'application/x-www-form-urlencoded' } end + let(:transformed_values) do + inputs.transform_values { |value| value.is_a?(String) ? value : value.to_json } + end + subject(:post_request) do post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{token}"), headers: headers, - params: { ref: 'refs/heads/other-branch', inputs: inputs.transform_values(&:to_json) } + params: { ref: 'refs/heads/other-branch', inputs: transformed_values } end it_behaves_like 'creating a succesful pipeline' diff --git a/spec/requests/api/graphql/mutations/projects/branch_rules/squash_options/update_spec.rb b/spec/requests/api/graphql/mutations/projects/branch_rules/squash_options/update_spec.rb index 65495371e99..7b5c25c5ca7 100644 --- a/spec/requests/api/graphql/mutations/projects/branch_rules/squash_options/update_spec.rb +++ b/spec/requests/api/graphql/mutations/projects/branch_rules/squash_options/update_spec.rb @@ -40,17 +40,6 @@ RSpec.describe 'Updating a squash option', feature_category: :source_code_manage project.add_maintainer(current_user) end - context 'and the branch_rule_squash_settings feature flag is disabled' do - before do - stub_feature_flags(branch_rule_squash_settings: false) - end - - it 'raises an error' do - mutation_request - expect(graphql_errors).to include(a_hash_including('message' => 'Squash options feature disabled')) - end - end - it 'updates the squash option' do expect do mutation_request diff --git a/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb b/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb index c45b8da2792..d7672cc5116 100644 --- a/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb +++ b/spec/requests/api/graphql/project/branch_protections/merge_access_levels_spec.rb @@ -4,12 +4,4 @@ require 'spec_helper' RSpec.describe 'getting merge access levels for a branch protection', feature_category: :source_code_management do it_behaves_like 'a GraphQL query for access levels', :merge - - context 'when the branch_rule_squash_settings not enabled' do - before do - stub_feature_flags(branch_rule_squash_settings: false) - end - - it_behaves_like 'a GraphQL query for access levels', :merge - end end diff --git a/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb b/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb index 28594b61d2e..65b9bc93a6b 100644 --- a/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb +++ b/spec/requests/api/graphql/project/branch_protections/push_access_levels_spec.rb @@ -4,12 +4,4 @@ require 'spec_helper' RSpec.describe 'getting push access levels for a branch protection', feature_category: :source_code_management do it_behaves_like 'a GraphQL query for access levels', :push - - context 'when the branch_rule_squash_settings not enabled' do - before do - stub_feature_flags(branch_rule_squash_settings: false) - end - - it_behaves_like 'a GraphQL query for access levels', :push - end end diff --git a/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb b/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb index 37939404634..bc699904db5 100644 --- a/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb +++ b/spec/requests/api/graphql/project/branch_rules/branch_protection_spec.rb @@ -33,44 +33,28 @@ RSpec.describe 'getting branch protection for a branch rule', feature_category: GQL end - shared_examples_for 'branch protection graphql query' do - context 'when the user does not have read_protected_branch abilities' do - before do - project.add_guest(current_user) - post_graphql(query, current_user: current_user, variables: variables) - end - - it_behaves_like 'a working graphql query' - - it { expect(branch_protection_data).not_to be_present } + context 'when the user does not have read_protected_branch abilities' do + before do + project.add_guest(current_user) + post_graphql(query, current_user: current_user, variables: variables) end - context 'when the user does have read_protected_branch abilities' do - before do - project.add_maintainer(current_user) - post_graphql(query, current_user: current_user, variables: variables) - end + it_behaves_like 'a working graphql query' - it_behaves_like 'a working graphql query' - - it 'includes allow_force_push' do - expect(branch_protection_data['allowForcePush']).to be_in([true, false]) - expect(branch_protection_data['allowForcePush']).to eq(branch_rule.allow_force_push) - end - end + it { expect(branch_protection_data).not_to be_present } end - it_behaves_like 'branch protection graphql query' - - context 'when the branch_rule_squash_settings flag is not enabled' do + context 'when the user does have read_protected_branch abilities' do before do - stub_feature_flags(branch_rule_squash_settings: false) + project.add_maintainer(current_user) + post_graphql(query, current_user: current_user, variables: variables) end - it_behaves_like 'branch protection graphql query' do - let(:branch_protection_data) do - graphql_data_at('project', 'branchRules', 'nodes', 0, 'branchProtection') - end + it_behaves_like 'a working graphql query' + + it 'includes allow_force_push' do + expect(branch_protection_data['allowForcePush']).to be_in([true, false]) + expect(branch_protection_data['allowForcePush']).to eq(branch_rule.allow_force_push) end end end diff --git a/spec/requests/api/graphql/project/branch_rules_spec.rb b/spec/requests/api/graphql/project/branch_rules_spec.rb index d948d0a01bc..397c1000797 100644 --- a/spec/requests/api/graphql/project/branch_rules_spec.rb +++ b/spec/requests/api/graphql/project/branch_rules_spec.rb @@ -99,6 +99,9 @@ RSpec.describe 'getting list of branch rules for a project', feature_category: : describe 'response' do let_it_be(:branch_name_a) { TestEnv::BRANCH_SHA.each_key.first } + let(:branch_rule_a_data) { branch_rules_data.dig(2, 'node') } + let(:branch_rule_b_data) { branch_rules_data.dig(1, 'node') } + let(:all_branches_rule_data) { branch_rules_data.dig(0, 'node') } let_it_be(:branch_name_b) { 'diff-*' } let_it_be(:protected_branch_a) do create(:protected_branch, project: project, name: branch_name_a) @@ -119,85 +122,46 @@ RSpec.describe 'getting list of branch rules for a project', feature_category: : end end - let(:branch_rule_squash_settings) { true } - before do - stub_feature_flags(branch_rule_squash_settings: branch_rule_squash_settings) post_graphql(query, current_user: current_user, variables: variables) end it_behaves_like 'a working graphql query' - context 'when the branch_rule_squash_settings flag is enabled' do - let(:all_branches_rule_data) { branch_rules_data.dig(0, 'node') } - let(:branch_rule_b_data) { branch_rules_data.dig(1, 'node') } - let(:branch_rule_a_data) { branch_rules_data.dig(2, 'node') } + it 'includes all fields', :use_sql_query_cache, :aggregate_failures do + expect(all_branches_rule_data).to include( + 'id' => all_branches_rule.to_global_id.to_s, + 'name' => 'All branches', + 'isDefault' => false, + 'isProtected' => false, + 'matchingBranchesCount' => project.repository.branch_count, + 'branchProtection' => nil, + "squashOption" => be_kind_of(Hash), + 'createdAt' => nil, + 'updatedAt' => nil + ) - it 'includes all fields', :use_sql_query_cache, :aggregate_failures do - expect(all_branches_rule_data).to include( - 'id' => all_branches_rule.to_global_id.to_s, - 'name' => 'All branches', - 'isDefault' => false, - 'isProtected' => false, - 'matchingBranchesCount' => project.repository.branch_count, - 'branchProtection' => nil, - "squashOption" => be_kind_of(Hash), - 'createdAt' => nil, - 'updatedAt' => nil - ) + expect(branch_rule_a_data).to include( + 'id' => branch_rule_a.to_global_id.to_s, + 'name' => branch_name_a, + 'isDefault' => be_boolean, + 'isProtected' => true, + 'matchingBranchesCount' => 1, + 'branchProtection' => be_kind_of(Hash), + 'createdAt' => be_kind_of(String), + 'updatedAt' => be_kind_of(String) + ) - expect(branch_rule_a_data).to include( - 'id' => branch_rule_a.to_global_id.to_s, - 'name' => branch_name_a, - 'isDefault' => be_boolean, - 'isProtected' => true, - 'matchingBranchesCount' => 1, - 'branchProtection' => be_kind_of(Hash), - 'createdAt' => be_kind_of(String), - 'updatedAt' => be_kind_of(String) - ) - - expect(branch_rule_b_data).to include( - 'id' => branch_rule_b.to_global_id.to_s, - 'name' => branch_name_b, - 'isDefault' => be_boolean, - 'isProtected' => true, - 'matchingBranchesCount' => wildcard_count, - 'branchProtection' => be_kind_of(Hash), - 'createdAt' => be_kind_of(String), - 'updatedAt' => be_kind_of(String) - ) - end - end - - context 'when the branch_rule_squash_settings flag is not enabled' do - let(:branch_rule_squash_settings) { false } - let(:branch_rule_b_data) { branch_rules_data.dig(0, 'node') } - let(:branch_rule_a_data) { branch_rules_data.dig(1, 'node') } - - it 'includes all fields', :use_sql_query_cache, :aggregate_failures do - expect(branch_rule_a_data).to include( - 'id' => branch_rule_a.to_global_id.to_s, - 'name' => branch_name_a, - 'isDefault' => be_boolean, - 'isProtected' => true, - 'matchingBranchesCount' => 1, - 'branchProtection' => be_kind_of(Hash), - 'createdAt' => be_kind_of(String), - 'updatedAt' => be_kind_of(String) - ) - - expect(branch_rule_b_data).to include( - 'id' => branch_rule_b.to_global_id.to_s, - 'name' => branch_name_b, - 'isDefault' => be_boolean, - 'isProtected' => true, - 'matchingBranchesCount' => wildcard_count, - 'branchProtection' => be_kind_of(Hash), - 'createdAt' => be_kind_of(String), - 'updatedAt' => be_kind_of(String) - ) - end + expect(branch_rule_b_data).to include( + 'id' => branch_rule_b.to_global_id.to_s, + 'name' => branch_name_b, + 'isDefault' => be_boolean, + 'isProtected' => true, + 'matchingBranchesCount' => wildcard_count, + 'branchProtection' => be_kind_of(Hash), + 'createdAt' => be_kind_of(String), + 'updatedAt' => be_kind_of(String) + ) end context 'when limiting the number of results' do diff --git a/spec/requests/projects/registry/repositories_controller_spec.rb b/spec/requests/projects/registry/repositories_controller_spec.rb index 15adb7398f3..0c0b91ee47c 100644 --- a/spec/requests/projects/registry/repositories_controller_spec.rb +++ b/spec/requests/projects/registry/repositories_controller_spec.rb @@ -21,6 +21,8 @@ RSpec.describe Projects::Registry::RepositoriesController, feature_category: :co end it { is_expected.to have_gitlab_http_status(:ok) } + + it_behaves_like 'pushed feature flag', :container_registry_immutable_tags end describe 'GET #show' do @@ -32,5 +34,7 @@ RSpec.describe Projects::Registry::RepositoriesController, feature_category: :co end it { is_expected.to have_gitlab_http_status(:ok) } + + it_behaves_like 'pushed feature flag', :container_registry_immutable_tags end end diff --git a/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb b/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb index 208ca500e82..d3ae2f04971 100644 --- a/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb +++ b/spec/workers/projects/inactive_projects_deletion_cron_worker_spec.rb @@ -73,6 +73,7 @@ RSpec.describe Projects::InactiveProjectsDeletionCronWorker, feature_category: : it 'does not invoke Projects::InactiveProjectsDeletionNotificationWorker' do expect(::Projects::InactiveProjectsDeletionNotificationWorker).not_to receive(:perform_async) + expect(::Projects::MarkForDeletionService).not_to receive(:new) expect(::Projects::DestroyService).not_to receive(:new) worker.perform @@ -100,11 +101,28 @@ RSpec.describe Projects::InactiveProjectsDeletionCronWorker, feature_category: : end expect(::Projects::InactiveProjectsDeletionNotificationWorker).to receive(:perform_async).with( inactive_large_project.id, deletion_date).and_call_original + expect(::Projects::MarkForDeletionService).not_to receive(:new) expect(::Projects::DestroyService).not_to receive(:new) worker.perform end + it 'does not invoke InactiveProjectsDeletionNotificationWorker for inactive projects marked for deletion' do + inactive_large_project.update!(marked_for_deletion_at: Date.current) + + expect(::Projects::InactiveProjectsDeletionNotificationWorker).not_to receive(:perform_async) + expect(::Projects::MarkForDeletionService).not_to receive(:new) + expect(::Projects::DestroyService).not_to receive(:new) + + worker.perform + + Gitlab::Redis::SharedState.with do |redis| + expect( + redis.hget('inactive_projects_deletion_warning_email_notified', "project:#{inactive_large_project.id}") + ).to be_nil + end + end + it 'does not invoke InactiveProjectsDeletionNotificationWorker for already notified inactive projects' do Gitlab::Redis::SharedState.with do |redis| redis.hset( @@ -115,6 +133,7 @@ RSpec.describe Projects::InactiveProjectsDeletionCronWorker, feature_category: : end expect(::Projects::InactiveProjectsDeletionNotificationWorker).not_to receive(:perform_async) + expect(::Projects::MarkForDeletionService).not_to receive(:new) expect(::Projects::DestroyService).not_to receive(:new) worker.perform @@ -131,6 +150,7 @@ RSpec.describe Projects::InactiveProjectsDeletionCronWorker, feature_category: : end expect(::Projects::InactiveProjectsDeletionNotificationWorker).not_to receive(:perform_async) + expect(::Projects::MarkForDeletionService).not_to receive(:new) expect(::Projects::DestroyService).to receive(:new).with(inactive_large_project, admin_bot, {}) .at_least(:once).and_call_original diff --git a/vendor/gems/attr_encrypted/lib/attr_encrypted.rb b/vendor/gems/attr_encrypted/lib/attr_encrypted.rb index 278a8fa7954..3e398b934f9 100644 --- a/vendor/gems/attr_encrypted/lib/attr_encrypted.rb +++ b/vendor/gems/attr_encrypted/lib/attr_encrypted.rb @@ -240,22 +240,15 @@ module AttrEncrypted def attr_decrypt(attribute, encrypted_value, options = {}) options = attr_encrypted_attributes[attribute.to_sym].merge(options) if options[:if] && !options[:unless] && not_empty?(encrypted_value) - keys = Array(options[:key]) - - keys.each.with_index do |key, index| - encrypted_value = encrypted_value.unpack(options[:encode]).first if options[:encode] - value = options[:encryptor].send(options[:decrypt_method], options.merge!(value: encrypted_value, key: key)) - if options[:marshal] - value = options[:marshaler].send(options[:load_method], value) - elsif defined?(Encoding) - encoding = Encoding.default_internal || Encoding.default_external - value = value.force_encoding(encoding.name) - end - - return value - rescue OpenSSL::Cipher::CipherError - raise if index == keys.length - 1 + encrypted_value = encrypted_value.unpack(options[:encode]).first if options[:encode] + value = options[:encryptor].send(options[:decrypt_method], options.merge!(value: encrypted_value)) + if options[:marshal] + value = options[:marshaler].send(options[:load_method], value) + elsif defined?(Encoding) + encoding = Encoding.default_internal || Encoding.default_external + value = value.force_encoding(encoding.name) end + value else encrypted_value end @@ -389,7 +382,7 @@ module AttrEncrypted evaluated_options.tap do |options| if options[:if] && !options[:unless] && options[:value_present] || options[:allow_empty_value] (attributes.keys - evaluated_options.keys).each do |option| - options[option] = evaluate_attr_encrypted_option(attributes[option], attribute.to_sym) + options[option] = evaluate_attr_encrypted_option(attributes[option]) end unless options[:mode] == :single_iv_and_salt @@ -406,17 +399,9 @@ module AttrEncrypted # Evaluates symbol (method reference) or proc (responds to call) options # # If the option is not a symbol or proc then the original option is returned - def evaluate_attr_encrypted_option(option, attribute = nil) + def evaluate_attr_encrypted_option(option) if option.is_a?(Symbol) && respond_to?(option, true) - method = method(option) - - # Allows a dynamic key method to receive the attribute name as argument - if method.arity == 1 - send(option, attribute) - else - send(option) - end - + send(option) elsif option.respond_to?(:call) option.call(self) else diff --git a/vendor/gems/attr_encrypted/test/attr_encrypted_test.rb b/vendor/gems/attr_encrypted/test/attr_encrypted_test.rb index ed1b998a64c..a57bd773b92 100644 --- a/vendor/gems/attr_encrypted/test/attr_encrypted_test.rb +++ b/vendor/gems/attr_encrypted/test/attr_encrypted_test.rb @@ -21,7 +21,6 @@ class User attr_encrypted :email, :without_encoding, :key => SECRET_KEY attr_encrypted :password, :prefix => 'crypted_', :suffix => '_test' attr_encrypted :ssn, :key => :secret_key, :attribute => 'ssn_encrypted' - attr_encrypted :ssn2, :key => :dynamic_secret_key, :attribute => 'ssn2_encrypted' attr_encrypted :credit_card, :encryptor => SillyEncryptor, :encrypt_method => :silly_encrypt, :decrypt_method => :silly_decrypt, :some_arg => 'test' attr_encrypted :with_encoding, :key => SECRET_KEY, :encode => true attr_encrypted :with_custom_encoding, :key => SECRET_KEY, :encode => 'm' @@ -48,16 +47,6 @@ class User def secret_key SECRET_KEY end - - def dynamic_secret_key(attribute) - operation = attr_encrypted_attributes[attribute][:operation] - - if operation == :encrypting - SECRET_KEY - else - [OLD_SECRET_KEY, SECRET_KEY] - end - end end class Admin < User @@ -215,17 +204,6 @@ class AttrEncryptedTest < Minitest::Test assert_equal encrypted, @user.ssn_encrypted end - def test_should_evaluate_a_key_passed_as_a_symbol_with_argument - @user = User.new - assert_nil @user.ssn2_encrypted - @user.ssn2 = 'testing' - refute_nil @user.ssn2_encrypted - encrypted = Encryptor.encrypt(:value => 'testing', :key => SECRET_KEY, :iv => @user.ssn2_encrypted_iv.unpack("m") - .first, :salt => @user.ssn2_encrypted_salt.unpack("m").first ) - assert_equal encrypted, @user.ssn2_encrypted - assert_equal 'testing', @user.ssn2 - end - def test_should_evaluate_a_key_passed_as_a_proc @user = User.new assert_nil @user.crypted_password_test diff --git a/vendor/gems/attr_encrypted/test/test_helper.rb b/vendor/gems/attr_encrypted/test/test_helper.rb index d418813a81c..75d7a6d9a20 100644 --- a/vendor/gems/attr_encrypted/test/test_helper.rb +++ b/vendor/gems/attr_encrypted/test/test_helper.rb @@ -49,7 +49,6 @@ end # This plugin re-enables it. Sequel::Model.plugin :after_initialize -OLD_SECRET_KEY = SecureRandom.random_bytes(32) SECRET_KEY = SecureRandom.random_bytes(32) def base64_encoding_regex