mirror of
https://github.com/gitlabhq/gitlabhq.git
synced 2025-07-21 23:37:47 +00:00
1031 lines
36 KiB
Ruby
1031 lines
36 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Namespace < ApplicationRecord
|
|
include Sortable
|
|
include Gitlab::VisibilityLevel
|
|
include Routable
|
|
include AfterCommitQueue
|
|
include Gitlab::SQL::Pattern
|
|
include FeatureGate
|
|
include FromUnion
|
|
include Gitlab::Utils::StrongMemoize
|
|
include Namespaces::Traversal::Recursive
|
|
include Namespaces::Traversal::Linear
|
|
include Namespaces::Traversal::Cached
|
|
include Namespaces::Traversal::Traversable
|
|
include Namespaces::AdjournedDeletable
|
|
include EachBatch
|
|
include BlocksUnsafeSerialization
|
|
include Ci::NamespaceSettings
|
|
include Referable
|
|
include UseSqlFunctionForPrimaryKeyLookups
|
|
include SafelyChangeColumnDefault
|
|
include Todoable
|
|
|
|
extend Gitlab::Utils::Override
|
|
|
|
ignore_columns :description, :description_html, :cached_markdown_version, remove_with: '18.3', remove_after: '2025-07-17'
|
|
ignore_column :unlock_membership_to_ldap, remove_with: '18.1', remove_after: '2025-05-20'
|
|
|
|
ignore_column :emails_disabled, remove_with: '18.1', remove_after: '2025-05-20'
|
|
|
|
columns_changing_default :organization_id
|
|
|
|
# Tells ActiveRecord not to store the full class name, in order to save some space
|
|
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69794
|
|
self.store_full_sti_class = false
|
|
self.store_full_class_name = false
|
|
|
|
# Prevent users from creating unreasonably deep level of nesting.
|
|
# The number 20 was taken based on maximum nesting level of
|
|
# Android repo (15) + some extra backup.
|
|
NUMBER_OF_ANCESTORS_ALLOWED = 20
|
|
|
|
SR_DISABLED_AND_UNOVERRIDABLE = 'disabled_and_unoverridable'
|
|
SR_DISABLED_AND_OVERRIDABLE = 'disabled_and_overridable'
|
|
SR_ENABLED = 'enabled'
|
|
SHARED_RUNNERS_SETTINGS = [SR_DISABLED_AND_UNOVERRIDABLE, SR_DISABLED_AND_OVERRIDABLE, SR_ENABLED].freeze
|
|
URL_MAX_LENGTH = 255
|
|
STATISTICS_COLUMNS = %i[
|
|
storage_size
|
|
repository_size
|
|
wiki_size
|
|
snippets_size
|
|
lfs_objects_size
|
|
build_artifacts_size
|
|
pipeline_artifacts_size
|
|
packages_size
|
|
uploads_size
|
|
].freeze
|
|
|
|
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
|
has_many :non_archived_projects, -> { where.not(archived: true) }, class_name: 'Project'
|
|
has_many :project_statistics
|
|
has_one :namespace_settings, inverse_of: :namespace, class_name: 'NamespaceSetting', autosave: true
|
|
has_one :namespace_settings_with_ancestors_inherited_settings, -> { with_ancestors_inherited_settings },
|
|
inverse_of: :namespace, class_name: 'NamespaceSetting', primary_key: :id, foreign_key: :namespace_id
|
|
|
|
has_one :ci_cd_settings, inverse_of: :namespace, class_name: 'NamespaceCiCdSetting', autosave: true
|
|
has_one :namespace_details, inverse_of: :namespace, class_name: 'Namespace::Detail', autosave: false
|
|
has_one :namespace_statistics
|
|
has_one :namespace_route, foreign_key: :namespace_id, autosave: false, inverse_of: :namespace, class_name: 'Route'
|
|
has_one :catalog_verified_namespace, class_name: 'Ci::Catalog::VerifiedNamespace', inverse_of: :namespace
|
|
|
|
has_many :namespace_members, foreign_key: :member_namespace_id, inverse_of: :member_namespace, class_name: 'Member'
|
|
|
|
has_one :namespace_ldap_settings, inverse_of: :namespace, class_name: 'Namespaces::LdapSetting', autosave: true
|
|
|
|
has_one :namespace_descendants, class_name: 'Namespaces::Descendants'
|
|
attribute :description
|
|
accepts_nested_attributes_for :namespace_descendants, allow_destroy: true
|
|
|
|
has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace'
|
|
has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner'
|
|
has_many :pending_builds, class_name: 'Ci::PendingBuild'
|
|
|
|
# This should _not_ be `inverse_of: :namespace`, because that would also set
|
|
# `user.namespace` when this user creates a group with themselves as `owner`.
|
|
belongs_to :owner, class_name: 'User'
|
|
belongs_to :organization, class_name: 'Organizations::Organization'
|
|
|
|
belongs_to :parent, class_name: "Namespace"
|
|
has_many :children, -> { where(type: Group.sti_name) }, class_name: "Namespace", foreign_key: :parent_id
|
|
has_many :custom_emoji, inverse_of: :namespace
|
|
has_one :chat_team, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
|
has_one :root_storage_statistics, class_name: 'Namespace::RootStorageStatistics'
|
|
has_one :aggregation_schedule, class_name: 'Namespace::AggregationSchedule'
|
|
has_one :package_setting_relation, inverse_of: :namespace, class_name: 'PackageSetting'
|
|
|
|
has_one :admin_note, inverse_of: :namespace
|
|
accepts_nested_attributes_for :admin_note, update_only: true
|
|
|
|
has_one :ci_namespace_mirror, class_name: 'Ci::NamespaceMirror'
|
|
has_many :sync_events, class_name: 'Namespaces::SyncEvent'
|
|
|
|
has_one :cluster_enabled_grant, inverse_of: :namespace, class_name: 'Clusters::ClusterEnabledGrant'
|
|
has_many :work_items, inverse_of: :namespace
|
|
has_many :work_items_dates_source, inverse_of: :namespace, class_name: 'WorkItems::DatesSource'
|
|
has_many :issues, inverse_of: :namespace
|
|
|
|
has_many :timelog_categories, class_name: 'TimeTracking::TimelogCategory'
|
|
has_many :achievements, class_name: 'Achievements::Achievement'
|
|
has_many :namespace_commit_emails, class_name: 'Users::NamespaceCommitEmail'
|
|
has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::Stage', foreign_key: :group_id, inverse_of: :namespace
|
|
has_many :value_streams, class_name: 'Analytics::CycleAnalytics::ValueStream', foreign_key: :group_id, inverse_of: :namespace
|
|
|
|
has_many :jira_connect_subscriptions, class_name: 'JiraConnectSubscription', foreign_key: :namespace_id, inverse_of: :namespace
|
|
|
|
has_many :import_source_users, class_name: 'Import::SourceUser', foreign_key: :namespace_id, inverse_of: :namespace
|
|
has_one :namespace_import_user, class_name: 'Import::NamespaceImportUser', foreign_key: :namespace_id, inverse_of: :namespace
|
|
has_one :import_user, class_name: 'User', through: :namespace_import_user, foreign_key: :user_id
|
|
|
|
has_many :bot_user_details, class_name: 'UserDetail', foreign_key: 'bot_namespace_id', inverse_of: :bot_namespace
|
|
has_many :bot_users, through: :bot_user_details, source: :user
|
|
has_one :placeholder_user_detail, class_name: 'Import::PlaceholderUserDetail'
|
|
|
|
has_one :deletion_schedule, class_name: 'Namespaces::DeletionSchedule'
|
|
delegate :deleting_user, :marked_for_deletion_at, to: :deletion_schedule, allow_nil: true
|
|
|
|
validates :owner, presence: true, if: ->(n) { n.owner_required? }
|
|
validates :organization, presence: true
|
|
validates :name,
|
|
presence: true,
|
|
length: { maximum: 255 }
|
|
validates :name, uniqueness: { scope: [:type, :parent_id] }, if: -> { parent_id.present? }
|
|
|
|
validates :path,
|
|
presence: true,
|
|
length: { maximum: URL_MAX_LENGTH }
|
|
|
|
validates :path,
|
|
format: { with: Gitlab::Regex.oci_repository_path_regex, message: Gitlab::Regex.oci_repository_path_regex_message },
|
|
if: :path_changed?
|
|
|
|
validates :path, namespace_path: true, if: ->(n) { !n.project_namespace? }
|
|
# Project path validator is used for project namespaces for now to assure
|
|
# compatibility with project paths
|
|
# Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/341764
|
|
validates :path, project_path: true, if: ->(n) { n.project_namespace? }
|
|
|
|
# Introduce minimal path length of 2 characters.
|
|
# Allow change of other attributes without forcing users to
|
|
# rename their user or group. At the same time prevent changing
|
|
# the path without complying with new 2 chars requirement.
|
|
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/225214
|
|
#
|
|
# For ProjectNamespace we don't check minimal path length to keep
|
|
# compatibility with existing project restrictions.
|
|
# Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/341764
|
|
validates :path, length: { minimum: 2 }, if: :enforce_minimum_path_length?
|
|
|
|
validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true }
|
|
|
|
validate :validate_parent_type
|
|
|
|
# ProjectNamespaces excluded as they are not meant to appear in the group hierarchy at the moment.
|
|
validate :nesting_level_allowed, unless: -> { project_namespace? }
|
|
validate :changing_shared_runners_enabled_is_allowed, unless: -> { project_namespace? }
|
|
validate :changing_allow_descendants_override_disabled_shared_runners_is_allowed, unless: -> { project_namespace? }
|
|
validate :parent_organization_match
|
|
validate :no_conflict_with_organization_user_details, if: :path_changed?
|
|
|
|
delegate :name, to: :owner, allow_nil: true, prefix: true
|
|
delegate :avatar_url, to: :owner, allow_nil: true
|
|
|
|
delegate :maven_package_requests_forwarding,
|
|
:pypi_package_requests_forwarding,
|
|
:npm_package_requests_forwarding,
|
|
to: :package_settings
|
|
|
|
delegate :add_creator, :deleted_at, :deleted_at=, :description, :description=, :description_html,
|
|
to: :namespace_details, allow_nil: true
|
|
|
|
with_options to: :namespace_settings do
|
|
delegate :show_diff_preview_in_email, :show_diff_preview_in_email?, :show_diff_preview_in_email=
|
|
delegate :runner_registration_enabled, :runner_registration_enabled?, :runner_registration_enabled=
|
|
delegate :allow_runner_registration_token, :allow_runner_registration_token=
|
|
delegate :math_rendering_limits_enabled?, :lock_math_rendering_limits_enabled?
|
|
delegate :resource_access_token_notify_inherited,
|
|
:resource_access_token_notify_inherited=,
|
|
:lock_resource_access_token_notify_inherited,
|
|
:lock_resource_access_token_notify_inherited=,
|
|
:resource_access_token_notify_inherited?,
|
|
:resource_access_token_notify_inherited_locked?,
|
|
:resource_access_token_notify_inherited_locked_by_ancestor?,
|
|
:resource_access_token_notify_inherited_locked_by_application_setting?
|
|
delegate :jwt_ci_cd_job_token_enabled?
|
|
|
|
with_options allow_nil: true do
|
|
delegate :prevent_sharing_groups_outside_hierarchy, :prevent_sharing_groups_outside_hierarchy=
|
|
delegate :default_branch_protection_defaults
|
|
delegate :archived, :archived=
|
|
delegate :math_rendering_limits_enabled, :lock_math_rendering_limits_enabled
|
|
delegate :emails_enabled, :emails_enabled=
|
|
delegate :web_based_commit_signing_enabled?, :lock_web_based_commit_signing_enabled?
|
|
delegate :web_based_commit_signing_enabled, :lock_web_based_commit_signing_enabled
|
|
end
|
|
end
|
|
|
|
before_create :sync_share_with_group_lock_with_parent
|
|
before_update :sync_share_with_group_lock_with_parent, if: :parent_changed?
|
|
after_update :force_share_with_group_lock_on_descendants, if: -> { saved_change_to_share_with_group_lock? && share_with_group_lock? }
|
|
after_update :expire_first_auto_devops_config_cache, if: -> { saved_change_to_auto_devops_enabled? }
|
|
|
|
after_save :save_namespace_details_changes
|
|
|
|
after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
|
|
|
|
after_sync_traversal_ids :schedule_sync_event_worker # custom callback defined in Namespaces::Traversal::Linear
|
|
|
|
after_commit :expire_child_caches, on: :update, if: -> {
|
|
(Feature.enabled?(:cached_route_lookups, self, type: :ops) &&
|
|
saved_change_to_name?) || saved_change_to_path? || saved_change_to_parent_id?
|
|
}
|
|
|
|
scope :without_deleted, -> { joins(:namespace_details).where(namespace_details: { deleted_at: nil }) }
|
|
scope :user_namespaces, -> { where(type: Namespaces::UserNamespace.sti_name) }
|
|
scope :group_namespaces, -> { where(type: Group.sti_name) }
|
|
scope :project_namespaces, -> { where(type: Namespaces::ProjectNamespace.sti_name) }
|
|
scope :without_project_namespaces, -> { where(Namespace.arel_table[:type].not_eq(Namespaces::ProjectNamespace.sti_name)) }
|
|
scope :sort_by_type, -> { order(arel_table[:type].asc.nulls_first) }
|
|
scope :include_route, -> { includes(:route) }
|
|
scope :by_parent, ->(parent) { where(parent_id: parent) }
|
|
scope :by_root_id, ->(root_id) { where('traversal_ids[1] IN (?)', root_id) }
|
|
scope :by_not_in_root_id, ->(root_id) { where('namespaces.traversal_ids[1] NOT IN (?)', root_id) }
|
|
scope :filter_by_path, ->(query) { where('lower(path) = :query', query: query.downcase) }
|
|
scope :in_organization, ->(organization) { where(organization: organization) }
|
|
scope :by_name, ->(name) { where('name LIKE ?', "#{sanitize_sql_like(name)}%") }
|
|
scope :ordered_by_name, -> { order(:name) }
|
|
scope :top_level, -> { by_parent(nil) }
|
|
scope :with_project_statistics, -> { includes(projects: :statistics) }
|
|
scope :with_namespace_details, -> { preload(:namespace_details) }
|
|
|
|
scope :archived, -> { joins(:namespace_settings).where(namespace_settings: { archived: true }) }
|
|
scope :self_or_ancestors_archived, -> { where(self_or_ancestors_archived_setting_subquery.exists) }
|
|
|
|
scope :non_archived, -> { joins(:namespace_settings).where(namespace_settings: { archived: false }) }
|
|
scope :self_and_ancestors_non_archived, -> { where.not(self_or_ancestors_archived_setting_subquery.exists) }
|
|
|
|
scope :with_statistics, -> do
|
|
namespace_statistic_columns = STATISTICS_COLUMNS.map { |column| sum_project_statistics_column(column) }
|
|
subquery = Arel::Table.new(:statistics)
|
|
project_statistics = ProjectStatistics.arel_table
|
|
|
|
statistics = project_statistics
|
|
.project(namespace_statistic_columns)
|
|
.where(project_statistics[:namespace_id].eq(arel_table[:id]))
|
|
.lateral(subquery.name)
|
|
|
|
model.select(arel_table[Arel.star], subquery[Arel.star])
|
|
.from([arel.as(arel_table.name), statistics])
|
|
end
|
|
|
|
scope :with_jira_installation, ->(installation_id) do
|
|
joins(:jira_connect_subscriptions)
|
|
.where(jira_connect_subscriptions: { jira_connect_installation_id: installation_id })
|
|
end
|
|
|
|
scope :sorted_by_similarity_and_parent_id_desc, ->(search) do
|
|
order_expression = Gitlab::Database::SimilarityScore.build_expression(
|
|
search: search,
|
|
rules: [
|
|
{ column: arel_table["path"], multiplier: 1 },
|
|
{ column: arel_table["name"], multiplier: 0.7 }
|
|
])
|
|
reorder(order_expression.desc, Namespace.arel_table['parent_id'].desc.nulls_last, Namespace.arel_table['id'].desc)
|
|
end
|
|
|
|
scope :with_shared_runners_enabled, -> { where(shared_runners_enabled: true) }
|
|
|
|
# Make sure that the name is same as strong_memoize name in root_ancestor
|
|
# method
|
|
attr_writer :root_ancestor, :emails_enabled_memoized
|
|
|
|
class << self
|
|
def sti_class_for(type_name)
|
|
case type_name
|
|
when Group.sti_name
|
|
Group
|
|
when Namespaces::ProjectNamespace.sti_name
|
|
Namespaces::ProjectNamespace
|
|
when Namespaces::UserNamespace.sti_name
|
|
Namespaces::UserNamespace
|
|
else
|
|
Namespace
|
|
end
|
|
end
|
|
|
|
def by_path(path)
|
|
find_by('lower(path) = :value', value: path.downcase)
|
|
end
|
|
|
|
# Case insensitive search for namespace by path or name
|
|
def find_by_path_or_name(path)
|
|
find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
|
|
end
|
|
|
|
def find_top_level
|
|
top_level.take
|
|
end
|
|
|
|
# Searches for namespaces matching the given query.
|
|
#
|
|
# This method uses ILIKE on PostgreSQL.
|
|
#
|
|
# query - The search query as a String.
|
|
#
|
|
# Returns an ActiveRecord::Relation.
|
|
def search(query, include_parents: false, use_minimum_char_limit: true, exact_matches_first: false)
|
|
if include_parents
|
|
route_columns = [Route.arel_table[:path], Route.arel_table[:name]]
|
|
namespaces = without_project_namespaces
|
|
.where(id: Route.for_routable_type(Namespace.name)
|
|
.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
|
|
.fuzzy_search(query, route_columns,
|
|
use_minimum_char_limit: use_minimum_char_limit)
|
|
.select(:source_id))
|
|
|
|
if exact_matches_first
|
|
namespaces = namespaces
|
|
.joins(:route)
|
|
.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
|
|
.order(exact_matches_first_sql(query, route_columns))
|
|
end
|
|
|
|
namespaces
|
|
else
|
|
without_project_namespaces.fuzzy_search(query, [:path, :name], use_minimum_char_limit: use_minimum_char_limit, exact_matches_first: exact_matches_first)
|
|
end
|
|
end
|
|
|
|
# This should be kept in sync with the frontend filtering in
|
|
# https://gitlab.com/gitlab-org/gitlab/-/blob/5d34e3488faa3982d30d7207773991c1e0b6368a/app/assets/javascripts/gfm_auto_complete.js#L68 and
|
|
# https://gitlab.com/gitlab-org/gitlab/-/blob/5d34e3488faa3982d30d7207773991c1e0b6368a/app/assets/javascripts/gfm_auto_complete.js#L1053
|
|
def gfm_autocomplete_search(query)
|
|
namespaces_cte = Gitlab::SQL::CTE.new(table_name, without_order)
|
|
|
|
# This scope does not work with `ProjectNamespace` records because they don't have a corresponding `route` association.
|
|
# We do not chain the `without_project_namespaces` scope because it results in an expensive query plan in certain cases
|
|
unscoped
|
|
.with(namespaces_cte.to_arel)
|
|
.from(namespaces_cte.table)
|
|
.joins(:route)
|
|
.where(
|
|
"REPLACE(routes.name, ' ', '') ILIKE :pattern OR routes.path ILIKE :pattern",
|
|
pattern: "%#{sanitize_sql_like(query)}%"
|
|
)
|
|
.order(
|
|
Arel.sql(sanitize_sql(
|
|
[
|
|
"CASE WHEN REPLACE(routes.name, ' ', '') ILIKE :prefix_pattern OR routes.path ILIKE :prefix_pattern THEN 1 ELSE 2 END",
|
|
{ prefix_pattern: "#{sanitize_sql_like(query)}%" }
|
|
]
|
|
)),
|
|
'routes.path'
|
|
)
|
|
end
|
|
|
|
def clean_path(path, limited_to: Namespace.all)
|
|
slug = Gitlab::Slug::Path.new(path).generate
|
|
path = Namespaces::RandomizedSuffixPath.new(slug)
|
|
Gitlab::Utils::Uniquify.new.string(path) { |s| limited_to.find_by_path_or_name(s) || ProjectSetting.unique_domain_exists?(s) }
|
|
end
|
|
|
|
def reference_prefix
|
|
User.reference_prefix
|
|
end
|
|
|
|
def reference_pattern
|
|
User.reference_pattern
|
|
end
|
|
|
|
def sum_project_statistics_column(column)
|
|
sum = ProjectStatistics.arel_table[column].sum
|
|
|
|
coalesce = Arel::Nodes::NamedFunction.new('COALESCE', [sum, 0])
|
|
coalesce.as(column.to_s)
|
|
end
|
|
|
|
def username_reserved?(username)
|
|
without_project_namespaces.top_level.find_by_path_or_name(username).present?
|
|
end
|
|
|
|
def username_reserved_for_organization?(username, organization, excluding: [])
|
|
without_project_namespaces
|
|
.top_level
|
|
.in_organization(organization)
|
|
.where.not(id: excluding)
|
|
.find_by_path_or_name(username)
|
|
.present?
|
|
end
|
|
|
|
def self_or_ancestors_archived_setting_subquery
|
|
namespace_setting_reflection = reflect_on_association(:namespace_settings)
|
|
namespace_setting_table = Arel::Table.new(namespace_setting_reflection.table_name)
|
|
traversal_ids_ref = "#{arel_table.name}.#{arel_table[:traversal_ids].name}"
|
|
|
|
namespace_setting_table
|
|
.project(1)
|
|
.where(
|
|
namespace_setting_table[namespace_setting_reflection.foreign_key]
|
|
.eq(Arel.sql("ANY (#{traversal_ids_ref})"))
|
|
)
|
|
.where(namespace_setting_table[:archived].eq(true))
|
|
end
|
|
end
|
|
|
|
def archive
|
|
return false if archived?
|
|
|
|
namespace_settings.update(archived: true)
|
|
end
|
|
|
|
def unarchive
|
|
return false unless archived?
|
|
|
|
namespace_settings.update(archived: false)
|
|
end
|
|
|
|
def archived?
|
|
!!namespace_settings&.archived?
|
|
end
|
|
|
|
def self_or_ancestors_archived?
|
|
if association(:namespace_settings_with_ancestors_inherited_settings).loaded?
|
|
return !!namespace_settings_with_ancestors_inherited_settings&.archived
|
|
end
|
|
|
|
self_and_ancestors(skope: Namespace).archived.exists?
|
|
end
|
|
|
|
def ancestors_archived?
|
|
ancestors.archived.exists?
|
|
end
|
|
|
|
def to_reference_base(from = nil, full: false, absolute_path: false)
|
|
if full || cross_namespace_reference?(from)
|
|
absolute_path ? "/#{full_path}" : full_path
|
|
elsif cross_project_reference?(from)
|
|
path
|
|
end
|
|
end
|
|
|
|
def to_reference(*)
|
|
"#{self.class.reference_prefix}#{full_path}"
|
|
end
|
|
|
|
def package_settings
|
|
package_setting_relation || build_package_setting_relation
|
|
end
|
|
|
|
def default_branch_protection
|
|
super || Gitlab::CurrentSettings.default_branch_protection
|
|
end
|
|
|
|
def default_branch_protection_settings
|
|
return Gitlab::CurrentSettings.default_branch_protection_defaults if user_namespace?
|
|
|
|
settings = default_branch_protection_defaults
|
|
|
|
return settings unless settings.blank?
|
|
|
|
Gitlab::CurrentSettings.default_branch_protection_defaults
|
|
end
|
|
|
|
def visibility_level_field
|
|
:visibility_level
|
|
end
|
|
|
|
def to_param
|
|
full_path
|
|
end
|
|
|
|
def human_name
|
|
owner_name || path
|
|
end
|
|
|
|
def any_project_has_container_registry_tags?
|
|
first_project_with_container_registry_tags.present?
|
|
end
|
|
|
|
def first_project_with_container_registry_tags
|
|
if Gitlab.com_except_jh? && ContainerRegistry::GitlabApiClient.supports_gitlab_api?
|
|
ContainerRegistry::GitlabApiClient.one_project_with_container_registry_tag(full_path)
|
|
else
|
|
all_projects.includes(:container_repositories).find(&:has_container_registry_tags?)
|
|
end
|
|
end
|
|
|
|
def send_update_instructions
|
|
projects.each do |project|
|
|
project.send_move_instructions("#{full_path_before_last_save}/#{project.path}")
|
|
end
|
|
end
|
|
|
|
def kind
|
|
return 'group' if group_namespace?
|
|
return 'project' if project_namespace?
|
|
|
|
'user' # defaults to user
|
|
end
|
|
|
|
def group_namespace?
|
|
type == Group.sti_name
|
|
end
|
|
|
|
def project_namespace?
|
|
type == Namespaces::ProjectNamespace.sti_name
|
|
end
|
|
|
|
def user_namespace?
|
|
# That last bit ensures we're considered a user namespace as a default
|
|
type.nil? || type == Namespaces::UserNamespace.sti_name || !(group_namespace? || project_namespace?)
|
|
end
|
|
|
|
def bot_user_namespace?
|
|
return false unless user_namespace?
|
|
return false unless owner && owner.bot?
|
|
|
|
true
|
|
end
|
|
|
|
def owner_required?
|
|
user_namespace?
|
|
end
|
|
|
|
def first_owner
|
|
owner
|
|
end
|
|
|
|
def find_fork_of(project)
|
|
return unless project.fork_network
|
|
|
|
if Gitlab::SafeRequestStore.active?
|
|
forks_in_namespace = Gitlab::SafeRequestStore.fetch("namespaces:#{id}:forked_projects") do
|
|
Hash.new do |found_forks, project|
|
|
found_forks[project] = project.fork_network.find_forks_in(projects).first
|
|
end
|
|
end
|
|
|
|
forks_in_namespace[project]
|
|
else
|
|
project.fork_network.find_forks_in(projects).first
|
|
end
|
|
end
|
|
|
|
# any ancestor can disable emails for all descendants
|
|
def emails_disabled?
|
|
!emails_enabled?
|
|
end
|
|
|
|
def default_branch_protected?
|
|
Gitlab::Access::DefaultBranchProtection.new(default_branch_protection_settings).any?
|
|
end
|
|
|
|
def emails_enabled?
|
|
# If no namespace_settings, we can assume it has not changed from enabled
|
|
return true unless namespace_settings
|
|
|
|
strong_memoize(:emails_enabled_memoized) do
|
|
namespace_settings.emails_enabled?
|
|
end
|
|
end
|
|
|
|
def lfs_enabled?
|
|
# User namespace will always default to the global setting
|
|
Gitlab.config.lfs.enabled
|
|
end
|
|
|
|
def any_project_with_shared_runners_enabled?
|
|
projects.with_shared_runners_enabled.any?
|
|
end
|
|
|
|
def user_ids_for_project_authorizations
|
|
[owner_id]
|
|
end
|
|
|
|
# Includes projects from this namespace and projects from all subgroups
|
|
# that belongs to this namespace
|
|
def all_projects
|
|
namespace = user_namespace? ? self : self_and_descendant_ids
|
|
Project.where(namespace: namespace)
|
|
end
|
|
|
|
def all_catalog_resources
|
|
Ci::Catalog::Resource.where(project: all_projects)
|
|
end
|
|
|
|
def all_projects_except_soft_deleted
|
|
all_projects.not_aimed_for_deletion
|
|
end
|
|
|
|
def has_parent?
|
|
parent_id.present? || parent.present?
|
|
end
|
|
|
|
def subgroup?
|
|
has_parent?
|
|
end
|
|
|
|
# Overridden on EE module
|
|
def multiple_issue_boards_available?
|
|
false
|
|
end
|
|
|
|
def all_project_ids_except(ids)
|
|
all_project_ids.where.not(id: ids)
|
|
end
|
|
|
|
# Deprecated, use #licensed_feature_available? instead. Remove once Namespace#feature_available? isn't used anymore.
|
|
def feature_available?(feature, _user = nil)
|
|
licensed_feature_available?(feature)
|
|
end
|
|
|
|
# Overridden in EE::Namespace
|
|
def licensed_feature_available?(_feature)
|
|
false
|
|
end
|
|
|
|
def full_path_before_last_save
|
|
if parent_id_before_last_save.nil?
|
|
path_before_last_save
|
|
else
|
|
previous_parent = Group.find_by(id: parent_id_before_last_save)
|
|
"#{previous_parent.full_path}/#{path_before_last_save}"
|
|
end
|
|
end
|
|
|
|
def refresh_project_authorizations
|
|
owner.refresh_authorized_projects
|
|
end
|
|
|
|
def auto_devops_enabled?
|
|
first_auto_devops_config[:status]
|
|
end
|
|
|
|
def first_auto_devops_config
|
|
return { scope: :group, status: auto_devops_enabled } unless auto_devops_enabled.nil?
|
|
|
|
strong_memoize(:first_auto_devops_config) do
|
|
if parent.present?
|
|
Rails.cache.fetch(first_auto_devops_config_cache_key_for(id), expires_in: 1.day) do
|
|
parent.first_auto_devops_config
|
|
end
|
|
else
|
|
{ scope: :instance, status: Gitlab::CurrentSettings.auto_devops_enabled? }
|
|
end
|
|
end
|
|
end
|
|
|
|
def aggregation_scheduled?
|
|
aggregation_schedule.present?
|
|
end
|
|
|
|
def container_repositories_size_cache_key
|
|
"namespaces:#{id}:container_repositories_size"
|
|
end
|
|
|
|
def container_repositories_size
|
|
strong_memoize(:container_repositories_size) do
|
|
next unless root?
|
|
next unless ContainerRegistry::GitlabApiClient.supports_gitlab_api?
|
|
next 0 if all_container_repositories.empty?
|
|
|
|
Rails.cache.fetch(container_repositories_size_cache_key, expires_in: 7.days) do
|
|
ContainerRegistry::GitlabApiClient.deduplicated_size(full_path)
|
|
end
|
|
end
|
|
end
|
|
|
|
def all_container_repositories
|
|
ContainerRepository.for_project_id(all_projects)
|
|
end
|
|
|
|
def any_project_with_pages_deployed?
|
|
all_projects.with_pages_deployed.any?
|
|
end
|
|
|
|
def closest_setting(name)
|
|
self_and_ancestors(hierarchy_order: :asc)
|
|
.find { |n| !n.read_attribute(name).nil? }
|
|
.try(name)
|
|
end
|
|
|
|
def actual_plan
|
|
Plan.default
|
|
end
|
|
|
|
def paid?
|
|
root? && actual_plan.paid?
|
|
end
|
|
|
|
def linked_to_subscription?
|
|
paid?
|
|
end
|
|
|
|
def actual_limits
|
|
# We default to PlanLimits.new otherwise a lot of specs would fail
|
|
# On production each plan should already have associated limits record
|
|
# https://gitlab.com/gitlab-org/gitlab/issues/36037
|
|
actual_plan.actual_limits
|
|
end
|
|
|
|
def actual_plan_name
|
|
actual_plan.name
|
|
end
|
|
|
|
def changing_shared_runners_enabled_is_allowed
|
|
return unless new_record? || changes.has_key?(:shared_runners_enabled)
|
|
|
|
if shared_runners_enabled && has_parent? && parent.shared_runners_setting == SR_DISABLED_AND_UNOVERRIDABLE
|
|
errors.add(:shared_runners_enabled, _('cannot be enabled because parent group has shared Runners disabled'))
|
|
end
|
|
end
|
|
|
|
def changing_allow_descendants_override_disabled_shared_runners_is_allowed
|
|
return unless new_record? || changes.has_key?(:allow_descendants_override_disabled_shared_runners)
|
|
|
|
if shared_runners_enabled && allow_descendants_override_disabled_shared_runners
|
|
errors.add(:allow_descendants_override_disabled_shared_runners, _('can not be true if shared runners are enabled'))
|
|
end
|
|
|
|
if allow_descendants_override_disabled_shared_runners && has_parent? && parent.shared_runners_setting == SR_DISABLED_AND_UNOVERRIDABLE
|
|
errors.add(:allow_descendants_override_disabled_shared_runners, _('cannot be enabled because parent group does not allow it'))
|
|
end
|
|
end
|
|
|
|
def shared_runners_setting
|
|
if shared_runners_enabled
|
|
SR_ENABLED
|
|
elsif allow_descendants_override_disabled_shared_runners
|
|
SR_DISABLED_AND_OVERRIDABLE
|
|
else
|
|
SR_DISABLED_AND_UNOVERRIDABLE
|
|
end
|
|
end
|
|
|
|
def shared_runners_setting_higher_than?(other_setting)
|
|
case other_setting
|
|
when SR_ENABLED
|
|
false
|
|
when SR_DISABLED_AND_OVERRIDABLE
|
|
shared_runners_setting == SR_ENABLED
|
|
when SR_DISABLED_AND_UNOVERRIDABLE
|
|
shared_runners_setting == SR_ENABLED || shared_runners_setting == SR_DISABLED_AND_OVERRIDABLE
|
|
else
|
|
raise ArgumentError
|
|
end
|
|
end
|
|
|
|
def shared_runners
|
|
@shared_runners ||= shared_runners_enabled ? Ci::Runner.instance_type : Ci::Runner.none
|
|
end
|
|
|
|
def root?
|
|
!has_parent?
|
|
end
|
|
|
|
def recent?
|
|
created_at >= 90.days.ago
|
|
end
|
|
|
|
def issue_repositioning_disabled?
|
|
Feature.enabled?(:block_issue_repositioning, self, type: :ops)
|
|
end
|
|
|
|
def certificate_based_clusters_enabled?
|
|
cluster_enabled_granted? || certificate_based_clusters_enabled_ff?
|
|
end
|
|
|
|
def enabled_git_access_protocol
|
|
# If the instance-level setting is enabled, we defer to that
|
|
return ::Gitlab::CurrentSettings.enabled_git_access_protocol unless ::Gitlab::CurrentSettings.enabled_git_access_protocol.blank?
|
|
|
|
# Otherwise we use the stored setting on the group
|
|
namespace_settings&.enabled_git_access_protocol
|
|
end
|
|
|
|
def all_ancestors_have_runner_registration_enabled?
|
|
namespace_settings&.all_ancestors_have_runner_registration_enabled?
|
|
end
|
|
|
|
def allow_runner_registration_token?
|
|
!!namespace_settings&.allow_runner_registration_token?
|
|
end
|
|
|
|
def pages_access_control_trie(namespaces = self_and_descendants)
|
|
strong_memoize_with(:pages_access_control_trie, namespaces) do
|
|
traversal_ids = namespaces.joins(:namespace_settings).where(namespace_settings: { force_pages_access_control: true }).map(&:traversal_ids)
|
|
|
|
Namespaces::Traversal::TrieNode.build(traversal_ids)
|
|
end
|
|
end
|
|
|
|
def pages_access_control_forced_by_self_or_ancestor?
|
|
pages_access_control_trie(self_and_ancestors)&.covered?(traversal_ids)
|
|
end
|
|
|
|
def pages_access_control_forced_by_ancestor?
|
|
pages_access_control_trie(ancestors)&.covered?(traversal_ids)
|
|
end
|
|
|
|
def all_projects_with_pages
|
|
all_projects.with_pages_deployed.includes(
|
|
:route,
|
|
:project_setting,
|
|
:project_feature,
|
|
:active_pages_deployments,
|
|
:namespace
|
|
)
|
|
end
|
|
|
|
def web_url(only_path: nil)
|
|
Gitlab::UrlBuilder.build(self, only_path: only_path)
|
|
end
|
|
|
|
# Overriding of Namespaces::AdjournedDeletable method
|
|
override :self_deletion_in_progress?
|
|
def self_deletion_in_progress?
|
|
!!deleted_at
|
|
end
|
|
|
|
def uploads_sharding_key
|
|
{ namespace_id: id }
|
|
end
|
|
|
|
def pipeline_variables_default_role
|
|
return namespace_settings.pipeline_variables_default_role if namespace_settings.present?
|
|
|
|
# We could have old namespaces that don't have an associated `namespace_settings` record.
|
|
# To avoid returning `nil` we return the database-level default.
|
|
NamespaceSetting.column_defaults['pipeline_variables_default_role']
|
|
end
|
|
|
|
def traversal_ids_as_sql
|
|
traversal_ids.join(',')
|
|
end
|
|
|
|
def namespace_details
|
|
super.presence || build_namespace_details
|
|
end
|
|
|
|
private
|
|
|
|
def parent_organization_match
|
|
return unless parent
|
|
return if parent.organization_id == organization_id
|
|
|
|
errors.add(:organization_id, _("must match the parent organization's ID"))
|
|
end
|
|
|
|
# route / path global uniqueness is handled by Routeable concern
|
|
# here we are checking only for conflicts with per-organization username aliases
|
|
def no_conflict_with_organization_user_details
|
|
return unless Organizations::OrganizationUserDetail.for_organization(organization).with_usernames(path).any?
|
|
|
|
errors.add(:path, _('has already been taken'))
|
|
end
|
|
|
|
def cross_namespace_reference?(from)
|
|
return false if from == self
|
|
|
|
comparable_namespace_id = project_namespace? ? parent_id : id
|
|
|
|
case from
|
|
when Project
|
|
from.namespace_id != comparable_namespace_id
|
|
when Namespaces::ProjectNamespace
|
|
from.parent_id != comparable_namespace_id
|
|
when Namespace
|
|
is_a?(Group) ? from.id != id : parent != from
|
|
when User
|
|
true
|
|
end
|
|
end
|
|
|
|
# Check if a reference is being done cross-project
|
|
def cross_project_reference?(from)
|
|
case from
|
|
when Project
|
|
from.project_namespace_id != id
|
|
else
|
|
from && self != from
|
|
end
|
|
end
|
|
|
|
def cluster_enabled_granted?
|
|
(Gitlab.com? || Gitlab.dev_or_test_env?) && root_ancestor.cluster_enabled_grant.present?
|
|
end
|
|
|
|
def certificate_based_clusters_enabled_ff?
|
|
Feature.enabled?(:certificate_based_clusters, type: :ops)
|
|
end
|
|
|
|
def expire_child_caches
|
|
Namespace.where(id: descendants).each_batch do |namespaces|
|
|
namespaces.touch_all
|
|
end
|
|
|
|
all_projects.each_batch do |projects|
|
|
projects.touch_all
|
|
end
|
|
end
|
|
|
|
def parent_changed?
|
|
parent_id_changed?
|
|
end
|
|
|
|
def saved_change_to_parent?
|
|
saved_change_to_parent_id?
|
|
end
|
|
|
|
def saved_change_to_path_or_parent?
|
|
saved_change_to_path? || saved_change_to_parent_id?
|
|
end
|
|
|
|
def refresh_access_of_projects_invited_groups
|
|
Project
|
|
.where(namespace_id: id)
|
|
.joins(:project_group_links)
|
|
.distinct
|
|
.find_each do |project|
|
|
AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id)
|
|
end
|
|
|
|
# Until we compare the inconsistency rates of the new specialized worker and
|
|
# the old approach, we still run AuthorizedProjectsWorker
|
|
# but with some delay and lower urgency as a safety net.
|
|
enqueue_jobs_for_groups_requiring_authorizations_refresh(priority: UserProjectAccessChangedService::LOW_PRIORITY)
|
|
end
|
|
|
|
def enqueue_jobs_for_groups_requiring_authorizations_refresh(priority:)
|
|
groups_requiring_authorizations_refresh = Group
|
|
.joins(project_group_links: :project)
|
|
.where(projects: { namespace_id: id })
|
|
.distinct
|
|
|
|
groups_requiring_authorizations_refresh.find_each do |group|
|
|
group.refresh_members_authorized_projects(
|
|
priority: priority
|
|
)
|
|
end
|
|
end
|
|
|
|
def nesting_level_allowed
|
|
if ancestors.count > Group::NUMBER_OF_ANCESTORS_ALLOWED
|
|
errors.add(:parent_id, _('has too deep level of nesting'))
|
|
end
|
|
end
|
|
|
|
def validate_parent_type
|
|
unless has_parent?
|
|
if project_namespace?
|
|
errors.add(:parent_id, _('must be set for a project namespace'))
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
if parent&.project_namespace?
|
|
errors.add(:parent_id, _('project namespace cannot be the parent of another namespace'))
|
|
end
|
|
|
|
if user_namespace?
|
|
errors.add(:parent_id, _('cannot be used for user namespace'))
|
|
elsif group_namespace?
|
|
errors.add(:parent_id, _('user namespace cannot be the parent of another namespace')) if parent.user_namespace?
|
|
end
|
|
end
|
|
|
|
def save_namespace_details_changes
|
|
attribute_names_to_sync = Namespace::Detail.attribute_names - ['namespace_id']
|
|
attributes_to_sync = namespace_details.changes.slice(*attribute_names_to_sync)
|
|
.transform_values { |val| val[1] }
|
|
|
|
self.namespace_details = Namespace::Detail.find_by_namespace_id(id) || build_namespace_details
|
|
namespace_details.assign_attributes(attributes_to_sync)
|
|
namespace_details.save!
|
|
end
|
|
|
|
def sync_share_with_group_lock_with_parent
|
|
if parent&.share_with_group_lock?
|
|
self.share_with_group_lock = true
|
|
end
|
|
end
|
|
|
|
def force_share_with_group_lock_on_descendants
|
|
# We can't use `descendants.update_all` since Rails will throw away the WITH
|
|
# RECURSIVE statement. We also can't use WHERE EXISTS since we can't use
|
|
# different table aliases, hence we're just using WHERE IN. Since we have a
|
|
# maximum of 20 nested groups this should be fine.
|
|
Namespace.where(id: descendants.select(:id))
|
|
.update_all(share_with_group_lock: true)
|
|
end
|
|
|
|
def expire_first_auto_devops_config_cache
|
|
descendants_to_expire = self_and_descendants.as_ids
|
|
return if descendants_to_expire.load.empty?
|
|
|
|
keys = descendants_to_expire.map { |group| first_auto_devops_config_cache_key_for(group.id) }
|
|
Rails.cache.delete_multi(keys)
|
|
end
|
|
|
|
def enforce_minimum_path_length?
|
|
path_changed? && !project_namespace?
|
|
end
|
|
|
|
# SyncEvents are created by PG triggers (with the function `insert_namespaces_sync_event`)
|
|
def schedule_sync_event_worker
|
|
run_after_commit do
|
|
Namespaces::SyncEvent.enqueue_worker
|
|
end
|
|
end
|
|
|
|
def first_auto_devops_config_cache_key_for(group_id)
|
|
# Use SHA2 of `traversal_ids` to account for moving a namespace within the same root ancestor hierarchy.
|
|
"namespaces:{#{traversal_ids.first}}:first_auto_devops_config:#{group_id}:#{Digest::SHA2.hexdigest(traversal_ids.join(' '))}"
|
|
end
|
|
end
|
|
|
|
Namespace.prepend_mod_with('Namespace')
|