diff --git a/.gitlab/lint/unused_helper_methods/potential_methods_to_remove.yml b/.gitlab/lint/unused_helper_methods/potential_methods_to_remove.yml index 9401d141098..5ce9a44108f 100644 --- a/.gitlab/lint/unused_helper_methods/potential_methods_to_remove.yml +++ b/.gitlab/lint/unused_helper_methods/potential_methods_to_remove.yml @@ -23,10 +23,6 @@ discover_duo_pro_hand_raise_lead_data: file: ee/app/helpers/gitlab_subscriptions/hand_raise_leads_helper.rb buy_addon_data: file: ee/app/helpers/subscriptions_helper.rb -group_icon: - file: app/helpers/avatars_helper.rb -topic_icon: - file: app/helpers/avatars_helper.rb can_view_namespace_catalog?: file: app/helpers/ci/catalog/resources_helper.rb js_ci_catalog_data: diff --git a/.rubocop.yml b/.rubocop.yml index b562db994a8..80ea34d26e9 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -570,6 +570,18 @@ Gitlab/AvoidCurrentOrganization: - 'spec/**/*' - 'ee/spec/**/*' +Gitlab/DisallowCurrentOrganizationIdSafeNavigation: + Description: 'Use `Current.organization.id` instead of `Current.organization&.id`. `Current.organization` is expected to be assigned.' + Enabled: true + Include: + - 'app/**/*.rb' + - 'ee/app/**/*.rb' + - 'lib/**/*.rb' + - 'ee/lib/**/*.rb' + Exclude: + # Exclude the cop's own spec file to prevent self-reporting. + - 'spec/rubocop/cop/gitlab/disallow_current_organization_id_safe_navigation_spec.rb' + # See https://gitlab.com/groups/gitlab-org/-/epics/7374 Gitlab/AvoidGitlabInstanceChecks: Enabled: true diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json index f1dd965ceb5..d6bd2e3cbf8 100644 --- a/app/assets/javascripts/graphql_shared/possible_types.json +++ b/app/assets/javascripts/graphql_shared/possible_types.json @@ -60,6 +60,10 @@ "MergeRequest", "WorkItemWidgetCurrentUserTodos" ], + "CustomRoleInterface": [ + "AdminMemberRole", + "MemberRole" + ], "DependencyInterface": [ "Dependency", "DependencyAggregation" @@ -144,6 +148,10 @@ "PendingProjectMember", "ProjectMember" ], + "MemberRoleInterface": [ + "MemberRole", + "StandardRole" + ], "NamespaceUnion": [ "CiDeletedNamespace", "Namespace" diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/fragments/container_repository_tag_protection.fragment.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/fragments/container_repository_tag_protection.fragment.graphql new file mode 100644 index 00000000000..c3c7c29bd76 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/fragments/container_repository_tag_protection.fragment.graphql @@ -0,0 +1,4 @@ +fragment ContainerRepositoryTagProtection on ContainerProtectionAccessLevel { + minimumAccessLevelForPush + minimumAccessLevelForDelete +} diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql index 3cfe45d9c18..a598722cf4c 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql @@ -1,4 +1,5 @@ #import "~/graphql_shared/fragments/page_info.fragment.graphql" +#import "ee_else_ce/packages_and_registries/container_registry/explorer/graphql/fragments/container_repository_tag_protection.fragment.graphql" query getContainerRepositoryTags( $id: ContainerRepositoryID! @@ -40,9 +41,7 @@ query getContainerRepositoryTags( destroyContainerRepositoryTag } protection { - minimumAccessLevelForPush - minimumAccessLevelForDelete - immutable + ...ContainerRepositoryTagProtection } referrers { artifactType diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/container_protection_tag_rule.fragment.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/container_protection_tag_rule.fragment.graphql new file mode 100644 index 00000000000..ceb4f064962 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/fragments/container_protection_tag_rule.fragment.graphql @@ -0,0 +1,3 @@ +fragment ContainerProtectionTagRuleFragment on ContainerProtectionTagRule { + id +} diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_container_protection_tag_rules.query.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_container_protection_tag_rules.query.graphql index c1391cc1313..04eb8e751ca 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_container_protection_tag_rules.query.graphql +++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_container_protection_tag_rules.query.graphql @@ -1,16 +1,17 @@ +#import "ee_else_ce/packages_and_registries/settings/project/graphql/fragments/container_protection_tag_rule.fragment.graphql" + query getProjectContainerProtectionTagRules($projectPath: ID!, $first: Int) { project(fullPath: $projectPath) { id containerProtectionTagRules(first: $first) { nodes { - id tagNamePattern - immutable minimumAccessLevelForPush minimumAccessLevelForDelete userPermissions { destroyContainerRegistryProtectionTagRule } + ...ContainerProtectionTagRuleFragment } } } diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index dad603261ca..a5030ed7937 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -224,19 +224,19 @@ class ProjectsFinder < UnionFinder def by_marked_for_deletion_on(items) return items unless params[:marked_for_deletion_on].present? - items.by_marked_for_deletion_on(params[:marked_for_deletion_on]) + items.marked_for_deletion_on(params[:marked_for_deletion_on]) end def by_aimed_for_deletion(items) if ::Gitlab::Utils.to_boolean(params[:aimed_for_deletion]) - items.aimed_for_deletion(Date.current) + items.self_or_ancestors_aimed_for_deletion else items end end def by_not_aimed_for_deletion(items) - params[:not_aimed_for_deletion].present? ? items.not_aimed_for_deletion : items + params[:not_aimed_for_deletion].present? ? items.self_and_ancestors_not_aimed_for_deletion : items end def by_last_activity_after(items) @@ -280,14 +280,14 @@ class ProjectsFinder < UnionFinder def by_archived(projects) if params[:non_archived] - projects.non_archived + projects.self_and_ancestors_non_archived elsif params.key?(:archived) && !params[:archived].nil? if params[:archived] == 'only' - projects.archived + projects.self_or_ancestors_archived elsif Gitlab::Utils.to_boolean(params[:archived]) projects else - projects.non_archived + projects.self_and_ancestors_non_archived end else projects @@ -310,15 +310,7 @@ class ProjectsFinder < UnionFinder def by_active(items) return items if params[:active].nil? - params[:active] ? active(items) : inactive(items) - end - - def active(items) - items.non_archived.not_aimed_for_deletion - end - - def inactive(items) - items.archived.or(items.aimed_for_deletion(Date.current)) + params[:active] ? items.self_and_ancestors_active : items.self_or_ancestors_inactive end def finder_params diff --git a/app/graphql/mutations/container_registry/protection/tag_rule/create.rb b/app/graphql/mutations/container_registry/protection/tag_rule/create.rb index ba04ddab568..8cc88ba80b7 100644 --- a/app/graphql/mutations/container_registry/protection/tag_rule/create.rb +++ b/app/graphql/mutations/container_registry/protection/tag_rule/create.rb @@ -29,7 +29,7 @@ module Mutations argument :minimum_access_level_for_delete, Types::ContainerRegistry::Protection::TagRuleAccessLevelEnum, - required: false, + required: true, description: copy_field_description( Types::ContainerRegistry::Protection::TagRuleType, :minimum_access_level_for_delete @@ -37,7 +37,7 @@ module Mutations argument :minimum_access_level_for_push, Types::ContainerRegistry::Protection::TagRuleAccessLevelEnum, - required: false, + required: true, description: copy_field_description( Types::ContainerRegistry::Protection::TagRuleType, :minimum_access_level_for_push @@ -65,3 +65,5 @@ module Mutations end end end + +Mutations::ContainerRegistry::Protection::TagRule::Create.prepend_mod diff --git a/app/graphql/types/container_registry/protection/access_level_interface.rb b/app/graphql/types/container_registry/protection/access_level_interface.rb index 382a8446c69..d0b819cf0e9 100644 --- a/app/graphql/types/container_registry/protection/access_level_interface.rb +++ b/app/graphql/types/container_registry/protection/access_level_interface.rb @@ -8,29 +8,22 @@ module Types field :minimum_access_level_for_delete, Types::ContainerRegistry::Protection::TagRuleAccessLevelEnum, - null: true, + null: false, experiment: { milestone: '17.8' }, description: 'Minimum GitLab access level required to delete container image tags from the container repository. ' \ - 'Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. ' \ - 'If the value is `nil`, no access level can delete tags. ' + 'Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. ' field :minimum_access_level_for_push, Types::ContainerRegistry::Protection::TagRuleAccessLevelEnum, - null: true, + null: false, experiment: { milestone: '17.8' }, description: 'Minimum GitLab access level required to push container image tags to the container repository. ' \ - 'Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. ' \ - 'If the value is `nil`, no access level can push tags. ' - - field :immutable, - GraphQL::Types::Boolean, - null: false, - method: :immutable?, - experiment: { milestone: '17.11' }, - description: 'Returns true when tag rule is for tag immutability. Otherwise, false.' + 'Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. ' end end end end + +Types::ContainerRegistry::Protection::AccessLevelInterface.prepend_mod diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index cdf524816a9..4aaba8d2e63 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -876,12 +876,8 @@ module Types end def container_protection_tag_rules - rules = object.container_registry_protection_tag_rules - - return rules.mutable unless Feature.enabled?(:container_registry_immutable_tags, object) - - # mutable tag rules come first before immutable - rules.mutable + rules.immutable + # Immutable tag rules are added in EE extension + object.container_registry_protection_tag_rules.mutable end { diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index 599978af0a9..bfc344e88cf 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -3,14 +3,6 @@ module AvatarsHelper DEFAULT_AVATAR_PATH = 'no_avatar.png' - def group_icon(group, options = {}) - source_icon(group, options) - end - - def topic_icon(topic, options = {}) - source_icon(topic, options) - end - # Takes both user and email and returns the avatar_icon by # user (preferred) or email. def avatar_icon_for(user = nil, email = nil, size = nil, scale = 2, only_path: true) diff --git a/app/models/project.rb b/app/models/project.rb index a5cdfa5a27d..79d68dc7892 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -658,13 +658,30 @@ class Project < ApplicationRecord scope :not_hidden, -> { where(hidden: false) } scope :not_in_groups, ->(groups) { where.not(group: groups) } scope :by_not_in_root_id, ->(root_id) { joins(:project_namespace).where('namespaces.traversal_ids[1] NOT IN (?)', root_id) } + + scope :aimed_for_deletion, -> { where.not(marked_for_deletion_at: nil).without_deleted } + scope :self_or_ancestors_aimed_for_deletion, -> do + left_joins(:group) + .where.not(marked_for_deletion_at: nil) + .or(where(Group.self_or_ancestors_deletion_schedule_subquery.exists)) + .without_deleted + end + scope :not_aimed_for_deletion, -> { where(marked_for_deletion_at: nil).without_deleted } - scope :aimed_for_deletion, ->(date) { where('marked_for_deletion_at <= ?', date).without_deleted } - scope :with_deleting_user, -> { includes(:deleting_user) } - scope :by_marked_for_deletion_on, ->(marked_for_deletion_on) do + scope :self_and_ancestors_not_aimed_for_deletion, -> do + left_joins(:group) + .where(marked_for_deletion_at: nil) + .where.not(Group.self_or_ancestors_deletion_schedule_subquery.exists) + .without_deleted + end + + scope :marked_for_deletion_before, ->(date) { where('marked_for_deletion_at <= ?', date).without_deleted } + scope :marked_for_deletion_on, ->(marked_for_deletion_on) do where(marked_for_deletion_at: marked_for_deletion_on) end + scope :with_deleting_user, -> { includes(:deleting_user) } + scope :with_storage_feature, ->(feature) do where(arel_table[:storage_version].gteq(HASHED_STORAGE_FEATURES[feature])) end @@ -798,8 +815,24 @@ class Project < ApplicationRecord scope :starred_by, ->(user) { joins(:users_star_projects).where('users_star_projects.user_id': user.id) } scope :visible_to_user, ->(user) { where(id: user.authorized_projects.select(:id).reorder(nil)) } scope :visible_to_user_and_access_level, ->(user, access_level) { where(id: user.authorized_projects.where('project_authorizations.access_level >= ?', access_level).select(:id).reorder(nil)) } + scope :archived, -> { where(archived: true) } + scope :self_or_ancestors_archived, -> do + left_joins(:group) + .where(archived: true) + .or(where(Group.self_or_ancestors_archived_setting_subquery.exists)) + end + scope :non_archived, -> { where(archived: false) } + scope :self_and_ancestors_non_archived, -> do + left_joins(:group) + .where(archived: false) + .where.not(Group.self_or_ancestors_archived_setting_subquery.exists) + end + + scope :self_and_ancestors_active, -> { self_and_ancestors_non_archived.self_and_ancestors_not_aimed_for_deletion } + scope :self_or_ancestors_inactive, -> { self_or_ancestors_archived.or(self_or_ancestors_aimed_for_deletion) } + scope :with_push, -> { joins(:events).merge(Event.pushed_action) } scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') } scope :with_jira_dvcs_server, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false)) } diff --git a/app/workers/adjourned_projects_deletion_cron_worker.rb b/app/workers/adjourned_projects_deletion_cron_worker.rb index 57a14c054e0..6db0b593a14 100644 --- a/app/workers/adjourned_projects_deletion_cron_worker.rb +++ b/app/workers/adjourned_projects_deletion_cron_worker.rb @@ -17,7 +17,7 @@ class AdjournedProjectsDeletionCronWorker def perform deletion_cutoff = Gitlab::CurrentSettings.deletion_adjourned_period.days.ago.to_date - Project.with_route.with_deleting_user.aimed_for_deletion(deletion_cutoff).find_each(batch_size: 100).with_index do |project, index| # rubocop: disable CodeReuse/ActiveRecord -- existing class moved from EE to CE + Project.with_route.with_deleting_user.marked_for_deletion_before(deletion_cutoff).find_each(batch_size: 100).with_index do |project, index| # rubocop: disable CodeReuse/ActiveRecord -- existing class moved from EE to CE delay = index * INTERVAL with_context(project: project, user: project.deleting_user) do diff --git a/config/events/collect_sast_scan_metrics_from_pipeline.yml b/config/events/collect_sast_scan_metrics_from_pipeline.yml index 1bf6fed082e..05082606c6b 100644 --- a/config/events/collect_sast_scan_metrics_from_pipeline.yml +++ b/config/events/collect_sast_scan_metrics_from_pipeline.yml @@ -21,11 +21,11 @@ additional_properties: property: description: "a UUID that identifies a scan" value: - description: "exit status of the analyer where 0 indicates success and 1 indicates error" + description: "exit status of the analyzer where 0 indicates success and 1 indicates error" version: description: "version of the analyzer" exit_code: - description: "exit code of the analyer" + description: "exit code of the analyzer" override_count: description: "number of configured overrides" passthrough_count: @@ -36,3 +36,5 @@ additional_properties: description: "scan time duration in seconds" file_count: description: "project size in terms of number of files" + language_feature_usage: + description: "counts of programming language features used in the project" diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index f2bf8de8b98..186a07263e9 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -639,6 +639,8 @@ - 1 - - namespaces_cascade_duo_features_enabled - 1 +- - namespaces_cascade_web_based_commit_signing_enabled + - 1 - - namespaces_free_user_cap_group_over_limit_notification - 1 - - namespaces_process_sync_events diff --git a/db/migrate/20250529002831_add_created_by_id_to_custom_statuses.rb b/db/migrate/20250529002831_add_created_by_id_to_custom_statuses.rb new file mode 100644 index 00000000000..0551a296326 --- /dev/null +++ b/db/migrate/20250529002831_add_created_by_id_to_custom_statuses.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddCreatedByIdToCustomStatuses < Gitlab::Database::Migration[2.3] + milestone '18.1' + + def change + add_column :work_item_custom_statuses, :created_by_id, :bigint + end +end diff --git a/db/migrate/20250529005223_add_created_by_id_index_to_custom_statuses.rb b/db/migrate/20250529005223_add_created_by_id_index_to_custom_statuses.rb new file mode 100644 index 00000000000..212e7a71a7c --- /dev/null +++ b/db/migrate/20250529005223_add_created_by_id_index_to_custom_statuses.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddCreatedByIdIndexToCustomStatuses < Gitlab::Database::Migration[2.3] + disable_ddl_transaction! + milestone '18.1' + + INDEX_NAME = 'index_work_item_custom_statuses_on_created_by_id' + + def up + add_concurrent_index :work_item_custom_statuses, :created_by_id, name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :work_item_custom_statuses, name: INDEX_NAME + end +end diff --git a/db/migrate/20250529005709_add_created_by_foreign_key_to_custom_statuses.rb b/db/migrate/20250529005709_add_created_by_foreign_key_to_custom_statuses.rb new file mode 100644 index 00000000000..4f7f4b4a843 --- /dev/null +++ b/db/migrate/20250529005709_add_created_by_foreign_key_to_custom_statuses.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddCreatedByForeignKeyToCustomStatuses < Gitlab::Database::Migration[2.3] + disable_ddl_transaction! + milestone '18.1' + + def up + add_concurrent_foreign_key :work_item_custom_statuses, :users, + column: :created_by_id, on_delete: :nullify + end + + def down + remove_foreign_key_if_exists :work_item_custom_statuses, column: :created_by_id + end +end diff --git a/db/migrate/20250529010458_add_created_by_id_to_custom_lifecycles.rb b/db/migrate/20250529010458_add_created_by_id_to_custom_lifecycles.rb new file mode 100644 index 00000000000..2db48e76a15 --- /dev/null +++ b/db/migrate/20250529010458_add_created_by_id_to_custom_lifecycles.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddCreatedByIdToCustomLifecycles < Gitlab::Database::Migration[2.3] + milestone '18.1' + + def change + add_column :work_item_custom_lifecycles, :created_by_id, :bigint + end +end diff --git a/db/migrate/20250529010756_add_created_by_id_index_to_custom_lifecycles.rb b/db/migrate/20250529010756_add_created_by_id_index_to_custom_lifecycles.rb new file mode 100644 index 00000000000..a9652752531 --- /dev/null +++ b/db/migrate/20250529010756_add_created_by_id_index_to_custom_lifecycles.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddCreatedByIdIndexToCustomLifecycles < Gitlab::Database::Migration[2.3] + disable_ddl_transaction! + milestone '18.1' + + INDEX_NAME = 'index_work_item_custom_lifecycles_on_created_by_id' + + def up + add_concurrent_index :work_item_custom_lifecycles, :created_by_id, name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :work_item_custom_lifecycles, name: INDEX_NAME + end +end diff --git a/db/migrate/20250529010905_add_created_by_foreign_key_to_custom_lifecycles.rb b/db/migrate/20250529010905_add_created_by_foreign_key_to_custom_lifecycles.rb new file mode 100644 index 00000000000..0e1a86995d5 --- /dev/null +++ b/db/migrate/20250529010905_add_created_by_foreign_key_to_custom_lifecycles.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddCreatedByForeignKeyToCustomLifecycles < Gitlab::Database::Migration[2.3] + disable_ddl_transaction! + milestone '18.1' + + def up + add_concurrent_foreign_key :work_item_custom_lifecycles, :users, + column: :created_by_id, on_delete: :nullify + end + + def down + remove_foreign_key_if_exists :work_item_custom_lifecycles, column: :created_by_id + end +end diff --git a/db/migrate/20250529011429_add_updated_by_id_to_custom_statuses.rb b/db/migrate/20250529011429_add_updated_by_id_to_custom_statuses.rb new file mode 100644 index 00000000000..986dd5c823d --- /dev/null +++ b/db/migrate/20250529011429_add_updated_by_id_to_custom_statuses.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddUpdatedByIdToCustomStatuses < Gitlab::Database::Migration[2.3] + milestone '18.1' + + def change + add_column :work_item_custom_statuses, :updated_by_id, :bigint + end +end diff --git a/db/migrate/20250529011732_add_updated_by_id_index_to_custom_statuses.rb b/db/migrate/20250529011732_add_updated_by_id_index_to_custom_statuses.rb new file mode 100644 index 00000000000..82a2c9511c1 --- /dev/null +++ b/db/migrate/20250529011732_add_updated_by_id_index_to_custom_statuses.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddUpdatedByIdIndexToCustomStatuses < Gitlab::Database::Migration[2.3] + disable_ddl_transaction! + milestone '18.1' + + INDEX_NAME = 'index_work_item_custom_statuses_on_updated_by_id' + + def up + add_concurrent_index :work_item_custom_statuses, :updated_by_id, name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :work_item_custom_statuses, name: INDEX_NAME + end +end diff --git a/db/migrate/20250529011941_add_updated_by_foreign_key_to_custom_statuses.rb b/db/migrate/20250529011941_add_updated_by_foreign_key_to_custom_statuses.rb new file mode 100644 index 00000000000..e4864a9d83e --- /dev/null +++ b/db/migrate/20250529011941_add_updated_by_foreign_key_to_custom_statuses.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddUpdatedByForeignKeyToCustomStatuses < Gitlab::Database::Migration[2.3] + disable_ddl_transaction! + milestone '18.1' + + def up + add_concurrent_foreign_key :work_item_custom_statuses, :users, + column: :updated_by_id, on_delete: :nullify + end + + def down + remove_foreign_key_if_exists :work_item_custom_statuses, column: :updated_by_id + end +end diff --git a/db/migrate/20250529012147_add_updated_by_id_to_custom_lifecycles.rb b/db/migrate/20250529012147_add_updated_by_id_to_custom_lifecycles.rb new file mode 100644 index 00000000000..b1b7dae710d --- /dev/null +++ b/db/migrate/20250529012147_add_updated_by_id_to_custom_lifecycles.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddUpdatedByIdToCustomLifecycles < Gitlab::Database::Migration[2.3] + milestone '18.1' + + def change + add_column :work_item_custom_lifecycles, :updated_by_id, :bigint + end +end diff --git a/db/migrate/20250529012327_add_updated_by_id_index_to_custom_lifecycles.rb b/db/migrate/20250529012327_add_updated_by_id_index_to_custom_lifecycles.rb new file mode 100644 index 00000000000..135f0039755 --- /dev/null +++ b/db/migrate/20250529012327_add_updated_by_id_index_to_custom_lifecycles.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddUpdatedByIdIndexToCustomLifecycles < Gitlab::Database::Migration[2.3] + disable_ddl_transaction! + milestone '18.1' + + INDEX_NAME = 'index_work_item_custom_lifecycles_on_updated_by_id' + + def up + add_concurrent_index :work_item_custom_lifecycles, :updated_by_id, name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :work_item_custom_lifecycles, name: INDEX_NAME + end +end diff --git a/db/migrate/20250529012446_add_updated_by_foreign_key_to_custom_lifecycles.rb b/db/migrate/20250529012446_add_updated_by_foreign_key_to_custom_lifecycles.rb new file mode 100644 index 00000000000..e65105a5272 --- /dev/null +++ b/db/migrate/20250529012446_add_updated_by_foreign_key_to_custom_lifecycles.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddUpdatedByForeignKeyToCustomLifecycles < Gitlab::Database::Migration[2.3] + disable_ddl_transaction! + milestone '18.1' + + def up + add_concurrent_foreign_key :work_item_custom_lifecycles, :users, + column: :updated_by_id, on_delete: :nullify + end + + def down + remove_foreign_key_if_exists :work_item_custom_lifecycles, column: :updated_by_id + end +end diff --git a/db/post_migrate/20250602191335_remove_idx_ci_job_variables_on_partition_id_job_id.rb b/db/post_migrate/20250602191335_remove_idx_ci_job_variables_on_partition_id_job_id.rb new file mode 100644 index 00000000000..f1c4fa56234 --- /dev/null +++ b/db/post_migrate/20250602191335_remove_idx_ci_job_variables_on_partition_id_job_id.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class RemoveIdxCiJobVariablesOnPartitionIdJobId < Gitlab::Database::Migration[2.3] + milestone '18.1' + + disable_ddl_transaction! + + TABLE_NAME = :ci_job_variables + INDEX_NAME = :index_ci_job_variables_on_partition_id_job_id + COLUMNS = [:partition_id, :job_id] + + def up + remove_concurrent_index_by_name TABLE_NAME, name: INDEX_NAME + end + + def down + add_concurrent_index TABLE_NAME, COLUMNS, name: INDEX_NAME + end +end diff --git a/db/post_migrate/20250603033828_remove_project_fingerprint_from_security_findings.rb b/db/post_migrate/20250603033828_remove_project_fingerprint_from_security_findings.rb new file mode 100644 index 00000000000..17070a9692d --- /dev/null +++ b/db/post_migrate/20250603033828_remove_project_fingerprint_from_security_findings.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class RemoveProjectFingerprintFromSecurityFindings < Gitlab::Database::Migration[2.3] + include Gitlab::Database::PartitioningMigrationHelpers + + disable_ddl_transaction! + milestone '18.1' + + def up + remove_check_constraint :security_findings, 'check_b9508c6df8' + remove_column :security_findings, :project_fingerprint + end + + def down + add_column :security_findings, :project_fingerprint, :text, if_not_exists: true + add_check_constraint :security_findings, 'char_length(project_fingerprint) <= 40', 'check_b9508c6df8' + add_concurrent_partitioned_index :security_findings, :project_fingerprint, + name: 'security_findings_project_fingerprint_idx' + end +end diff --git a/db/schema_migrations/20250529002831 b/db/schema_migrations/20250529002831 new file mode 100644 index 00000000000..99078088546 --- /dev/null +++ b/db/schema_migrations/20250529002831 @@ -0,0 +1 @@ +8dc9aa1a310798758d7b115d50d20142af56c35af11ada707708a8d521389021 \ No newline at end of file diff --git a/db/schema_migrations/20250529005223 b/db/schema_migrations/20250529005223 new file mode 100644 index 00000000000..56a8463776b --- /dev/null +++ b/db/schema_migrations/20250529005223 @@ -0,0 +1 @@ +363c5d217044ac211c3bdadf3d8d2e582f4c1c8c705ab5b6d2d141c18431e861 \ No newline at end of file diff --git a/db/schema_migrations/20250529005709 b/db/schema_migrations/20250529005709 new file mode 100644 index 00000000000..94c46a1170a --- /dev/null +++ b/db/schema_migrations/20250529005709 @@ -0,0 +1 @@ +76f7874268107ae4a9d8bdbbfcdf522a869726b67a87a95ca184fa0409519c83 \ No newline at end of file diff --git a/db/schema_migrations/20250529010458 b/db/schema_migrations/20250529010458 new file mode 100644 index 00000000000..8cf105f9b47 --- /dev/null +++ b/db/schema_migrations/20250529010458 @@ -0,0 +1 @@ +ccb4e487017f8742eaffb87048a6ea7c6b2bbb59a31b276abef1c4b921725f73 \ No newline at end of file diff --git a/db/schema_migrations/20250529010756 b/db/schema_migrations/20250529010756 new file mode 100644 index 00000000000..d4260cf8782 --- /dev/null +++ b/db/schema_migrations/20250529010756 @@ -0,0 +1 @@ +cc592eedda6586088a1049c9ca27618a73b50281335ccac5ea4c425c0c5f9444 \ No newline at end of file diff --git a/db/schema_migrations/20250529010905 b/db/schema_migrations/20250529010905 new file mode 100644 index 00000000000..568509b5b79 --- /dev/null +++ b/db/schema_migrations/20250529010905 @@ -0,0 +1 @@ +e3a4587ea36f0c4b266a8e908b73e867e5df0afefa202417a439cf099a112e63 \ No newline at end of file diff --git a/db/schema_migrations/20250529011429 b/db/schema_migrations/20250529011429 new file mode 100644 index 00000000000..d1a855ef0dd --- /dev/null +++ b/db/schema_migrations/20250529011429 @@ -0,0 +1 @@ +a3f50469cd58afe7fb6012ff7b27eef52e45c966f7cffb0516d499b70b0de666 \ No newline at end of file diff --git a/db/schema_migrations/20250529011732 b/db/schema_migrations/20250529011732 new file mode 100644 index 00000000000..459b8255aad --- /dev/null +++ b/db/schema_migrations/20250529011732 @@ -0,0 +1 @@ +f0c69d5d3bfdcc30df065db0c658ee86bbe4e28dc4eb50233308d947faaf0b3f \ No newline at end of file diff --git a/db/schema_migrations/20250529011941 b/db/schema_migrations/20250529011941 new file mode 100644 index 00000000000..df209f154c1 --- /dev/null +++ b/db/schema_migrations/20250529011941 @@ -0,0 +1 @@ +4a373f4a79e4bf14b5509ce63a9fb79267246b2dfc03c83d20868142f6eaca9d \ No newline at end of file diff --git a/db/schema_migrations/20250529012147 b/db/schema_migrations/20250529012147 new file mode 100644 index 00000000000..42ea3c0b270 --- /dev/null +++ b/db/schema_migrations/20250529012147 @@ -0,0 +1 @@ +5674253dd6f59541e4a396635aceffd7963bf4fef38bbebb58851ecd8b10b1ee \ No newline at end of file diff --git a/db/schema_migrations/20250529012327 b/db/schema_migrations/20250529012327 new file mode 100644 index 00000000000..efe81355c34 --- /dev/null +++ b/db/schema_migrations/20250529012327 @@ -0,0 +1 @@ +e0ae0be50f1fd197267e524d0bed195be8692dec84c3ddb24ad5bf754fa26a02 \ No newline at end of file diff --git a/db/schema_migrations/20250529012446 b/db/schema_migrations/20250529012446 new file mode 100644 index 00000000000..9f18e3d9546 --- /dev/null +++ b/db/schema_migrations/20250529012446 @@ -0,0 +1 @@ +d8de1cea7fdafa9d31cab776eff7ea66015d253abed20056cff1fcfd4334561b \ No newline at end of file diff --git a/db/schema_migrations/20250602191335 b/db/schema_migrations/20250602191335 new file mode 100644 index 00000000000..30b27a423e5 --- /dev/null +++ b/db/schema_migrations/20250602191335 @@ -0,0 +1 @@ +a05765899ad07672f1c007ac86087e799aeb507328675566585cfd684878b4ba \ No newline at end of file diff --git a/db/schema_migrations/20250603033828 b/db/schema_migrations/20250603033828 new file mode 100644 index 00000000000..96819f8dd5a --- /dev/null +++ b/db/schema_migrations/20250603033828 @@ -0,0 +1 @@ +1826f2a6ec247577a18af2a0c6d14713ef817760ca0607c0a8f49badce883504 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 51fc8fe5443..48934b7283d 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -4965,15 +4965,13 @@ CREATE TABLE security_findings ( scan_id bigint NOT NULL, scanner_id bigint NOT NULL, severity smallint NOT NULL, - project_fingerprint text, deduplicated boolean DEFAULT false NOT NULL, uuid uuid, overridden_uuid uuid, partition_number integer DEFAULT 1 NOT NULL, finding_data jsonb DEFAULT '{}'::jsonb NOT NULL, project_id bigint, - CONSTRAINT check_6c2851a8c9 CHECK ((uuid IS NOT NULL)), - CONSTRAINT check_b9508c6df8 CHECK ((char_length(project_fingerprint) <= 40)) + CONSTRAINT check_6c2851a8c9 CHECK ((uuid IS NOT NULL)) ) PARTITION BY LIST (partition_number); @@ -25788,6 +25786,8 @@ CREATE TABLE work_item_custom_lifecycles ( created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, name text NOT NULL, + created_by_id bigint, + updated_by_id bigint, CONSTRAINT check_1feff2de99 CHECK ((char_length(name) <= 255)) ); @@ -25809,6 +25809,8 @@ CREATE TABLE work_item_custom_statuses ( name text NOT NULL, description text, color text NOT NULL, + created_by_id bigint, + updated_by_id bigint, CONSTRAINT check_4789467800 CHECK ((char_length(color) <= 7)), CONSTRAINT check_720a7c4d24 CHECK ((char_length(name) <= 255)), CONSTRAINT check_8ea8b3c991 CHECK ((char_length(description) <= 255)), @@ -34438,8 +34440,6 @@ CREATE INDEX index_ci_job_variables_on_job_id ON ci_job_variables USING btree (j CREATE UNIQUE INDEX index_ci_job_variables_on_key_and_job_id ON ci_job_variables USING btree (key, job_id); -CREATE INDEX index_ci_job_variables_on_partition_id_job_id ON ci_job_variables USING btree (partition_id, job_id); - CREATE INDEX index_ci_job_variables_on_project_id ON ci_job_variables USING btree (project_id); CREATE INDEX index_ci_minutes_additional_packs_on_namespace_id_purchase_xid ON ci_minutes_additional_packs USING btree (namespace_id, purchase_xid); @@ -38204,10 +38204,18 @@ CREATE INDEX index_work_item_current_statuses_on_namespace_id ON work_item_curre CREATE UNIQUE INDEX index_work_item_current_statuses_on_work_item_id ON work_item_current_statuses USING btree (work_item_id); +CREATE INDEX index_work_item_custom_lifecycles_on_created_by_id ON work_item_custom_lifecycles USING btree (created_by_id); + CREATE UNIQUE INDEX index_work_item_custom_lifecycles_on_namespace_id_and_name ON work_item_custom_lifecycles USING btree (namespace_id, name); +CREATE INDEX index_work_item_custom_lifecycles_on_updated_by_id ON work_item_custom_lifecycles USING btree (updated_by_id); + +CREATE INDEX index_work_item_custom_statuses_on_created_by_id ON work_item_custom_statuses USING btree (created_by_id); + CREATE UNIQUE INDEX index_work_item_custom_statuses_on_namespace_id_and_lower_name ON work_item_custom_statuses USING btree (namespace_id, TRIM(BOTH FROM lower(name))); +CREATE INDEX index_work_item_custom_statuses_on_updated_by_id ON work_item_custom_statuses USING btree (updated_by_id); + CREATE INDEX index_work_item_hierarchy_restrictions_on_child_type_id ON work_item_hierarchy_restrictions USING btree (child_type_id); CREATE UNIQUE INDEX index_work_item_hierarchy_restrictions_on_parent_and_child ON work_item_hierarchy_restrictions USING btree (parent_type_id, child_type_id); @@ -38708,8 +38716,6 @@ CREATE INDEX scan_finding_approval_project_rule_index_created_at_project_id ON a CREATE INDEX scan_finding_approval_project_rule_index_project_id ON approval_project_rules USING btree (project_id) WHERE (report_type = 4); -CREATE INDEX security_findings_project_fingerprint_idx ON ONLY security_findings USING btree (project_fingerprint); - CREATE INDEX security_findings_scan_id_deduplicated_idx ON ONLY security_findings USING btree (scan_id, deduplicated); CREATE INDEX security_findings_scan_id_id_idx ON ONLY security_findings USING btree (scan_id, id); @@ -42339,6 +42345,9 @@ ALTER TABLE ONLY deployment_approvals ALTER TABLE ONLY bulk_import_trackers ADD CONSTRAINT fk_2d0e051bc3 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; +ALTER TABLE ONLY work_item_custom_lifecycles + ADD CONSTRAINT fk_2d0f7ebf48 FOREIGN KEY (updated_by_id) REFERENCES users(id) ON DELETE SET NULL; + ALTER TABLE ONLY audit_events_instance_external_audit_event_destinations ADD CONSTRAINT fk_2d3ebd0fbc FOREIGN KEY (stream_destination_id) REFERENCES audit_events_instance_external_streaming_destinations(id) ON DELETE SET NULL; @@ -42708,6 +42717,9 @@ ALTER TABLE ONLY deploy_keys_projects ALTER TABLE ONLY merge_requests_approval_rules_groups ADD CONSTRAINT fk_59068f09e5 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; +ALTER TABLE ONLY work_item_custom_statuses + ADD CONSTRAINT fk_590e87b7c7 FOREIGN KEY (updated_by_id) REFERENCES users(id) ON DELETE SET NULL; + ALTER TABLE ONLY oauth_access_grants ADD CONSTRAINT fk_59cdb2323c FOREIGN KEY (organization_id) REFERENCES organizations(id) ON DELETE CASCADE; @@ -42777,6 +42789,9 @@ ALTER TABLE ONLY user_achievements ALTER TABLE ONLY merge_requests ADD CONSTRAINT fk_6149611a04 FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL; +ALTER TABLE ONLY work_item_custom_lifecycles + ADD CONSTRAINT fk_614a3cdb95 FOREIGN KEY (created_by_id) REFERENCES users(id) ON DELETE SET NULL; + ALTER TABLE ONLY member_approvals ADD CONSTRAINT fk_619f381144 FOREIGN KEY (member_role_id) REFERENCES member_roles(id) ON DELETE SET NULL; @@ -44139,6 +44154,9 @@ ALTER TABLE ONLY merge_requests_approval_rules ALTER TABLE ONLY clusters_managed_resources ADD CONSTRAINT fk_fad3c3b2e2 FOREIGN KEY (environment_id) REFERENCES environments(id) ON DELETE CASCADE; +ALTER TABLE ONLY work_item_custom_statuses + ADD CONSTRAINT fk_fb28a15e7b FOREIGN KEY (created_by_id) REFERENCES users(id) ON DELETE SET NULL; + ALTER TABLE ONLY agent_group_authorizations ADD CONSTRAINT fk_fb70782616 FOREIGN KEY (agent_id) REFERENCES cluster_agents(id) ON DELETE CASCADE; diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 69812467d22..26ecb98826a 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -4261,8 +4261,8 @@ Input type: `createContainerProtectionTagRuleInput` | Name | Type | Description | | ---- | ---- | ----------- | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | -| `minimumAccessLevelForDelete` | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | Minimum GitLab access level required to delete container image tags from the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, no access level can delete tags. Introduced in GitLab 17.8: **Status**: Experiment. | -| `minimumAccessLevelForPush` | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | Minimum GitLab access level required to push container image tags to the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, no access level can push tags. Introduced in GitLab 17.8: **Status**: Experiment. | +| `minimumAccessLevelForDelete` | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | Minimum GitLab access level required to delete container image tags from the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. Introduced in GitLab 17.8: **Status**: Experiment. If the value is `nil`, no access level can delete tags. | +| `minimumAccessLevelForPush` | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | Minimum GitLab access level required to push container image tags to the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. Introduced in GitLab 17.8: **Status**: Experiment. If the value is `nil`, no access level can push tags. | | `projectPath` | [`ID!`](#id) | Full path of the project containing the container image tags. | | `tagNamePattern` | [`String!`](#string) | The pattern that matches container image tags to protect. For example, `v1.*`. Wildcard character `*` allowed. Introduced in GitLab 17.8: **Status**: Experiment. | @@ -21197,9 +21197,9 @@ Represents an admin member role. | `editPath` {{< icon name="warning-solid" >}} | [`String!`](#string) | **Introduced** in GitLab 16.11. **Status**: Experiment. Web UI path to edit the custom role. | | `enabledPermissions` {{< icon name="warning-solid" >}} | [`CustomizableAdminPermissionConnection!`](#customizableadminpermissionconnection) | **Introduced** in GitLab 17.7. **Status**: Experiment. Array of all permissions enabled for the custom role. | | `id` | [`ID!`](#id) | Role ID. | -| `membersCount` {{< icon name="warning-solid" >}} | [`Int`](#int) | **Introduced** in GitLab 17.3. **Status**: Experiment. Number of times the role has been directly assigned to a group or project member. | +| `ldapAdminRoleLinks` {{< icon name="warning-solid" >}} | [`LdapAdminRoleLinkConnection`](#ldapadminrolelinkconnection) | **Introduced** in GitLab 18.1. **Status**: Experiment. LDAP admin role sync configurations that will assign the admin member role. | | `name` | [`String`](#string) | Role name. | -| `usersCount` {{< icon name="warning-solid" >}} | [`Int`](#int) | **Introduced** in GitLab 17.5. **Status**: Experiment. Number of users who have been directly assigned the role in at least one group or project. | +| `usersCount` {{< icon name="warning-solid" >}} | [`Int`](#int) | **Introduced** in GitLab 17.5. **Status**: Experiment. Number of users who have been directly assigned the admin member role. | ### `AgentConfiguration` @@ -24455,8 +24455,8 @@ Represents the most restrictive permissions for a container image tag. | Name | Type | Description | | ---- | ---- | ----------- | | `immutable` {{< icon name="warning-solid" >}} | [`Boolean!`](#boolean) | **Introduced** in GitLab 17.11. **Status**: Experiment. Returns true when tag rule is for tag immutability. Otherwise, false. | -| `minimumAccessLevelForDelete` {{< icon name="warning-solid" >}} | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to delete container image tags from the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, no access level can delete tags. | -| `minimumAccessLevelForPush` {{< icon name="warning-solid" >}} | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to push container image tags to the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, no access level can push tags. | +| `minimumAccessLevelForDelete` {{< icon name="warning-solid" >}} | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to delete container image tags from the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. | +| `minimumAccessLevelForPush` {{< icon name="warning-solid" >}} | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to push container image tags to the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. | ### `ContainerProtectionRepositoryRule` @@ -24481,8 +24481,8 @@ A container repository tag protection rule designed to prevent users with a cert | ---- | ---- | ----------- | | `id` {{< icon name="warning-solid" >}} | [`ContainerRegistryProtectionTagRuleID!`](#containerregistryprotectiontagruleid) | **Introduced** in GitLab 17.8. **Status**: Experiment. ID of the container repository tag protection rule. | | `immutable` {{< icon name="warning-solid" >}} | [`Boolean!`](#boolean) | **Introduced** in GitLab 17.11. **Status**: Experiment. Returns true when tag rule is for tag immutability. Otherwise, false. | -| `minimumAccessLevelForDelete` {{< icon name="warning-solid" >}} | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to delete container image tags from the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, no access level can delete tags. | -| `minimumAccessLevelForPush` {{< icon name="warning-solid" >}} | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to push container image tags to the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, no access level can push tags. | +| `minimumAccessLevelForDelete` {{< icon name="warning-solid" >}} | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to delete container image tags from the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. | +| `minimumAccessLevelForPush` {{< icon name="warning-solid" >}} | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to push container image tags to the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. | | `tagNamePattern` {{< icon name="warning-solid" >}} | [`String!`](#string) | **Introduced** in GitLab 17.8. **Status**: Experiment. The pattern that matches container image tags to protect. For example, `v1.*`. Wildcard character `*` allowed. | | `userPermissions` | [`ContainerRegistryProtectionTagRulePermissions!`](#containerregistryprotectiontagrulepermissions) | Permissions for the current user on the resource. | @@ -48627,8 +48627,8 @@ Implementations: | Name | Type | Description | | ---- | ---- | ----------- | | `immutable` {{< icon name="warning-solid" >}} | [`Boolean!`](#boolean) | **Introduced** in GitLab 17.11. **Status**: Experiment. Returns true when tag rule is for tag immutability. Otherwise, false. | -| `minimumAccessLevelForDelete` {{< icon name="warning-solid" >}} | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to delete container image tags from the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, no access level can delete tags. | -| `minimumAccessLevelForPush` {{< icon name="warning-solid" >}} | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to push container image tags to the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. If the value is `nil`, no access level can push tags. | +| `minimumAccessLevelForDelete` {{< icon name="warning-solid" >}} | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to delete container image tags from the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. | +| `minimumAccessLevelForPush` {{< icon name="warning-solid" >}} | [`ContainerProtectionTagRuleAccessLevel`](#containerprotectiontagruleaccesslevel) | **Introduced** in GitLab 17.8. **Status**: Experiment. Minimum GitLab access level required to push container image tags to the container repository. Valid values include `MAINTAINER`, `OWNER`, or `ADMIN`. | #### `AlertManagementIntegration` @@ -48870,6 +48870,20 @@ four standard [pagination arguments](#pagination-arguments): | ---- | ---- | ----------- | | `state` | [`TodoStateEnum`](#todostateenum) | State of the to-do items. | +#### `CustomRoleInterface` + +Implementations: + +- [`AdminMemberRole`](#adminmemberrole) +- [`MemberRole`](#memberrole) + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `createdAt` | [`Time!`](#time) | Timestamp of when the member role was created. | +| `editPath` {{< icon name="warning-solid" >}} | [`String!`](#string) | **Introduced** in GitLab 16.11. **Status**: Experiment. Web UI path to edit the custom role. | + #### `DependencyInterface` Implementations: @@ -49110,6 +49124,20 @@ Returns [`UserMergeRequestInteraction`](#usermergerequestinteraction). | ---- | ---- | ----------- | | `id` | [`MergeRequestID!`](#mergerequestid) | Global ID of the merge request. | +#### `MemberRoleInterface` + +Implementations: + +- [`MemberRole`](#memberrole) +- [`StandardRole`](#standardrole) + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `membersCount` {{< icon name="warning-solid" >}} | [`Int`](#int) | **Introduced** in GitLab 17.3. **Status**: Experiment. Number of times the role has been directly assigned to a group or project member. | +| `usersCount` {{< icon name="warning-solid" >}} | [`Int`](#int) | **Introduced** in GitLab 17.5. **Status**: Experiment. Number of users who have been directly assigned the role in at least one group or project. | + #### `NamespacesLinkPaths` Implementations: @@ -49323,9 +49351,7 @@ Implementations: | `description` | [`String`](#string) | Role description. | | `detailsPath` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 17.4. **Status**: Experiment. URL path to the role details webpage. | | `id` | [`ID!`](#id) | Role ID. | -| `membersCount` {{< icon name="warning-solid" >}} | [`Int`](#int) | **Introduced** in GitLab 17.3. **Status**: Experiment. Number of times the role has been directly assigned to a group or project member. | | `name` | [`String`](#string) | Role name. | -| `usersCount` {{< icon name="warning-solid" >}} | [`Int`](#int) | **Introduced** in GitLab 17.5. **Status**: Experiment. Number of users who have been directly assigned the role in at least one group or project. | #### `Service` diff --git a/doc/development/sidekiq/logging.md b/doc/development/sidekiq/logging.md index 7d37efc1e6c..436785745ea 100644 --- a/doc/development/sidekiq/logging.md +++ b/doc/development/sidekiq/logging.md @@ -58,7 +58,7 @@ somewhere within the worker: deletion_cutoff = Gitlab::CurrentSettings .deletion_adjourned_period.days.ago.to_date projects = Project.with_route.with_namespace - .aimed_for_deletion(deletion_cutoff) + .marked_for_deletion_before(deletion_cutoff) projects.find_each(batch_size: 100).with_index do |project, index| delay = index * INTERVAL diff --git a/doc/user/compliance/compliance_frameworks.md b/doc/user/compliance/compliance_frameworks.md index 6355d0624c6..33b10448463 100644 --- a/doc/user/compliance/compliance_frameworks.md +++ b/doc/user/compliance/compliance_frameworks.md @@ -195,6 +195,11 @@ You can use GitLab compliance controls or external controls for framework requir GitLab compliance controls can be used in GitLab compliance frameworks. Controls are checks against the configuration or behavior of projects that are assigned to a compliance framework. +Combine GitLab compliance controls to help you meet +[compliance standards](compliance_frameworks/compliance_standards.md). + + + | Control name | Control ID | Description | |:---------------------------------------------------------|:-----------------------------------------------------------|:------------| | API security running | `scanner_api_security_running` | Ensures that [API security scanning](../application_security/api_security/_index.md) is configured and running in the project pipelines. | diff --git a/doc/user/compliance/compliance_frameworks/compliance_standards.md b/doc/user/compliance/compliance_frameworks/compliance_standards.md new file mode 100644 index 00000000000..3dd06d941ff --- /dev/null +++ b/doc/user/compliance/compliance_frameworks/compliance_standards.md @@ -0,0 +1,28 @@ +--- +stage: Software Supply Chain Security +group: Compliance +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: Compliance standards +--- + +You can use [GitLab compliance controls](../compliance_frameworks.md#gitlab-compliance-controls) to help meet the +requirements of many compliance standards. + +## ISO 27001 compliance requirements + +ISO 27001 is an internationally recognized standard that provides a framework for implementing and managing an +Information Security Management System (ISMS). + +The following table lists the requirements supported by GitLab for ISO 27001 and the controls for the requirements. + +| ISO 27001 requirement | Description | Supported controls | +|:----------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------| +| 5.3 Segregation of duties | Conflicting duties and conflicting areas of responsibility shall be segregated. | | +| 5.17 Authentication information | Allocation and management of authentication information should be controlled by a management process, including advising personnel on the appropriate handling of authentication information. | | +| 5.18 Access rights | Access rights to information and other associated assets should be provisioned, reviewed, modified, and removed in accordance with the organization's topic-specific policy on and rules for access control. | | +| 5.32 Intellectual property rights | The organization should implement appropriate procedures to protect intellectual property rights. | | +| 8.4 Access to source code | Read and write access to source code, development tools and software libraries shall be appropriately managed. | | +| 8.8 Management of technical vulnerabilities | Information about technical vulnerabilities of information systems in use shall be obtained, the organization's exposure to such vulnerabilities shall be evaluated and appropriate measures shall be taken. | | +| 8.28 Secure coding | Secure coding principles shall be applied to software development. | | +| 8.29 Security testing in development and acceptance | Security testing processes shall be defined and implemented in the development lifecycle. | | +| 8.32 Change management | Changes to information processing facilities and information systems shall be subject to change management procedures. | | diff --git a/lib/backup/targets/database.rb b/lib/backup/targets/database.rb index 49565c19caa..989b5e3b9af 100644 --- a/lib/backup/targets/database.rb +++ b/lib/backup/targets/database.rb @@ -16,7 +16,9 @@ module Backup # Ignore the DROP errors; recent database dumps will use --if-exists with pg_dump /does not exist$/, # User may not have permissions to drop extensions or schemas - /must be owner of/ + /must be owner of/, + # PG16 introduced generally ignorable error `must be able to SET ROLE "gitlab-psql"` + /must be able to SET ROLE "gitlab-psql"/i ].freeze IGNORED_ERRORS_REGEXP = Regexp.union(IGNORED_ERRORS).freeze diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bfac41ff80d..4c99d733850 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -37689,6 +37689,9 @@ msgstr "" msgid "MemberRole|Admin role is assigned to one or more users. Remove role from all users, then delete role." msgstr "" +msgid "MemberRole|Admin role is used by one or more LDAP synchronizations. Remove LDAP syncs, then delete role." +msgstr "" + msgid "MemberRole|Are you sure you want to delete this role?" msgstr "" diff --git a/rubocop/cop/gitlab/disallow_current_organization_id_safe_navigation.rb b/rubocop/cop/gitlab/disallow_current_organization_id_safe_navigation.rb new file mode 100644 index 00000000000..ef78d4d88d5 --- /dev/null +++ b/rubocop/cop/gitlab/disallow_current_organization_id_safe_navigation.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module Gitlab + # Discourages the use of `Current.organization&.id`. + # + # `Current.organization` is expected to be assigned in contexts where its ID is accessed. + # If `Current.organization` is not assigned, attempting to access `id` directly + # (i.e., `Current.organization.id`) will correctly raise a + # `Current::OrganizationNotAssignedError`. Using the safe navigation operator (`&.id`) + # prevents this error from being raised, potentially hiding issues where + # `Current.organization` was not properly set up. + # + # This cop enforces the direct use of `Current.organization.id` to ensure + # that `Current::OrganizationNotAssignedError` is raised if `Current.organization` is nil. + # + # @example + # + # # bad + # id = Current.organization&.id + # id = ::Current.organization&.id + # + # # good + # # If Current.organization is expected to be present (which it is), + # # this will raise Current::OrganizationNotAssignedError if it's unexpectedly nil, + # # making the underlying issue visible. + # id = Current.organization.id + # + class DisallowCurrentOrganizationIdSafeNavigation < RuboCop::Cop::Base + extend AutoCorrector + + MSG = 'Use `Current.organization.id` instead of `Current.organization&.id`. ' \ + '`Current.organization` is expected to be assigned.' + + # @!method current_organization_safe_id?(node) + def_node_matcher :current_organization_safe_id?, <<~PATTERN + (csend + (send + (const {nil? | cbase} :Current) :organization) :id) + PATTERN + + def on_csend(node) + return unless current_organization_safe_id?(node) + + add_offense(node) do |corrector| + operator_range = node.loc.operator + + if operator_range.nil? && node.receiver && node.loc.selector + # Fallback: If node.loc.operator is nil, try to determine the range + # by looking at the space between the receiver and the method selector. + receiver_end_pos = node.receiver.source_range.end_pos + selector_begin_pos = node.loc.selector.begin_pos + + if receiver_end_pos < selector_begin_pos + # This range covers the characters between the end of the receiver + # and the start of the selector, which should be the operator. + operator_range = Parser::Source::Range.new(node.source_range.source_buffer, + receiver_end_pos, + selector_begin_pos) + end + end + + corrector.replace(operator_range, '.') if operator_range + end + end + end + end + end +end diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index 85052ba992d..070f4cc22b2 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -198,7 +198,10 @@ RSpec.describe ProjectsFinder, feature_category: :groups_and_projects do let_it_be(:aimed_for_deletion_project) { create(:project, :public, marked_for_deletion_at: 2.days.ago, pending_delete: false) } let_it_be(:pending_deletion_project) { create(:project, :public, marked_for_deletion_at: 1.month.ago, pending_delete: true) } - it { is_expected.to contain_exactly(aimed_for_deletion_project) } + let_it_be(:group_aimed_for_deletion) { create(:group_with_deletion_schedule) } + let_it_be(:group_aimed_for_deletion_project) { create(:project, :public, group: group_aimed_for_deletion) } + + it { is_expected.to contain_exactly(aimed_for_deletion_project, group_aimed_for_deletion_project) } end describe 'filter by not aimed for deletion' do @@ -206,6 +209,9 @@ RSpec.describe ProjectsFinder, feature_category: :groups_and_projects do let_it_be(:aimed_for_deletion_project) { create(:project, :public, marked_for_deletion_at: 2.days.ago, pending_delete: false) } let_it_be(:pending_deletion_project) { create(:project, :public, marked_for_deletion_at: 1.month.ago, pending_delete: true) } + let_it_be(:group_aimed_for_deletion) { create(:group_with_deletion_schedule) } + let_it_be(:group_aimed_for_deletion_project) { create(:project, :public, group: group_aimed_for_deletion) } + it { is_expected.to contain_exactly(public_project, internal_project) } end @@ -370,11 +376,16 @@ RSpec.describe ProjectsFinder, feature_category: :groups_and_projects do let_it_be(:archived_project) { create(:project, :archived, :public) } let_it_be(:for_deletion_project) { create(:project, :public, marked_for_deletion_at: Date.current) } + let_it_be(:archived_group_project) { create(:project, :public, group: create(:group, :archived)) } + let_it_be(:for_deletion_group_project) do + create(:project, :public, group: create(:group_with_deletion_schedule)) + end + where :test_params, :expected_projects do - {} | [ref(:active_projects), ref(:archived_project), ref(:for_deletion_project)] - { active: nil } | [ref(:active_projects), ref(:archived_project), ref(:for_deletion_project)] + {} | [ref(:active_projects), ref(:archived_project), ref(:for_deletion_project), ref(:archived_group_project), ref(:for_deletion_group_project)] + { active: nil } | [ref(:active_projects), ref(:archived_project), ref(:for_deletion_project), ref(:archived_group_project), ref(:for_deletion_group_project)] { active: true } | [ref(:active_projects)] - { active: false } | [ref(:archived_project), ref(:for_deletion_project)] + { active: false } | [ref(:archived_project), ref(:for_deletion_project), ref(:archived_group_project), ref(:for_deletion_group_project)] end with_them do @@ -385,7 +396,8 @@ RSpec.describe ProjectsFinder, feature_category: :groups_and_projects do end describe 'filter by archived' do - let!(:archived_project) { create(:project, :public, :archived, name: 'E', path: 'E') } + let_it_be(:archived_project) { create(:project, :public, :archived, name: 'E', path: 'E') } + let_it_be(:archived_group_project) { create(:project, :public, group: create(:group, :archived)) } context 'non_archived=true' do let(:params) { { non_archived: true } } @@ -396,13 +408,13 @@ RSpec.describe ProjectsFinder, feature_category: :groups_and_projects do context 'non_archived=false' do let(:params) { { non_archived: false } } - it { is_expected.to match_array([public_project, internal_project, archived_project]) } + it { is_expected.to match_array([public_project, internal_project, archived_project, archived_group_project]) } end describe 'filter by archived only' do let(:params) { { archived: 'only' } } - it { is_expected.to eq([archived_project]) } + it { is_expected.to eq([archived_project, archived_group_project]) } end describe 'filter by archived for backward compatibility' do @@ -414,7 +426,7 @@ RSpec.describe ProjectsFinder, feature_category: :groups_and_projects do describe 'filter by archived is present and is nil' do let(:params) { { archived: nil } } - it { is_expected.to match_array([public_project, internal_project, archived_project]) } + it { is_expected.to match_array([public_project, internal_project, archived_project, archived_group_project]) } end end diff --git a/spec/fixtures/trace/sample_trace b/spec/fixtures/trace/sample_trace index ad01c113062..f17839b850a 100644 --- a/spec/fixtures/trace/sample_trace +++ b/spec/fixtures/trace/sample_trace @@ -2480,8 +2480,6 @@ Groups::MilestonesController returns not found GroupsHelper - group_icon - returns an url for the avatar group_icon_url returns an url for the avatar gives default avatar_icon when no avatar is present diff --git a/spec/frontend/fixtures/container_registry.rb b/spec/frontend/fixtures/container_registry.rb index 2cb69024f20..d277373ecc6 100644 --- a/spec/frontend/fixtures/container_registry.rb +++ b/spec/frontend/fixtures/container_registry.rb @@ -32,18 +32,6 @@ RSpec.describe 'Container registry (JavaScript fixtures)', feature_category: :co } end - shared_examples 'container registry protection tag rules' do |fixture_suffix| - before do - create(:container_registry_protection_tag_rule, :immutable, project: project) - end - - it "graphql/#{project_container_protection_tag_rules_query_path}.#{fixture_suffix}.json" do - post_graphql(query, current_user: user, variables: variables) - - expect_graphql_errors_to_be_empty - end - end - before do stub_gitlab_api_client_to_support_gitlab_api(supported: true) end @@ -85,18 +73,6 @@ RSpec.describe 'Container registry (JavaScript fixtures)', feature_category: :co end end - context 'with immutable tag protection rules' do - it_behaves_like 'container registry protection tag rules', 'immutable_rules' - end - - context 'with immutable tag protection rules as maintainer' do - before_all do - project.add_maintainer(user) - end - - it_behaves_like 'container registry protection tag rules', 'immutable_rules_maintainer' - end - context 'with maximum number of tag protection rules' do before do 5.times do |i| diff --git a/spec/graphql/types/container_registry/protection/access_level_type_spec.rb b/spec/graphql/types/container_registry/protection/access_level_type_spec.rb index 9061818df37..a614c54e7b7 100644 --- a/spec/graphql/types/container_registry/protection/access_level_type_spec.rb +++ b/spec/graphql/types/container_registry/protection/access_level_type_spec.rb @@ -7,21 +7,15 @@ RSpec.describe GitlabSchema.types['ContainerProtectionAccessLevel'], feature_cat specify { expect(described_class.description).to be_present } - describe 'minimum_access_level_for_push' do + describe 'minimum_access_level_for_push', unless: Gitlab.ee? do subject { described_class.fields['minimumAccessLevelForPush'] } - it { is_expected.to have_nullable_graphql_type(Types::ContainerRegistry::Protection::TagRuleAccessLevelEnum) } + it { is_expected.to have_non_null_graphql_type(Types::ContainerRegistry::Protection::TagRuleAccessLevelEnum) } end - describe 'minimum_access_level_for_delete' do + describe 'minimum_access_level_for_delete', unless: Gitlab.ee? do subject { described_class.fields['minimumAccessLevelForDelete'] } - it { is_expected.to have_nullable_graphql_type(Types::ContainerRegistry::Protection::TagRuleAccessLevelEnum) } - end - - describe 'immutable' do - subject { described_class.fields['immutable'] } - - it { is_expected.to have_non_null_graphql_type(GraphQL::Types::Boolean) } + it { is_expected.to have_non_null_graphql_type(Types::ContainerRegistry::Protection::TagRuleAccessLevelEnum) } end end diff --git a/spec/graphql/types/container_registry/protection/tag_rule_type_spec.rb b/spec/graphql/types/container_registry/protection/tag_rule_type_spec.rb index 14e4fc380fc..41735efe99b 100644 --- a/spec/graphql/types/container_registry/protection/tag_rule_type_spec.rb +++ b/spec/graphql/types/container_registry/protection/tag_rule_type_spec.rb @@ -25,21 +25,15 @@ RSpec.describe GitlabSchema.types['ContainerProtectionTagRule'], feature_categor it { is_expected.to have_non_null_graphql_type(GraphQL::Types::String) } end - describe 'minimum_access_level_for_push' do + describe 'minimum_access_level_for_push', unless: Gitlab.ee? do subject { described_class.fields['minimumAccessLevelForPush'] } - it { is_expected.to have_nullable_graphql_type(Types::ContainerRegistry::Protection::TagRuleAccessLevelEnum) } + it { is_expected.to have_non_null_graphql_type(Types::ContainerRegistry::Protection::TagRuleAccessLevelEnum) } end - describe 'minimum_access_level_for_delete' do + describe 'minimum_access_level_for_delete', unless: Gitlab.ee? do subject { described_class.fields['minimumAccessLevelForDelete'] } - it { is_expected.to have_nullable_graphql_type(Types::ContainerRegistry::Protection::TagRuleAccessLevelEnum) } - end - - describe 'immutable' do - subject { described_class.fields['immutable'] } - - it { is_expected.to have_non_null_graphql_type(GraphQL::Types::Boolean) } + it { is_expected.to have_non_null_graphql_type(Types::ContainerRegistry::Protection::TagRuleAccessLevelEnum) } end end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 3bc72b65702..c6e8ba3a95f 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -1578,29 +1578,10 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj end end - describe 'container_protection_tag_rules' do + describe 'container_protection_tag_rules', unless: Gitlab.ee? do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } - before_all do - create(:container_registry_protection_tag_rule, :immutable, - project: project, - tag_name_pattern: 'immutable-1' - ) - - create(:container_registry_protection_tag_rule, - project: project, - minimum_access_level_for_push: Gitlab::Access::MAINTAINER, - minimum_access_level_for_delete: Gitlab::Access::OWNER, - tag_name_pattern: 'mutable' - ) - - create(:container_registry_protection_tag_rule, :immutable, - project: project, - tag_name_pattern: 'immutable-2' - ) - end - let(:query) do %( query { @@ -1618,52 +1599,37 @@ RSpec.describe GitlabSchema.types['Project'], feature_category: :groups_and_proj ) end - subject { GitlabSchema.execute(query, context: { current_user: user }).as_json } + before_all do + create(:container_registry_protection_tag_rule, :immutable, + project: project, + tag_name_pattern: 'immutable-1' + ) + + create(:container_registry_protection_tag_rule, + project: project, + minimum_access_level_for_push: Gitlab::Access::MAINTAINER, + minimum_access_level_for_delete: Gitlab::Access::OWNER, + tag_name_pattern: 'mutable' + ) + end + + subject do + GitlabSchema.execute(query, context: { current_user: user }) + .as_json.dig('data', 'project', 'containerProtectionTagRules', 'nodes') + end before do project.add_maintainer(user) end - it 'returns tag rules with mutable ones first' do - result_nodes = subject.dig('data', 'project', 'containerProtectionTagRules', 'nodes') - - expect(result_nodes.size).to eq(3) - - expect(result_nodes[0]).to include( - 'tagNamePattern' => 'mutable', - 'minimumAccessLevelForPush' => 'MAINTAINER', - 'minimumAccessLevelForDelete' => 'OWNER' - ) - - expect(result_nodes[1]).to include( - 'tagNamePattern' => 'immutable-1', - 'minimumAccessLevelForPush' => nil, - 'minimumAccessLevelForDelete' => nil - ) - - expect(result_nodes[2]).to include( - 'tagNamePattern' => 'immutable-2', - 'minimumAccessLevelForPush' => nil, - 'minimumAccessLevelForDelete' => nil - ) - end - - context 'when the feature container_registry_immutable_tags is disabled' do - before do - stub_feature_flags(container_registry_immutable_tags: false) - end - - it 'only returns mutable tag rules' do - result_nodes = subject.dig('data', 'project', 'containerProtectionTagRules', 'nodes') - - expect(result_nodes.size).to eq(1) - - expect(result_nodes[0]).to include( + it do + is_expected.to have_attributes(size: 1).and contain_exactly( + a_hash_including( 'tagNamePattern' => 'mutable', 'minimumAccessLevelForPush' => 'MAINTAINER', 'minimumAccessLevelForDelete' => 'OWNER' ) - end + ) end end end diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb index d988bded4ea..129a69c1d65 100644 --- a/spec/helpers/avatars_helper_spec.rb +++ b/spec/helpers/avatars_helper_spec.rb @@ -7,54 +7,6 @@ RSpec.describe AvatarsHelper, feature_category: :source_code_management do let_it_be(:user) { create(:user) } - describe '#group_icon, #topic_icon' do - shared_examples 'resource with a default avatar' do |source_type| - it 'returns a default avatar div' do - expect(public_send("#{source_type}_icon", *helper_args)) - .to match(%r{F}) - end - end - - shared_examples 'resource with a custom avatar' do |source_type| - it 'returns a custom avatar image' do - expect(public_send("#{source_type}_icon", *helper_args)) - .to eq "" - end - end - - shared_examples 'Gitaly exception handling' do - before do - allow(resource).to receive(:avatar_url).and_raise(error_class) - end - - it_behaves_like 'resource with a default avatar', 'project' - end - - context 'when providing a group' do - it_behaves_like 'resource with a default avatar', 'group' do - let(:resource) { create(:group, name: 'foo') } - let(:helper_args) { [resource] } - end - - it_behaves_like 'resource with a custom avatar', 'group' do - let(:resource) { create(:group, avatar: File.open(uploaded_image_temp_path)) } - let(:helper_args) { [resource] } - end - end - - context 'when providing a topic' do - it_behaves_like 'resource with a default avatar', 'topic' do - let(:resource) { create(:topic, name: 'foo') } - let(:helper_args) { [resource] } - end - - it_behaves_like 'resource with a custom avatar', 'topic' do - let(:resource) { create(:topic, avatar: File.open(uploaded_image_temp_path)) } - let(:helper_args) { [resource] } - end - end - end - describe '#avatar_icon_for' do let!(:user) { create(:user, avatar: File.open(uploaded_image_temp_path), email: 'bar@example.com') } let(:email) { 'foo@example.com' } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 62f2254dd44..d634a47ab60 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -669,6 +669,216 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr end end + describe 'scopes' do + shared_examples 'includes projects in hierarchy marked for deletion' do + let_it_be(:group) { create(:group) } + let_it_be(:active_project) { create(:project, group: group) } + let_it_be(:for_deletion_project) { create(:project, group: group, marked_for_deletion_at: Date.current) } + + context 'when parent group is active' do + it 'returns only projects marked for deletion' do + expect(subject).to include(for_deletion_project) + expect(subject).not_to include(active_project) + end + end + + context 'when parent group is marked for deletion' do + let_it_be(:group_deletion_schedule) { create(:group_deletion_schedule, group: group) } + + it 'returns all projects in the group' do + expect(subject).to include(for_deletion_project, active_project) + end + end + end + + shared_examples 'excludes projects in hierarchy marked for deletion' do + let_it_be(:group) { create(:group) } + let_it_be(:active_project) { create(:project, group: group) } + let_it_be(:for_deletion_project) { create(:project, group: group, marked_for_deletion_at: Date.current) } + + context 'when parent group is active' do + it 'returns only active projects' do + expect(subject).to include(active_project) + expect(subject).not_to include(for_deletion_project) + end + end + + context 'when parent group is marked for deletion' do + let_it_be(:group_deletion_schedule) { create(:group_deletion_schedule, group: group) } + + it 'excludes all projects in the group' do + expect(subject).not_to include(for_deletion_project, active_project) + end + end + end + + shared_examples 'includes projects in archived hierarchy' do + let_it_be(:group) { create(:group) } + let_it_be(:active_project) { create(:project, group: group) } + let_it_be(:archived_project) { create(:project, group: group, archived: true) } + + context 'when parent group is active' do + it 'returns only archived projects' do + expect(subject).to include(archived_project) + expect(subject).not_to include(active_project) + end + end + + context 'when parent group is archived' do + before do + group.archive + end + + it 'returns all projects in the group' do + expect(subject).to include(archived_project, active_project) + end + end + end + + shared_examples 'excludes projects in archived hierarchy' do + let_it_be(:group) { create(:group) } + let_it_be(:active_project) { create(:project, group: group) } + let_it_be(:archived_project) { create(:project, group: group, archived: true) } + + context 'when parent group is active' do + it 'returns only active projects' do + expect(subject).to include(active_project) + expect(subject).not_to include(archived_project) + end + end + + context 'when parent group is archived' do + before do + group.archive + end + + it 'excludes all projects in the group' do + expect(subject).not_to include(archived_project, active_project) + end + end + end + + describe '.aimed_for_deletion' do + let_it_be(:active_project) { create(:project) } + let_it_be(:for_deletion_project) { create(:project, marked_for_deletion_at: Date.current) } + + it 'returns projects marked for deletion' do + result = described_class.aimed_for_deletion + + expect(result).to include(for_deletion_project) + expect(result).not_to include(active_project) + end + end + + describe '.self_or_ancestors_aimed_for_deletion' do + subject { described_class.self_or_ancestors_aimed_for_deletion } + + it_behaves_like 'includes projects in hierarchy marked for deletion' + end + + describe '.not_aimed_for_deletion' do + let_it_be(:active_project) { create(:project) } + let_it_be(:for_deletion_project) { create(:project, marked_for_deletion_at: Date.current) } + + it 'returns projects not marked for deletion' do + result = described_class.not_aimed_for_deletion + + expect(result).to include(active_project) + expect(result).not_to include(for_deletion_project) + end + end + + describe '.self_and_ancestors_not_aimed_for_deletion' do + subject { described_class.self_and_ancestors_not_aimed_for_deletion } + + it_behaves_like 'excludes projects in hierarchy marked for deletion' + end + + describe '.marked_for_deletion_on' do + let_it_be(:active_project) { create(:project) } + let_it_be(:for_deletion_project) { create(:project, marked_for_deletion_at: Date.parse('2024-01-01')) } + + context 'when date is provided' do + it 'returns projects marked for deletion on that date' do + result = described_class.marked_for_deletion_on(Date.parse('2024-01-01')) + expect(result).to contain_exactly(for_deletion_project) + end + end + + context 'when date is nil' do + it 'returns projects not marked for deletion' do + result = described_class.marked_for_deletion_on(nil) + expect(result).to contain_exactly(active_project) + end + end + end + + describe '.marked_for_deletion_before' do + let_it_be(:cutoff_date) { 10.days.ago } + let_it_be(:active_project) { create(:project) } + let_it_be(:marked_after) { create(:project, marked_for_deletion_at: cutoff_date + 2.days) } + let_it_be(:marked_before) { create(:project, marked_for_deletion_at: cutoff_date - 2.days) } + let_it_be(:marked_on_date) { create(:project, marked_for_deletion_at: cutoff_date) } + + it 'returns projects marked for deletion on or before the specified date' do + result = described_class.marked_for_deletion_before(cutoff_date) + + expect(result).to include(marked_before, marked_on_date) + expect(result).not_to include(marked_after, active_project) + end + end + + describe '.archived' do + let_it_be(:active_project) { create(:project, archived: false) } + let_it_be(:archived_project) { create(:project, archived: true) } + + it 'returns archived projects' do + result = described_class.archived + + expect(result).to include(archived_project) + expect(result).not_to include(active_project) + end + end + + describe '.self_or_ancestors_archived' do + subject { described_class.self_or_ancestors_archived } + + it_behaves_like 'includes projects in archived hierarchy' + end + + describe '.non_archived' do + let_it_be(:active_project) { create(:project, archived: false) } + let_it_be(:archived_project) { create(:project, archived: true) } + + it 'returns non-archived projects' do + result = described_class.non_archived + + expect(result).to include(active_project) + expect(result).not_to include(archived_project) + end + end + + describe '.self_and_ancestors_non_archived' do + subject { described_class.self_and_ancestors_non_archived } + + it_behaves_like 'excludes projects in archived hierarchy' + end + + describe '.self_and_ancestors_active' do + subject { described_class.self_and_ancestors_active } + + it_behaves_like 'excludes projects in archived hierarchy' + it_behaves_like 'excludes projects in hierarchy marked for deletion' + end + + describe '.self_or_ancestors_inactive' do + subject { described_class.self_or_ancestors_inactive } + + it_behaves_like 'includes projects in archived hierarchy' + it_behaves_like 'includes projects in hierarchy marked for deletion' + end + end + describe 'modules' do subject { described_class } @@ -2454,32 +2664,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr end end - describe '.not_aimed_for_deletion' do - let_it_be(:project) { create(:project) } - let_it_be(:delayed_deletion_project) { create(:project, marked_for_deletion_at: Date.current) } - - it do - expect(described_class.not_aimed_for_deletion).to contain_exactly(project) - end - end - - describe '.by_marked_for_deletion_on' do - let_it_be(:project) { create(:project) } - let_it_be(:marked_for_deletion_project) { create(:project, marked_for_deletion_at: Date.parse('2024-01-01')) } - - context 'when marked_for_deletion_on is present' do - it 'return projects marked for deletion' do - expect(described_class.by_marked_for_deletion_on(Date.parse('2024-01-01'))).to contain_exactly(marked_for_deletion_project) - end - end - - context 'when marked_for_deletion_on is not present' do - it 'return projects not marked for deletion' do - expect(described_class.by_marked_for_deletion_on(nil)).to contain_exactly(project) - end - end - end - describe '.sorted_by_similarity_desc' do let_it_be(:project_a) { create(:project, path: 'similar-1', name: 'similar-1', description: 'A similar project') } let_it_be_with_reload(:project_b) { create(:project, path: 'similar-2', name: 'similar-2', description: 'A related project') } diff --git a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb index 5360de50a9b..17a9c11862a 100644 --- a/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb +++ b/spec/requests/api/graphql/container_repository/container_repository_details_spec.rb @@ -720,45 +720,6 @@ RSpec.describe 'container repository details', feature_category: :container_regi } ) end - - context 'when there is an immutable rule' do - before_all do - create( - :container_registry_protection_tag_rule, - :immutable, - project: project, - tag_name_pattern: 'la' - ) - end - - it 'returns the maximum access fields from the matching protection rules' do - subject - - expect(tag_permissions_response).to eq( - { - 'minimumAccessLevelForPush' => nil, - 'minimumAccessLevelForDelete' => nil - } - ) - end - - context 'when the feature container_registry_immutable_tags is disabled' do - before do - stub_feature_flags(container_registry_immutable_tags: false) - end - - it 'ignores the immutable rule' do - subject - - expect(tag_permissions_response).to eq( - { - 'minimumAccessLevelForPush' => 'OWNER', - 'minimumAccessLevelForDelete' => 'OWNER' - } - ) - end - end - end end context 'for tags destroyContainerRepositoryTag field' do diff --git a/spec/requests/api/graphql/mutations/container_registry/protection/tag_rule/create_spec.rb b/spec/requests/api/graphql/mutations/container_registry/protection/tag_rule/create_spec.rb index 691444eadf8..c804ae37ca4 100644 --- a/spec/requests/api/graphql/mutations/container_registry/protection/tag_rule/create_spec.rb +++ b/spec/requests/api/graphql/mutations/container_registry/protection/tag_rule/create_spec.rb @@ -83,16 +83,16 @@ RSpec.describe 'Creating the container registry tag protection rule', :aggregate it_behaves_like 'returning a GraphQL error', [/minimumAccessLevelForPush/, /minimumAccessLevelForDelete/] end - context 'with blank input for the field `minimumAccessLevelForPush`' do + context 'with blank input for the field `minimumAccessLevelForPush`', unless: Gitlab.ee? do let(:input) { super().merge(minimum_access_level_for_push: nil) } - it_behaves_like 'returning a mutation error', 'Access levels should either both be present or both be nil' + it_behaves_like 'returning a GraphQL error', /minimumAccessLevelForPush/ end - context 'with blank input for the field `minimumAccessLevelForDelete`' do + context 'with blank input for the field `minimumAccessLevelForDelete`', unless: Gitlab.ee? do let(:input) { super().merge(minimum_access_level_for_delete: nil) } - it_behaves_like 'returning a mutation error', 'Access levels should either both be present or both be nil' + it_behaves_like 'returning a GraphQL error', /minimumAccessLevelForDelete/ end context 'with blank input field `tagNamePattern`' do diff --git a/spec/requests/api/graphql/mutations/container_registry/protection/tag_rule/delete_spec.rb b/spec/requests/api/graphql/mutations/container_registry/protection/tag_rule/delete_spec.rb index ab65d333782..2ffd1aead9b 100644 --- a/spec/requests/api/graphql/mutations/container_registry/protection/tag_rule/delete_spec.rb +++ b/spec/requests/api/graphql/mutations/container_registry/protection/tag_rule/delete_spec.rb @@ -38,20 +38,19 @@ RSpec.describe Mutations::ContainerRegistry::Protection::TagRule::Delete, :aggre it 'responds with deleted container registry tag protection rule' do expect { post_graphql_mutation_request } - .to change { ::ContainerRegistry::Protection::TagRule.count }.from(1).to(0) + .to change { ::ContainerRegistry::Protection::TagRule.count }.by(-1) expect(mutation_response).to include( 'errors' => be_blank, - 'containerProtectionTagRule' => { + 'containerProtectionTagRule' => hash_including( 'id' => container_protection_rule.to_global_id.to_s, 'tagNamePattern' => container_protection_rule.tag_name_pattern, 'minimumAccessLevelForDelete' => container_protection_rule.minimum_access_level_for_delete.upcase, 'minimumAccessLevelForPush' => container_protection_rule.minimum_access_level_for_push.upcase, - 'immutable' => container_protection_rule.immutable?, 'userPermissions' => { 'destroyContainerRegistryProtectionTagRule' => true } - } + ) ) end diff --git a/spec/requests/api/graphql/mutations/container_registry/protection/tag_rule/update_spec.rb b/spec/requests/api/graphql/mutations/container_registry/protection/tag_rule/update_spec.rb index ef855a630f2..e5ae6e8fa7d 100644 --- a/spec/requests/api/graphql/mutations/container_registry/protection/tag_rule/update_spec.rb +++ b/spec/requests/api/graphql/mutations/container_registry/protection/tag_rule/update_spec.rb @@ -13,10 +13,6 @@ RSpec.describe 'Updating the container registry tag protection rule', :aggregate let_it_be(:current_user) { create(:user, maintainer_of: project) } - let(:container_protection_tag_rule_attributes) do - build_stubbed(:container_protection_tag_rule, project: project) - end - let(:mutation) do graphql_mutation(:update_container_protection_tag_rule, input, <<~QUERY @@ -68,8 +64,8 @@ RSpec.describe 'Updating the container registry tag protection rule', :aggregate post_graphql_mutation_request.tap do expect(container_protection_tag_rule.reload).to have_attributes( tag_name_pattern: input[:tag_name_pattern], - minimum_access_level_for_push: input[:minimum_access_level_for_push]&.downcase, - minimum_access_level_for_delete: input[:minimum_access_level_for_delete]&.downcase + minimum_access_level_for_push: input[:minimum_access_level_for_push].downcase, + minimum_access_level_for_delete: input[:minimum_access_level_for_delete].downcase ) end end @@ -121,12 +117,6 @@ RSpec.describe 'Updating the container registry tag protection rule', :aggregate it_behaves_like 'returning a GraphQL error', /tagNamePattern can't be blank/ end - context 'with blank input fields `minimumAccessLevelForPush` and `minimumAccessLevelForDelete`' do - let(:input) { super().merge(minimum_access_level_for_push: nil, minimum_access_level_for_delete: nil) } - - it_behaves_like 'a successful response' - end - context 'with only `minimumAccessLevelForDelete` blank' do let(:input) { super().merge(minimum_access_level_for_delete: nil) } diff --git a/spec/rubocop/cop/gitlab/disallow_current_organization_id_safe_navigation_spec.rb b/spec/rubocop/cop/gitlab/disallow_current_organization_id_safe_navigation_spec.rb new file mode 100644 index 00000000000..5cc998ccb7b --- /dev/null +++ b/spec/rubocop/cop/gitlab/disallow_current_organization_id_safe_navigation_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'rubocop_spec_helper' +require 'rubocop/rspec/support' +require_relative '../../../../rubocop/cop/gitlab/disallow_current_organization_id_safe_navigation' + +RSpec.describe RuboCop::Cop::Gitlab::DisallowCurrentOrganizationIdSafeNavigation, feature_category: :organization do + include RuboCop::RSpec::ExpectOffense + + let(:hardcoded_expected_message) do + 'Use `Current.organization.id` instead of `Current.organization&.id`. ' \ + '`Current.organization` is expected to be assigned.' + end + + context 'when `Current.organization&.id` is used' do + it 'registers an offense and autocorrects `Current.organization&.id`' do + expect_offense(<<~RUBY) + id = Current.organization&.id + ^^^^^^^^^^^^^^^^^^^^^^^^ #{hardcoded_expected_message} + RUBY + + expect_correction(<<~RUBY) + id = Current.organization.id + RUBY + end + + # This is the test case that was failing due to the spec's conditional logic. + # Simplify it as follows: + it 'registers an offense and autocorrects `::Current.organization&.id` (top-level constant)' do + expect_offense(<<~RUBY) + id = ::Current.organization&.id + ^^^^^^^^^^^^^^^^^^^^^^^^^^ #{hardcoded_expected_message} + RUBY + + expect_correction(<<~RUBY) + id = ::Current.organization.id + RUBY + end + + it 'registers an offense and autocorrects when used in a condition' do + expect_offense(<<~RUBY) + if Current.organization&.id == 5 + ^^^^^^^^^^^^^^^^^^^^^^^^ #{hardcoded_expected_message} + end + RUBY + + expect_correction(<<~RUBY) + if Current.organization.id == 5 + end + RUBY + end + end + + context 'when related but non-offending patterns are used' do + it 'does not register an offense for `Current.organization.id` (no safe navigation)' do + expect_no_offenses(<<~RUBY) + id = Current.organization.id + RUBY + end + + it 'does not register an offense for `other_object.organization&.id`' do + expect_no_offenses(<<~RUBY) + id = other_object.organization&.id + RUBY + end + + it 'does not register an offense for `Current.other_method&.id`' do + expect_no_offenses(<<~RUBY) + id = Current.other_method&.id + RUBY + end + + it 'does not register an offense for `Current.organization&.other_attribute`' do + expect_no_offenses(<<~RUBY) + id = Current.organization&.other_attribute + RUBY + end + + it 'does not register an offense for just `Current.organization`' do + expect_no_offenses(<<~RUBY) + org = Current.organization + RUBY + end + + it 'does not register an offense for a different safe navigation chain' do + expect_no_offenses(<<~RUBY) + name = Current.user&.name + RUBY + end + end +end