namespace.rb 18.6 KB
Newer Older
1 2
# frozen_string_literal: true

Mark Chao's avatar
Mark Chao committed
3
class Namespace < ApplicationRecord
4
  include CacheMarkdownField
5
  include Sortable
6
  include Gitlab::VisibilityLevel
7
  include Routable
8
  include AfterCommitQueue
9
  include Storage::LegacyNamespace
10
  include Gitlab::SQL::Pattern
11
  include FeatureGate
12
  include FromUnion
13
  include Gitlab::Utils::StrongMemoize
14
  include IgnorableColumns
15
  include Namespaces::Traversal::Recursive
16
  include Namespaces::Traversal::Linear
17
  include EachBatch
18

19 20
  ignore_column :delayed_project_removal, remove_with: '14.1', remove_after: '2021-05-22'

21 22 23 24 25
  # Tells ActiveRecord not to store the full class name, in order to space some space
  # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69794
  self.store_full_sti_class = false
  self.store_full_class_name = false

26 27 28 29 30
  # 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

31
  SHARED_RUNNERS_SETTINGS = %w[disabled_and_unoverridable disabled_with_override enabled].freeze
32
  URL_MAX_LENGTH = 255
33

34 35
  cache_markdown_field :description, pipeline: :description

36
  has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
37
  has_many :project_statistics
38
  has_one :namespace_settings, inverse_of: :namespace, class_name: 'NamespaceSetting', autosave: true
39

40
  has_many :runner_namespaces, inverse_of: :namespace, class_name: 'Ci::RunnerNamespace'
41
  has_many :runners, through: :runner_namespaces, source: :runner, class_name: 'Ci::Runner'
42
  has_many :pending_builds, class_name: 'Ci::PendingBuild'
43
  has_one :onboarding_progress
44

45 46
  # This should _not_ be `inverse_of: :namespace`, because that would also set
  # `user.namespace` when this user creates a group with themselves as `owner`.
47 48
  belongs_to :owner, class_name: "User"

49 50
  belongs_to :parent, class_name: "Namespace"
  has_many :children, class_name: "Namespace", foreign_key: :parent_id
51
  has_many :custom_emoji, inverse_of: :namespace
52
  has_one :chat_team, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
53 54
  has_one :root_storage_statistics, class_name: 'Namespace::RootStorageStatistics'
  has_one :aggregation_schedule, class_name: 'Namespace::AggregationSchedule'
55
  has_one :package_setting_relation, inverse_of: :namespace, class_name: 'PackageSetting'
56

57 58 59
  has_one :admin_note, inverse_of: :namespace
  accepts_nested_attributes_for :admin_note, update_only: true

60
  validates :owner, presence: true, if: ->(n) { n.owner_required? }
61
  validates :name,
62
    presence: true,
63
    length: { maximum: 255 }
64

65
  validates :description, length: { maximum: 255 }
66

67
  validates :path,
68
    presence: true,
69 70 71 72 73 74 75
    length: { maximum: URL_MAX_LENGTH }

  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? }
76

77 78 79 80 81
  # 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
82 83 84 85 86
  #
  # 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?
87

88 89
  validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true }

90
  validate :validate_parent_type, if: -> { Feature.enabled?(:validate_namespace_parent_type, default_enabled: :yaml) }
91
  validate :nesting_level_allowed
92 93
  validate :changing_shared_runners_enabled_is_allowed
  validate :changing_allow_descendants_override_disabled_shared_runners_is_allowed
94

95
  delegate :name, to: :owner, allow_nil: true, prefix: true
96
  delegate :avatar_url, to: :owner, allow_nil: true
97

98
  after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
99

100 101
  before_create :sync_share_with_group_lock_with_parent
  before_update :sync_share_with_group_lock_with_parent, if: :parent_changed?
Jasper Maes's avatar
Jasper Maes committed
102
  after_update :force_share_with_group_lock_on_descendants, if: -> { saved_change_to_share_with_group_lock? && share_with_group_lock? }
103

104 105
  # Legacy Storage specific hooks

106
  after_update :move_dir, if: :saved_change_to_path_or_parent?
107
  before_destroy(prepend: true) { prepare_for_destroy }
108
  after_destroy :rm_dir
109 110 111 112
  after_commit :expire_child_caches, on: :update, if: -> {
    Feature.enabled?(:cached_route_lookups, self, type: :ops, default_enabled: :yaml) &&
      saved_change_to_name? || saved_change_to_path? || saved_change_to_parent_id?
  }
