group.rb 12.3 KB
Newer Older
1 2
# frozen_string_literal: true

Steven Thonus's avatar
Steven Thonus committed
3 4
require 'carrierwave/orm/activerecord'

5
class Group < Namespace
6
  include Gitlab::ConfigHelper
7
  include AfterCommitQueue
8
  include AccessRequestable
9
  include Avatarable
10
  include Referable
11
  include SelectForProjectAuthorization
12
  include LoadedInGroupList
13
  include Descendant
14
  include GroupDescendant
15
  include TokenAuthenticatable
Jan Provaznik's avatar
Jan Provaznik committed
16
  include WithUploads
17
  include Gitlab::Utils::StrongMemoize
18

19
  has_many :group_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
20
  alias_method :members, :group_members
21
  has_many :users, through: :group_members
22
  has_many :owners,
23
    -> { where(members: { access_level: Gitlab::Access::OWNER }) },
24 25 26
    through: :group_members,
    source: :user

27
  has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
28
  has_many :members_and_requesters, as: :source, class_name: 'GroupMember'
29

Felipe Artur's avatar
Felipe Artur committed
30
  has_many :milestones
31
  has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
32
  has_many :shared_projects, through: :project_group_links, source: :project
33 34 35

  # Overridden on another method
  # Left here just to be dependent: :destroy
36
  has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
37

38
  has_many :labels, class_name: 'GroupLabel'
Shinya Maeda's avatar
Shinya Maeda committed
39
  has_many :variables, class_name: 'Ci::GroupVariable'
40
  has_many :custom_attributes, class_name: 'GroupCustomAttribute'
Andrey Kumanyaev's avatar
Andrey Kumanyaev committed
41

Felipe Artur's avatar
Felipe Artur committed
42
  has_many :boards
43
  has_many :badges, class_name: 'GroupBadge'
Felipe Artur's avatar
Felipe Artur committed
44

45 46 47
  has_many :cluster_groups, class_name: 'Clusters::Group'
  has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster'

48 49
  has_many :todos

50 51
  accepts_nested_attributes_for :variables, allow_destroy: true

52
  validate :visibility_level_allowed_by_projects
53
  validate :visibility_level_allowed_by_sub_groups
54
  validate :visibility_level_allowed_by_parent
55
  validates :variables, variable_duplicates: true
56

57 58
  validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }

59
  add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
60

61 62
  after_create :post_create_hook
  after_destroy :post_destroy_hook
63
  after_save :update_two_factor_requirement
64
  after_update :path_changed_hook, if: :path_changed?
65

66
  class << self
67
    def sort_by_attribute(method)
Markus Koller's avatar
Markus Koller committed
68 69 70 71 72 73 74
      if method == 'storage_size_desc'
        # storage_size is a virtual column so we need to
        # pass a string to avoid AR adding the table name
        reorder('storage_size DESC, namespaces.id DESC')
      else
        order_by(method)
      end
75
    end
76 77

    def reference_prefix
78 79 80 81 82
      User.reference_prefix
    end

    def reference_pattern
      User.reference_pattern
83
    end
84

85 86 87 88 89 90 91 92 93 94 95
    # WARNING: This method should never be used on its own
    # please do make sure the number of rows you are filtering is small
    # enough for this query
    def public_or_visible_to_user(user)
      return public_to_user unless user

      public_for_user = public_to_user_arel(user)
      visible_for_user = visible_to_user_arel(user)
      public_or_visible = public_for_user.or(visible_for_user)

      where(public_or_visible)
96
    end
97 98 99

    def select_for_project_authorization
      if current_scope.joins_values.include?(:shared_projects)
100
        joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
101
          .where('project_namespace.share_with_group_lock = ?', false)
102
          .select("projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level")
103 104 105 106
      else
        super
      end
    end
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123

    private

    def public_to_user_arel(user)
      self.arel_table[:visibility_level]
        .in(Gitlab::VisibilityLevel.levels_for_user(user))
    end

    def visible_to_user_arel(user)
      groups_table = self.arel_table
      authorized_groups = user.authorized_groups.as('authorized')

      groups_table.project(1)
        .from(authorized_groups)
        .where(authorized_groups[:id].eq(groups_table[:id]))
        .exists
    end
124 125
  end

126 127 128 129 130 131 132 133 134
  # Overrides notification_settings has_many association
  # This allows to apply notification settings from parent groups
  # to child groups and projects.
  def notification_settings
    source_type = self.class.base_class.name

    NotificationSetting.where(source_type: source_type, source_id: self_and_ancestors_ids)
  end

135
  def to_reference(_from = nil, full: nil)
136
    "#{self.class.reference_prefix}#{full_path}"
137 138
  end

139
  def web_url
140
    Gitlab::Routing.url_helpers.group_canonical_url(self)
141 142
  end

143
  def human_name
144
    full_name
145
  end
146

