2018-11-18 11:00:15 +05:30
# frozen_string_literal: true
2014-09-02 18:07:02 +05:30
require 'carrierwave/orm/activerecord'
class Group < Namespace
2015-09-11 14:41:01 +05:30
include Gitlab :: ConfigHelper
2018-03-17 18:26:18 +05:30
include AfterCommitQueue
2016-06-16 23:09:34 +05:30
include AccessRequestable
2017-09-10 17:25:29 +05:30
include Avatarable
2015-09-11 14:41:01 +05:30
include Referable
2017-08-17 22:00:37 +05:30
include SelectForProjectAuthorization
2018-03-17 18:26:18 +05:30
include LoadedInGroupList
include GroupDescendant
2018-10-15 14:42:47 +05:30
include TokenAuthenticatable
2018-11-08 19:23:39 +05:30
include WithUploads
include Gitlab :: Utils :: StrongMemoize
2019-12-21 20:55:43 +05:30
include GroupAPICompatibility
2016-04-02 18:10:28 +05:30
2019-12-04 20:38:33 +05:30
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
2017-09-10 17:25:29 +05:30
has_many :group_members , - > { where ( requested_at : nil ) } , dependent : :destroy , as : :source # rubocop:disable Cop/ActiveRecordDependent
2015-11-26 14:37:03 +05:30
alias_method :members , :group_members
2016-08-24 12:49:21 +05:30
has_many :users , through : :group_members
2016-06-22 15:30:34 +05:30
has_many :owners ,
- > { where ( members : { access_level : Gitlab :: Access :: OWNER } ) } ,
through : :group_members ,
source : :user
2017-09-10 17:25:29 +05:30
has_many :requesters , - > { where . not ( requested_at : nil ) } , dependent : :destroy , as : :source , class_name : 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
2018-03-17 18:26:18 +05:30
has_many :members_and_requesters , as : :source , class_name : 'GroupMember'
2016-08-24 12:49:21 +05:30
2017-09-10 17:25:29 +05:30
has_many :milestones
2019-12-26 22:10:19 +05:30
has_many :shared_group_links , foreign_key : :shared_with_group_id , class_name : 'GroupGroupLink'
has_many :shared_with_group_links , foreign_key : :shared_group_id , class_name : 'GroupGroupLink'
has_many :shared_groups , through : :shared_group_links , source : :shared_group
has_many :shared_with_groups , through : :shared_with_group_links , source : :shared_with_group
2017-09-10 17:25:29 +05:30
has_many :project_group_links , dependent : :destroy # rubocop:disable Cop/ActiveRecordDependent
2016-06-02 11:05:42 +05:30
has_many :shared_projects , through : :project_group_links , source : :project
2018-11-08 19:23:39 +05:30
# Overridden on another method
# Left here just to be dependent: :destroy
2017-09-10 17:25:29 +05:30
has_many :notification_settings , dependent : :destroy , as : :source # rubocop:disable Cop/ActiveRecordDependent
2018-11-08 19:23:39 +05:30
2016-11-03 12:29:30 +05:30
has_many :labels , class_name : 'GroupLabel'
2017-09-10 17:25:29 +05:30
has_many :variables , class_name : 'Ci::GroupVariable'
2018-03-17 18:26:18 +05:30
has_many :custom_attributes , class_name : 'GroupCustomAttribute'
2014-09-02 18:07:02 +05:30
2018-03-27 19:54:05 +05:30
has_many :boards
has_many :badges , class_name : 'GroupBadge'
2018-12-13 13:39:08 +05:30
has_many :cluster_groups , class_name : 'Clusters::Group'
has_many :clusters , through : :cluster_groups , class_name : 'Clusters::Cluster'
2019-10-12 21:52:04 +05:30
has_many :container_repositories , through : :projects
2018-11-18 11:00:15 +05:30
has_many :todos
2019-12-26 22:10:19 +05:30
has_one :import_export_upload
2020-03-13 15:44:24 +05:30
has_many :import_failures , inverse_of : :group
2018-03-17 18:26:18 +05:30
accepts_nested_attributes_for :variables , allow_destroy : true
2014-09-02 18:07:02 +05:30
2018-03-17 18:26:18 +05:30
validate :visibility_level_allowed_by_projects
validate :visibility_level_allowed_by_sub_groups
validate :visibility_level_allowed_by_parent
validates :variables , variable_duplicates : true
2017-08-17 22:00:37 +05:30
2018-03-17 18:26:18 +05:30
validates :two_factor_grace_period , presence : true , numericality : { greater_than_or_equal_to : 0 }
2020-03-28 13:19:24 +05:30
validates :name ,
format : { with : Gitlab :: Regex . group_name_regex ,
message : Gitlab :: Regex . group_name_regex_message }
2015-04-26 12:48:37 +05:30
2019-07-07 11:18:12 +05:30
add_authentication_token_field :runners_token , encrypted : - > { Feature . enabled? ( :groups_tokens_optional_encryption , default_enabled : true ) ? :optional : :required }
2018-10-15 14:42:47 +05:30
2015-04-26 12:48:37 +05:30
after_create :post_create_hook
after_destroy :post_destroy_hook
2017-08-17 22:00:37 +05:30
after_save :update_two_factor_requirement
2019-07-31 22:56:46 +05:30
after_update :path_changed_hook , if : :saved_change_to_path?
2015-04-26 12:48:37 +05:30
2019-09-30 21:07:59 +05:30
scope :with_users , - > { includes ( :users ) }
2015-04-26 12:48:37 +05:30
class << self
2018-05-09 12:01:36 +05:30
def sort_by_attribute ( method )
2017-08-17 22:00:37 +05:30
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
2015-04-26 12:48:37 +05:30
end
2015-09-11 14:41:01 +05:30
def reference_prefix
User . reference_prefix
end
def reference_pattern
User . reference_pattern
end
2015-11-26 14:37:03 +05:30
2018-11-08 19:23:39 +05:30
# 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 )
2015-11-26 14:37:03 +05:30
end
2017-08-17 22:00:37 +05:30
def select_for_project_authorization
if current_scope . joins_values . include? ( :shared_projects )
joins ( 'INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id' )
2019-03-02 22:35:43 +05:30
. where ( 'project_namespace.share_with_group_lock = ?' , false )
2017-09-10 17:25:29 +05:30
. select ( " projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level " )
2017-08-17 22:00:37 +05:30
else
super
end
end
2018-11-08 19:23:39 +05:30
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
2019-12-26 22:10:19 +05:30
authorized_groups = user . authorized_groups . arel . as ( 'authorized' )
2018-11-08 19:23:39 +05:30
groups_table . project ( 1 )
. from ( authorized_groups )
. where ( authorized_groups [ :id ] . eq ( groups_table [ :id ] ) )
. exists
end
end
# Overrides notification_settings has_many association
# This allows to apply notification settings from parent groups
# to child groups and projects.
2019-09-04 21:01:54 +05:30
def notification_settings ( hierarchy_order : nil )
2018-11-08 19:23:39 +05:30
source_type = self . class . base_class . name
2019-09-04 21:01:54 +05:30
settings = NotificationSetting . where ( source_type : source_type , source_id : self_and_ancestors_ids )
2018-11-08 19:23:39 +05:30
2019-09-04 21:01:54 +05:30
return settings unless hierarchy_order && self_and_ancestors_ids . length > 1
settings
. joins ( " LEFT JOIN ( #{ self_and_ancestors ( hierarchy_order : hierarchy_order ) . to_sql } ) AS ordered_groups ON notification_settings.source_id = ordered_groups.id " )
. select ( 'notification_settings.*, ordered_groups.depth AS depth' )
. order ( " ordered_groups.depth #{ hierarchy_order } " )
end
def notification_settings_for ( user , hierarchy_order : nil )
notification_settings ( hierarchy_order : hierarchy_order ) . where ( user : user )
2015-09-11 14:41:01 +05:30
end
2019-10-12 21:52:04 +05:30
def notification_email_for ( user )
# Finds the closest notification_setting with a `notification_email`
notification_settings = notification_settings_for ( user , hierarchy_order : :asc )
notification_settings . find { | n | n . notification_email . present? } & . notification_email
end
2018-03-17 18:26:18 +05:30
def to_reference ( _from = nil , full : nil )
2017-08-17 22:00:37 +05:30
" #{ self . class . reference_prefix } #{ full_path } "
2015-04-26 12:48:37 +05:30
end
2014-09-02 18:07:02 +05:30
2016-06-16 23:09:34 +05:30
def web_url
2016-11-03 12:29:30 +05:30
Gitlab :: Routing . url_helpers . group_canonical_url ( self )
2016-06-16 23:09:34 +05:30
end
2014-09-02 18:07:02 +05:30
def human_name
2017-08-17 22:00:37 +05:30
full_name
2014-09-02 18:07:02 +05:30
end
2018-03-17 18:26:18 +05:30
def visibility_level_allowed_by_parent? ( level = self . visibility_level )
return true unless parent_id && parent_id . nonzero?
2016-06-02 11:05:42 +05:30
2018-03-17 18:26:18 +05:30
level < = parent . visibility_level
end
def visibility_level_allowed_by_projects? ( level = self . visibility_level )
! projects . where ( 'visibility_level > ?' , level ) . exists?
end
2016-06-02 11:05:42 +05:30
2018-03-17 18:26:18 +05:30
def visibility_level_allowed_by_sub_groups? ( level = self . visibility_level )
! children . where ( 'visibility_level > ?' , level ) . exists?
2016-06-02 11:05:42 +05:30
end
2018-03-17 18:26:18 +05:30
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 )
2015-09-11 14:41:01 +05:30
end
2016-09-29 09:46:39 +05:30
def lfs_enabled?
return false unless Gitlab . config . lfs . enabled
return Gitlab . config . lfs . enabled if self [ :lfs_enabled ] . nil?
self [ :lfs_enabled ]
end
2018-10-15 14:42:47 +05:30
def owned_by? ( user )
owners . include? ( user )
end
2016-11-03 12:29:30 +05:30
def add_users ( users , access_level , current_user : nil , expires_at : nil )
2017-08-17 22:00:37 +05:30
GroupMember . add_users (
2016-11-03 12:29:30 +05:30
self ,
users ,
access_level ,
current_user : current_user ,
expires_at : expires_at
)
2014-09-02 18:07:02 +05:30
end
2018-11-08 19:23:39 +05:30
def add_user ( user , access_level , current_user : nil , expires_at : nil , ldap : false )
2016-11-03 12:29:30 +05:30
GroupMember . add_user (
self ,
user ,
access_level ,
current_user : current_user ,
2018-11-08 19:23:39 +05:30
expires_at : expires_at ,
ldap : ldap
2016-11-03 12:29:30 +05:30
)
2014-09-02 18:07:02 +05:30
end
2015-09-11 14:41:01 +05:30
def add_guest ( user , current_user = nil )
2016-11-03 12:29:30 +05:30
add_user ( user , :guest , current_user : current_user )
2015-09-11 14:41:01 +05:30
end
def add_reporter ( user , current_user = nil )
2016-11-03 12:29:30 +05:30
add_user ( user , :reporter , current_user : current_user )
2015-09-11 14:41:01 +05:30
end
def add_developer ( user , current_user = nil )
2016-11-03 12:29:30 +05:30
add_user ( user , :developer , current_user : current_user )
2015-09-11 14:41:01 +05:30
end
2018-11-18 11:00:15 +05:30
def add_maintainer ( user , current_user = nil )
add_user ( user , :maintainer , current_user : current_user )
2015-09-11 14:41:01 +05:30
end
2018-11-18 11:00:15 +05:30
# @deprecated
alias_method :add_master , :add_maintainer
2015-04-26 12:48:37 +05:30
def add_owner ( user , current_user = nil )
2016-11-03 12:29:30 +05:30
add_user ( user , :owner , current_user : current_user )
2014-09-02 18:07:02 +05:30
end
2018-03-17 18:26:18 +05:30
def member? ( user , min_access_level = Gitlab :: Access :: GUEST )
return false unless user
max_member_access_for_user ( user ) > = min_access_level
end
2014-09-02 18:07:02 +05:30
def has_owner? ( user )
2017-09-10 17:25:29 +05:30
return false unless user
2019-07-07 11:18:12 +05:30
members_with_parents . owners . exists? ( user_id : user )
2014-09-02 18:07:02 +05:30
end
2018-11-18 11:00:15 +05:30
def has_maintainer? ( user )
2017-09-10 17:25:29 +05:30
return false unless user
2019-07-07 11:18:12 +05:30
members_with_parents . maintainers . exists? ( user_id : user )
2014-09-02 18:07:02 +05:30
end
2019-12-26 22:10:19 +05:30
def has_container_repository_including_subgroups?
:: ContainerRepository . for_group_and_its_subgroups ( self ) . exists?
2019-12-21 20:55:43 +05:30
end
2018-11-18 11:00:15 +05:30
# @deprecated
alias_method :has_master? , :has_maintainer?
2017-08-17 22:00:37 +05:30
# Check if user is a last owner of the group.
2014-09-02 18:07:02 +05:30
def last_owner? ( user )
2019-07-07 11:18:12 +05:30
has_owner? ( user ) && members_with_parents . owners . size == 1
2014-09-02 18:07:02 +05:30
end
2018-11-08 19:23:39 +05:30
def ldap_synced?
false
end
2015-04-26 12:48:37 +05:30
def post_create_hook
2015-09-11 14:41:01 +05:30
Gitlab :: AppLogger . info ( " Group \" #{ name } \" was created " )
2015-04-26 12:48:37 +05:30
system_hook_service . execute_hooks_for ( self , :create )
end
2014-09-02 18:07:02 +05:30
2015-04-26 12:48:37 +05:30
def post_destroy_hook
2015-09-11 14:41:01 +05:30
Gitlab :: AppLogger . info ( " Group \" #{ name } \" was removed " )
2015-04-26 12:48:37 +05:30
system_hook_service . execute_hooks_for ( self , :destroy )
end
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2015-04-26 12:48:37 +05:30
def system_hook_service
SystemHooksService . new
2014-09-02 18:07:02 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2017-08-17 22:00:37 +05:30
2018-12-05 23:21:45 +05:30
# rubocop: disable CodeReuse/ServiceClass
2018-03-17 18:26:18 +05:30
def refresh_members_authorized_projects ( blocking : true )
2017-09-10 17:25:29 +05:30
UserProjectAccessChangedService . new ( user_ids_for_project_authorizations )
2018-03-17 18:26:18 +05:30
. execute ( blocking : blocking )
2017-08-17 22:00:37 +05:30
end
2018-12-05 23:21:45 +05:30
# rubocop: enable CodeReuse/ServiceClass
2017-08-17 22:00:37 +05:30
def user_ids_for_project_authorizations
2017-09-10 17:25:29 +05:30
members_with_parents . pluck ( :user_id )
2017-08-17 22:00:37 +05:30
end
2018-11-08 19:23:39 +05:30
def self_and_ancestors_ids
strong_memoize ( :self_and_ancestors_ids ) do
self_and_ancestors . pluck ( :id )
end
end
2017-08-17 22:00:37 +05:30
def members_with_parents
2017-09-10 17:25:29 +05:30
# 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
2018-04-04 21:44:52 +05:30
. active_without_invites_and_requests
2017-09-10 17:25:29 +05:30
. where ( source_id : source_ids )
end
def members_with_descendants
GroupMember
2018-04-04 21:44:52 +05:30
. active_without_invites_and_requests
2017-09-10 17:25:29 +05:30
. where ( source_id : self_and_descendants . reorder ( nil ) . select ( :id ) )
2017-08-17 22:00:37 +05:30
end
2018-10-15 14:42:47 +05:30
# 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
2017-08-17 22:00:37 +05:30
def users_with_parents
2017-09-10 17:25:29 +05:30
User
. where ( id : members_with_parents . select ( :user_id ) )
. reorder ( nil )
end
def users_with_descendants
User
. where ( id : members_with_descendants . select ( :user_id ) )
. reorder ( nil )
end
2018-10-15 14:42:47 +05:30
# 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
2018-12-05 23:21:45 +05:30
User . from_union ( [
2018-10-15 14:42:47 +05:30
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
2017-09-10 17:25:29 +05:30
def max_member_access_for_user ( user )
2019-09-04 21:01:54 +05:30
return GroupMember :: NO_ACCESS unless user
2017-09-10 17:25:29 +05:30
return GroupMember :: OWNER if user . admin?
2019-12-26 22:10:19 +05:30
max_member_access = members_with_parents . where ( user_id : user )
. reorder ( access_level : :desc )
. first
& . access_level
max_member_access || max_member_access_for_user_from_shared_groups ( user ) || GroupMember :: NO_ACCESS
2017-08-17 22:00:37 +05:30
end
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
2018-12-13 13:39:08 +05:30
def ci_variables_for ( ref , project )
2017-09-10 17:25:29 +05:30
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 )
2019-10-12 21:52:04 +05:30
list_of_ids . reverse . flat_map { | group | variables [ group . id ] } . compact
2017-09-10 17:25:29 +05:30
end
2018-03-17 18:26:18 +05:30
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
2019-03-02 22:35:43 +05:30
def highest_group_member ( user )
GroupMember . where ( source_id : self_and_ancestors_ids , user_id : user . id ) . order ( :access_level ) . last
end
2020-03-13 15:44:24 +05:30
def related_group_ids
[ id ,
* ancestors . pluck ( :id ) ,
* shared_with_group_links . pluck ( :shared_with_group_id ) ]
end
2018-03-17 18:26:18 +05:30
def hashed_storage? ( _feature )
false
end
2018-05-09 12:01:36 +05:30
def refresh_project_authorizations
refresh_members_authorized_projects ( blocking : false )
end
2018-10-15 14:42:47 +05:30
# 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
2019-07-07 11:18:12 +05:30
def project_creation_level
super || :: Gitlab :: CurrentSettings . default_project_creation
end
2019-10-12 21:52:04 +05:30
def subgroup_creation_level
super || :: Gitlab :: Access :: OWNER_SUBGROUP_ACCESS
end
2019-12-04 20:38:33 +05:30
def access_request_approvers_to_be_notified
members . owners . order_recent_sign_in . limit ( ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT )
end
2019-12-21 20:55:43 +05:30
def supports_events?
false
end
2019-12-26 22:10:19 +05:30
def export_file_exists?
export_file & . file
end
def export_file
import_export_upload & . export_file
end
2020-03-13 15:44:24 +05:30
def adjourned_deletion?
false
end
2018-03-17 18:26:18 +05:30
private
2017-08-17 22:00:37 +05:30
def update_two_factor_requirement
2019-07-31 22:56:46 +05:30
return unless saved_change_to_require_two_factor_authentication? || saved_change_to_two_factor_grace_period?
2017-08-17 22:00:37 +05:30
2019-09-04 21:01:54 +05:30
members_with_descendants . find_each ( & :update_two_factor_requirement )
2017-08-17 22:00:37 +05:30
end
2018-03-17 18:26:18 +05:30
def path_changed_hook
system_hook_service . execute_hooks_for ( self , :rename )
end
def visibility_level_allowed_by_parent
return if visibility_level_allowed_by_parent?
errors . add ( :visibility_level , " #{ visibility } is not allowed since the parent group has a #{ parent . visibility } visibility. " )
end
def visibility_level_allowed_by_projects
return if visibility_level_allowed_by_projects?
errors . add ( :visibility_level , " #{ visibility } is not allowed since this group contains projects with higher visibility. " )
end
def visibility_level_allowed_by_sub_groups
return if visibility_level_allowed_by_sub_groups?
errors . add ( :visibility_level , " #{ visibility } is not allowed since there are sub-groups with higher visibility. " )
end
2019-12-21 20:55:43 +05:30
2019-12-26 22:10:19 +05:30
def max_member_access_for_user_from_shared_groups ( user )
2020-03-13 15:44:24 +05:30
return unless Feature . enabled? ( :share_group_with_group , default_enabled : true )
2019-12-26 22:10:19 +05:30
group_group_link_table = GroupGroupLink . arel_table
group_member_table = GroupMember . arel_table
group_group_links_query = GroupGroupLink . where ( shared_group_id : self_and_ancestors_ids )
cte = Gitlab :: SQL :: CTE . new ( :group_group_links_cte , group_group_links_query )
2020-03-07 23:17:34 +05:30
cte_alias = cte . table . alias ( GroupGroupLink . table_name )
2019-12-26 22:10:19 +05:30
link = GroupGroupLink
. with ( cte . to_arel )
2020-03-07 23:17:34 +05:30
. select ( smallest_value_arel ( [ cte_alias [ :group_access ] , group_member_table [ :access_level ] ] ,
'group_access' ) )
2019-12-26 22:10:19 +05:30
. from ( [ group_member_table , cte . alias_to ( group_group_link_table ) ] )
. where ( group_member_table [ :user_id ] . eq ( user . id ) )
2020-03-07 23:17:34 +05:30
. where ( group_member_table [ :requested_at ] . eq ( nil ) )
2019-12-26 22:10:19 +05:30
. where ( group_member_table [ :source_id ] . eq ( group_group_link_table [ :shared_with_group_id ] ) )
2020-03-07 23:17:34 +05:30
. where ( group_member_table [ :source_type ] . eq ( 'Namespace' ) )
2019-12-26 22:10:19 +05:30
. reorder ( Arel :: Nodes :: Descending . new ( group_group_link_table [ :group_access ] ) )
. first
link & . group_access
end
2020-03-07 23:17:34 +05:30
def smallest_value_arel ( args , column_alias )
Arel :: Nodes :: As . new (
Arel :: Nodes :: NamedFunction . new ( 'LEAST' , args ) ,
Arel :: Nodes :: SqlLiteral . new ( column_alias ) )
end
2019-12-21 20:55:43 +05:30
def self . groups_including_descendants_by ( group_ids )
Gitlab :: ObjectHierarchy
. new ( Group . where ( id : group_ids ) )
. base_and_descendants
end
2014-09-02 18:07:02 +05:30
end
2019-12-04 20:38:33 +05:30
Group . prepend_if_ee ( 'EE::Group' )