113

Brett Walker's avatar
Brett Walker committed
114 115
  # TODO: change to `type: Namespaces::UserNamespace.sti_name` when
  #       working on issue https://gitlab.com/gitlab-org/gitlab/-/issues/341070
116
  scope :user_namespaces, -> { where(type: [nil, Namespaces::UserNamespace.sti_name]) }
117
  scope :sort_by_type, -> { order(Gitlab::Database.nulls_first_order(:type)) }
118
  scope :include_route, -> { includes(:route) }
119 120
  scope :by_parent, -> (parent) { where(parent_id: parent) }
  scope :filter_by_path, -> (query) { where('lower(path) = :query', query: query.downcase) }
121

122 123 124 125 126 127 128
  scope :with_statistics, -> do
    joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id')
      .group('namespaces.id')
      .select(
        'namespaces.*',
        'COALESCE(SUM(ps.storage_size), 0) AS storage_size',
        'COALESCE(SUM(ps.repository_size), 0) AS repository_size',
129
        'COALESCE(SUM(ps.wiki_size), 0) AS wiki_size',
130
        'COALESCE(SUM(ps.snippets_size), 0) AS snippets_size',
131
        'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
132
        'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size',
133 134
        'COALESCE(SUM(ps.packages_size), 0) AS packages_size',
        'COALESCE(SUM(ps.uploads_size), 0) AS uploads_size'
135 136 137
      )
  end

138 139 140 141 142 143 144 145
  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

146 147
  # Make sure that the name is same as strong_memoize name in root_ancestor
  # method
148
  attr_writer :root_ancestor, :emails_disabled_memoized
149

150
  class << self
151 152
    def sti_class_for(type_name)
      case type_name
153
      when Group.sti_name
154
        Group
155
      when Namespaces::ProjectNamespace.sti_name
156
        Namespaces::ProjectNamespace
157
      when Namespaces::UserNamespace.sti_name
158
        Namespaces::UserNamespace
159 160 161 162 163
      else
        Namespace
      end
    end

164
    def by_path(path)
Gabriel Mazetto's avatar
Gabriel Mazetto committed
165
      find_by('lower(path) = :value', value: path.downcase)
166 167
    end

168
    # Case insensitive search for namespace by path or name
169 170 171 172
    def find_by_path_or_name(path)
      find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
    end

173 174
    # Searches for namespaces matching the given query.
    #
175
    # This method uses ILIKE on PostgreSQL.
176
    #
177
    # query - The search query as a String.
178
    #
179
    # Returns an ActiveRecord::Relation.
180 181
    def search(query, include_parents: false)
      if include_parents
182
        where(id: Route.for_routable_type(Namespace.name).fuzzy_search(query, [Route.arel_table[:path], Route.arel_table[:name]]).select(:source_id))
183 184 185
      else
        fuzzy_search(query, [:path, :name])
      end
186 187 188
    end

    def clean_path(path)
189
      path = path.dup
190
      # Get the email username by removing everything after an `@` sign.
191
      path.gsub!(/@.*\z/,                "")
192
      # Remove everything that's not in the list of allowed characters.
193 194 195 196 197
      path.gsub!(/[^a-zA-Z0-9_\-\.]/,    "")
      # Remove trailing violations ('.atom', '.git', or '.')
      path.gsub!(/(\.atom|\.git|\.)*\z/, "")
      # Remove leading violations ('-')
      path.gsub!(/\A\-+/,                "")
198

199
      # Users with the great usernames of "." or ".." would end up with a blank username.
200
      # Work around that by setting their username to "blank", followed by a counter.
201 202
      path = "blank" if path.blank?

203
      uniquify = Uniquify.new
204
      uniquify.string(path) { |s| Namespace.find_by_path_or_name(s) }
205
    end
206

207 208 209 210
    def clean_name(value)
      value.scan(Gitlab::Regex.group_name_regex_chars).join(' ')
    end

211 212
    def find_by_pages_host(host)
      gitlab_host = "." + Settings.pages.host.downcase
213 214
      host = host.downcase
      return unless host.ends_with?(gitlab_host)
215

216
      name = host.delete_suffix(gitlab_host)
217
      Namespace.where(parent_id: nil).by_path(name)