147
  def visibility_level_allowed_by_parent?(level = self.visibility_level)
Rubén Dávila's avatar
Rubén Dávila committed
148
    return true unless parent_id && parent_id.nonzero?
149

150 151
    level <= parent.visibility_level
  end
152

153
  def visibility_level_allowed_by_projects?(level = self.visibility_level)
154
    !projects.where('visibility_level > ?', level).exists?
155
  end
156

157
  def visibility_level_allowed_by_sub_groups?(level = self.visibility_level)
158
    !children.where('visibility_level > ?', level).exists?
159 160
  end

161 162 163 164
  def visibility_level_allowed?(level = self.visibility_level)
    visibility_level_allowed_by_parent?(level) &&
      visibility_level_allowed_by_projects?(level) &&
      visibility_level_allowed_by_sub_groups?(level)
165 166
  end

167 168 169 170 171 172 173
  def lfs_enabled?
    return false unless Gitlab.config.lfs.enabled
    return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil?

    self[:lfs_enabled]
  end

174 175 176 177
  def owned_by?(user)
    owners.include?(user)
  end

178
  def add_users(users, access_level, current_user: nil, expires_at: nil)
179
    GroupMember.add_users(
180 181 182 183 184 185
      self,
      users,
      access_level,
      current_user: current_user,
      expires_at: expires_at
    )
186 187
  end

188
  def add_user(user, access_level, current_user: nil, expires_at: nil, ldap: false)
189 190 191 192 193
    GroupMember.add_user(
      self,
      user,
      access_level,
      current_user: current_user,
194 195
      expires_at: expires_at,
      ldap: ldap
196
    )
197 198
  end

199
  def add_guest(user, current_user = nil)
200
    add_user(user, :guest, current_user: current_user)
201 202 203
  end

  def add_reporter(user, current_user = nil)
204
    add_user(user, :reporter, current_user: current_user)
205 206 207
  end

  def add_developer(user, current_user = nil)
208
    add_user(user, :developer, current_user: current_user)
209 210
  end

211 212
  def add_maintainer(user, current_user = nil)
    add_user(user, :maintainer, current_user: current_user)
213 214
  end

215 216 217
  # @deprecated
  alias_method :add_master, :add_maintainer

Douwe Maan's avatar
Douwe Maan committed
218
  def add_owner(user, current_user = nil)
219
    add_user(user, :owner, current_user: current_user)
Douwe Maan's avatar
Douwe Maan committed
220 221
  end

222 223 224 225 226 227
  def member?(user, min_access_level = Gitlab::Access::GUEST)
    return false unless user

    max_member_access_for_user(user) >= min_access_level
  end

Douwe Maan's avatar
Douwe Maan committed
228
  def has_owner?(user)
229 230
    return false unless user

231
    members_with_parents.owners.where(user_id: user).any?
Douwe Maan's avatar
Douwe Maan committed
232 233
  end

234
  def has_maintainer?(user)
235 236
    return false unless user

237
    members_with_parents.maintainers.where(user_id: user).any?
Douwe Maan's avatar
Douwe Maan committed
238 239
  end

240 241 242
  # @deprecated
  alias_method :has_master?, :has_maintainer?

243 244
  # Check if user is a last owner of the group.
  # Parent owners are ignored for nested groups.
Douwe Maan's avatar
Douwe Maan committed
245
  def last_owner?(user)
246
    owners.include?(user) && owners.size == 1
Douwe Maan's avatar
Douwe Maan committed
247 248
  end

249 250 251 252
  def ldap_synced?
    false
  end

253
  def post_create_hook
254 255
    Gitlab::AppLogger.info("Group \"#{name}\" was created")

256 257 258 259
    system_hook_service.execute_hooks_for(self, :create)
  end

  def post_destroy_hook
260 261
    Gitlab::AppLogger.info("Group \"#{name}\" was removed")

262 263 264
    system_hook_service.execute_hooks_for(self, :destroy)
  end

265
  # rubocop: disable CodeReuse/ServiceClass
266 267 268
  def system_hook_service
    SystemHooksService.new
  end
269
  # rubocop: enable CodeReuse/ServiceClass
270

271
  # rubocop: disable CodeReuse/ServiceClass
272
  def refresh_members_authorized_projects(blocking: true)
273
    UserProjectAccessChangedService.new(user_ids_for_project_authorizations)
274
      .execute(blocking: blocking)
275
  end
276
  # rubocop: enable CodeReuse/ServiceClass
277 278

  def user_ids_for_project_authorizations
279
    members_with_parents.pluck(:user_id)
280 281
  end

282 283 284 285 286 287
  def self_and_ancestors_ids
    strong_memoize(:self_and_ancestors_ids) do
      self_and_ancestors.pluck(:id)
    end
  end

288
  def members_with_parents
