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
Rémy Coutable's avatar
Rémy Coutable committed
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
Felipe Artur's avatar
Felipe Artur committed
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
  validates :path, format: { without: /\A([a-zA-Z]{4}[0-9]{2}|hg[0-9]+)\z/, message: "must not match the format of THM usernames" }
57

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

60
  add_authentication_token_field :runners_token, encrypted: true, migrating: true
61

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

67
  class << self
68
    def sort_by_attribute(method)
Markus Koller's avatar
Markus Koller committed
69
70
71
72
73
74
75
      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
76
    end
77
78

    def reference_prefix
79
80
81
82
83
      User.reference_prefix
    end

    def reference_pattern
      User.reference_pattern
84
    end
85

86
87
88
89
90
91
92
93
94
95
96
    # 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)
97
    end
98
99
100

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

    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
125
126
  end

127
128
129
130
131
132
133
134
135
  # 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

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

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

144
  def human_name
145
    full_name
146
  end
147

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

151
152
    level <= parent.visibility_level
  end
153

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

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

162
163
164
165
  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)
166
167
  end

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

    self[:lfs_enabled]
  end

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

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

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

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

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

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

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

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

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

223
224
225
226
227
228
  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
229
  def has_owner?(user)
230
231
    return false unless user

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

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

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

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

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

250
251
252
253
  def ldap_synced?
    false
  end

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

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

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

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

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

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

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

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

289
  def members_with_parents
290
291
292
293
294
295
296
297
298
    # 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
299
      .active_without_invites_and_requests
300
301
302
303
304
      .where(source_id: source_ids)
  end

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

309
310
311
312
313
314
315
  # 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

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

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

328
329
330
331
332
333
  # 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
334
    User.from_union([
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
      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

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

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

Z.J. van de Weg's avatar
Z.J. van de Weg committed
360
361
362
363
364
365
366
367
368
  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
369

370
  def ci_variables_for(ref, project)
371
372
373
374
375
    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
376
377
  end

378
379
380
381
382
383
384
385
  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

Jarka Kadlecova's avatar
Jarka Kadlecova committed
386
387
388
389
  def hashed_storage?(_feature)
    false
  end

390
391
392
393
  def refresh_project_authorizations
    refresh_members_authorized_projects(blocking: false)
  end

394
395
396
397
398
399
400
  # 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

401
402
403
404
  def group_clusters_enabled?
    Feature.enabled?(:group_clusters, root_ancestor, default_enabled: true)
  end

405
  private
406
407
408
409
410
411

  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
412

413
414
415
416
  def path_changed_hook
    system_hook_service.execute_hooks_for(self, :rename)
  end

417
418
419
  def visibility_level_allowed_by_parent
    return if visibility_level_allowed_by_parent?

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

  def visibility_level_allowed_by_projects
    return if visibility_level_allowed_by_projects?

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

  def visibility_level_allowed_by_sub_groups
    return if visibility_level_allowed_by_sub_groups?

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