218
    end
219 220 221 222

    def top_most
      where(parent_id: nil)
    end
223 224
  end

225 226 227 228
  def package_settings
    package_setting_relation || build_package_setting_relation
  end

229 230 231 232
  def default_branch_protection
    super || Gitlab::CurrentSettings.default_branch_protection
  end

233 234 235 236
  def visibility_level_field
    :visibility_level
  end

237
  def to_param
238
    full_path
239
  end
240 241 242 243

  def human_name
    owner_name
  end
244

245
  def any_project_has_container_registry_tags?
246
    all_projects.includes(:container_repositories).any?(&:has_container_registry_tags?)
247 248 249 250
  end

  def first_project_with_container_registry_tags
    all_projects.find(&:has_container_registry_tags?)
251 252
  end

253
  def send_update_instructions
254
    projects.each do |project|
255
      project.send_move_instructions("#{full_path_before_last_save}/#{project.path}")
256
    end
257
  end
258 259

  def kind
260
    return 'group' if group_namespace?
261
    return 'project' if project_namespace?
262

263
    'user' # defaults to user
264 265
  end

266
  def group_namespace?
267 268 269
    type == Group.sti_name
  end

270
  def project_namespace?
271 272 273
    type == Namespaces::ProjectNamespace.sti_name
  end

274
  def user_namespace?
275
    # That last bit ensures we're considered a user namespace as a default
276
    type.nil? || type == Namespaces::UserNamespace.sti_name || !(group_namespace? || project_namespace?)
277 278
  end

279
  def owner_required?
280
    user_namespace?
281 282
  end

283
  def find_fork_of(project)
284
    return unless project.fork_network
285

286 287
    if Gitlab::SafeRequestStore.active?
      forks_in_namespace = Gitlab::SafeRequestStore.fetch("namespaces:#{id}:forked_projects") do
288 289 290 291 292 293 294 295 296
        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
297
  end
298

299 300
  # any ancestor can disable emails for all descendants
  def emails_disabled?
301
    strong_memoize(:emails_disabled_memoized) do
302 303 304 305 306
      if parent_id
        self_and_ancestors.where(emails_disabled: true).exists?
      else
        !!emails_disabled
      end
307 308 309
    end
  end

310 311 312 313 314
  def lfs_enabled?
    # User namespace will always default to the global setting
    Gitlab.config.lfs.enabled
  end

315
  def any_project_with_shared_runners_enabled?
316 317 318
    projects.with_shared_runners.any?
  end

319 320 321 322
  def user_ids_for_project_authorizations
    [owner_id]
  end

323 324 325
  # Includes projects from this namespace and projects from all subgroups
  # that belongs to this namespace
  def all_projects
326
    if Feature.enabled?(:recursive_approach_for_all_projects, default_enabled: :yaml)
327
      namespace = user_namespace? ? self : self_and_descendant_ids
328 329 330 331
      Project.where(namespace: namespace)
    else
      Project.inside_path(full_path)
    end
332 333
  end

334
  def has_parent?
335
    parent_id.present? || parent.present?
336 337
  end

338 339 340 341
  def subgroup?
    has_parent?
  end

342
  # Overridden on EE module
343 344
  def multiple_issue_boards_available?
    false
345
  end
346

347 348 349 350 351
  # Deprecated, use #licensed_feature_available? instead. Remove once Namespace#feature_available? isn't used anymore.
  def feature_available?(feature)
    licensed_feature_available?(feature)
  end

352
  # Overridden in EE::Namespace
353
  def licensed_feature_available?(_feature)
354 355 356
    false
  end

357 358 359
  def full_path_before_last_save
    if parent_id_before_last_save.nil?
      path_before_last_save
360
    else
361 362
      previous_parent = Group.find_by(id: parent_id_before_last_save)
      previous_parent.full_path + '/' + path_before_last_save
363
    end
364 365
  end

366 367 368 369
  def refresh_project_authorizations
    owner.refresh_authorized_projects
  end

370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385
  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 has_parent?
        parent.first_auto_devops_config
      else
        { scope: :instance, status: Gitlab::CurrentSettings.auto_devops_enabled? }
      end
    end
  end

386 387 388 389
  def aggregation_scheduled?
    aggregation_schedule.present?
  end

390
  def pages_virtual_domain