289 290 291 292 293 294 295 296 297
    # Avoids an unnecessary SELECT when the group has no parents
    source_ids =
      if parent_id
        self_and_ancestors.reorder(nil).select(:id)
      else
        id
      end

    GroupMember
298
      .active_without_invites_and_requests
299 300 301 302 303
      .where(source_id: source_ids)
  end

  def members_with_descendants
    GroupMember
304
      .active_without_invites_and_requests
305
      .where(source_id: self_and_descendants.reorder(nil).select(:id))
306 307
  end

308 309 310 311 312 313 314
  # Returns all members that are part of the group, it's subgroups, and ancestor groups
  def direct_and_indirect_members
    GroupMember
      .active_without_invites_and_requests
      .where(source_id: self_and_hierarchy.reorder(nil).select(:id))
  end

315
  def users_with_parents
316 317 318
    User
      .where(id: members_with_parents.select(:user_id))
      .reorder(nil)
319
  end
Z.J. van de Weg's avatar
Z.J. van de Weg committed
320

321
  def users_with_descendants
322 323 324
    User
      .where(id: members_with_descendants.select(:user_id))
      .reorder(nil)
325 326
  end

327 328 329 330 331 332
  # Returns all users that are members of the group because:
  # 1. They belong to the group
  # 2. They belong to a project that belongs to the group
  # 3. They belong to a sub-group or project in such sub-group
  # 4. They belong to an ancestor group
  def direct_and_indirect_users
333
    User.from_union([
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
      User
        .where(id: direct_and_indirect_members.select(:user_id))
        .reorder(nil),
      project_users_with_descendants
    ])
  end

  # Returns all users that are members of projects
  # belonging to the current group or sub-groups
  def project_users_with_descendants
    User
      .joins(projects: :group)
      .where(namespaces: { id: self_and_descendants.select(:id) })
  end

349 350 351
  def max_member_access_for_user(user)
    return GroupMember::OWNER if user.admin?

352 353 354 355
    members_with_parents
      .where(user_id: user)
      .reorder(access_level: :desc)
      .first&.
356 357 358
      access_level || GroupMember::NO_ACCESS
  end

Z.J. van de Weg's avatar
Z.J. van de Weg committed
359 360 361 362 363 364 365 366 367
  def mattermost_team_params
    max_length = 59

    {
      name: path[0..max_length],
      display_name: name[0..max_length],
      type: public? ? 'O' : 'I' # Open vs Invite-only
    }
  end
368

369
  def ci_variables_for(ref, project)
370 371 372 373 374
    list_of_ids = [self] + ancestors
    variables = Ci::GroupVariable.where(group: list_of_ids)
    variables = variables.unprotected unless project.protected_for?(ref)
    variables = variables.group_by(&:group_id)
    list_of_ids.reverse.map { |group| variables[group.id] }.compact.flatten
Shinya Maeda's avatar
Shinya Maeda committed
375 376
  end

377 378 379 380 381 382 383 384
  def group_member(user)
    if group_members.loaded?
      group_members.find { |gm| gm.user_id == user.id }
    else
      group_members.find_by(user_id: user)
    end
  end

385 386 387 388
  def highest_group_member(user)
    GroupMember.where(source_id: self_and_ancestors_ids, user_id: user.id).order(:access_level).last
  end

Jarka Kadlecova's avatar
Jarka Kadlecova committed
389 390 391 392
  def hashed_storage?(_feature)
    false
  end

393 394 395 396
  def refresh_project_authorizations
    refresh_members_authorized_projects(blocking: false)
  end

397 398 399 400 401 402 403
  # each existing group needs to have a `runners_token`.
  # we do this on read since migrating all existing groups is not a feasible
  # solution.
  def runners_token
    ensure_runners_token!
  end

404 405 406 407
  def group_clusters_enabled?
    Feature.enabled?(:group_clusters, root_ancestor, default_enabled: true)
  end

408
  private
409 410 411 412 413 414

  def update_two_factor_requirement
    return unless require_two_factor_authentication_changed? || two_factor_grace_period_changed?

    users.find_each(&:update_two_factor_requirement)
  end
415

416 417 418 419
  def path_changed_hook
    system_hook_service.execute_hooks_for(self, :rename)
  end

420 421 422
  def visibility_level_allowed_by_parent
    return if visibility_level_allowed_by_parent?

423
    errors.add(:visibility_level, "#{visibility} is not allowed since the parent group has a #{parent.visibility} visibility.")
424 425 426 427 428
  end

  def visibility_level_allowed_by_projects
    return if visibility_level_allowed_by_projects?

429
    errors.add(:visibility_level, "#{visibility} is not allowed since this group contains projects with higher visibility.")
430 431 432 433 434
  end

  def visibility_level_allowed_by_sub_groups
    return if visibility_level_allowed_by_sub_groups?

435
    errors.add(:visibility_level, "#{visibility} is not allowed since there are sub-groups with higher visibility.")
436
  end
437
end