391
    Pages::VirtualDomain.new(
392
      all_projects_with_pages.includes(:route, :project_feature, pages_metadatum: :pages_deployment),
393 394
      trim_prefix: full_path
    )
395 396
  end

397 398 399 400
  def any_project_with_pages_deployed?
    all_projects.with_pages_deployed.any?
  end

401 402 403 404 405 406
  def closest_setting(name)
    self_and_ancestors(hierarchy_order: :asc)
      .find { |n| !n.read_attribute(name).nil? }
      .try(name)
  end

407 408 409 410
  def actual_plan
    Plan.default
  end

411 412 413 414
  def paid?
    root? && actual_plan.paid?
  end

415 416 417 418
  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
419
    actual_plan.actual_limits
420 421 422 423 424 425
  end

  def actual_plan_name
    actual_plan.name
  end

426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469
  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 == '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 && !new_record?
      errors.add(:allow_descendants_override_disabled_shared_runners, _('cannot be changed if shared runners are enabled'))
    end

    if allow_descendants_override_disabled_shared_runners && has_parent? && parent.shared_runners_setting == '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
      'enabled'
    else
      if allow_descendants_override_disabled_shared_runners
        'disabled_with_override'
      else
        'disabled_and_unoverridable'
      end
    end
  end

  def shared_runners_setting_higher_than?(other_setting)
    if other_setting == 'enabled'
      false
    elsif other_setting == 'disabled_with_override'
      shared_runners_setting == 'enabled'
    elsif other_setting == 'disabled_and_unoverridable'
      shared_runners_setting == 'enabled' || shared_runners_setting == 'disabled_with_override'
    else
      raise ArgumentError
    end
  end

470 471 472 473
  def root?
    !has_parent?
  end

474 475 476 477
  def recent?
    created_at >= 90.days.ago
  end

478 479 480 481
  def issue_repositioning_disabled?
    Feature.enabled?(:block_issue_repositioning, self, type: :ops, default_enabled: :yaml)
  end

482 483
  private

484 485 486 487 488 489 490 491 492 493
  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

494 495 496 497
  def all_projects_with_pages
    all_projects.with_pages_deployed
  end

498 499 500 501 502 503 504 505 506
  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?
Jasper Maes's avatar
Jasper Maes committed
507
    saved_change_to_path? || saved_change_to_parent_id?
508 509
  end

510
  def refresh_access_of_projects_invited_groups
511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538
    if Feature.enabled?(:specialized_worker_for_group_lock_update_auth_recalculation)
      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.
      Group
        .joins(project_group_links: :project)
        .where(projects: { namespace_id: id })
        .distinct
        .find_each do |group|
        group.refresh_members_authorized_projects(
          blocking: false,
          priority: UserProjectAccessChangedService::LOW_PRIORITY
        )
      end
    else
      Group
        .joins(project_group_links: :project)
        .where(projects: { namespace_id: id })
        .find_each(&:refresh_members_authorized_projects)
    end
539
  end
540

541 542
  def nesting_level_allowed
    if ancestors.count > Group::NUMBER_OF_ANCESTORS_ALLOWED
543
      errors.add(:parent_id, _('has too deep level of nesting'))
544 545
    end
  end
546

547
  def validate_parent_type
548
    unless has_parent?
549
      if project_namespace?
550 551 552 553 554 555
        errors.add(:parent_id, _('must be set for a project namespace'))
      end

      return
    end

556
    if parent.project_namespace?
557 558
      errors.add(:parent_id, _('project namespace cannot be the parent of another namespace'))
    end
559

560
    if user_namespace?
561
      errors.add(:parent_id, _('cannot not be used for user namespace'))
562
    elsif group_namespace?
563
      errors.add(:parent_id, _('user namespace cannot be the parent of another namespace')) if parent.user_namespace?
564 565 566
    end
  end

567 568 569 570 571
  def sync_share_with_group_lock_with_parent
    if parent&.share_with_group_lock?
      self.share_with_group_lock = true
    end
  end
572

573
  def force_share_with_group_lock_on_descendants
574 575 576 577 578 579
    # 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)
580
  end
581

582 583
  def write_projects_repository_config
    all_projects.find_each do |project|
584
      project.set_full_path
585
      project.track_project_repository
586 587
    end
  end
588 589 590 591

  def enforce_minimum_path_length?
    path_changed? && !project_namespace?
  end
592
end
593

594
Namespace.prepend_mod_with('Namespace')