app/models/group.rb (922 lines of code) (raw):
# frozen_string_literal: true
require 'carrierwave/orm/activerecord'
class Group < Namespace
include Gitlab::ConfigHelper
include AfterCommitQueue
include AccessRequestable
include Avatarable
include SelectForProjectAuthorization
include LoadedInGroupList
include GroupDescendant
include TokenAuthenticatable
include WithUploads
include Gitlab::Utils::StrongMemoize
include GroupAPICompatibility
include EachBatch
include BulkMemberAccessLoad
include BulkUsersByEmailLoad
include ChronicDurationAttribute
include RunnerTokenExpirationInterval
include Importable
include IdInOrdered
include Members::Enumerable
extend ::Gitlab::Utils::Override
self.allow_legacy_sti_class = true
README_PROJECT_PATH = 'gitlab-profile'
def self.sti_name
'Group'
end
def self.supported_keyset_orderings
{ name: [:asc] }
end
has_many :all_group_members, -> { non_request }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
has_many :all_owner_members, -> { non_request.all_owners }, as: :source, class_name: 'GroupMember'
has_many :group_members, -> { non_request.non_minimal_access }, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :non_invite_group_members, -> { non_request.non_minimal_access.non_invite }, class_name: 'GroupMember', as: :source
has_many :non_invite_owner_members, -> { non_request.non_invite.all_owners }, class_name: 'GroupMember', as: :source
has_many :request_group_members, -> do
request.non_minimal_access
end, inverse_of: :group, class_name: 'GroupMember', as: :source
has_many :namespace_members, -> { non_request.non_minimal_access.unscope(where: %i[source_id source_type]) },
foreign_key: :member_namespace_id, inverse_of: :group, class_name: 'GroupMember'
alias_method :members, :group_members
has_many :users, through: :group_members
has_many :owners, through: :all_owner_members, source: :user
has_many :requesters, -> { where.not(requested_at: nil) }, dependent: :destroy, as: :source, class_name: 'GroupMember' # rubocop:disable Cop/ActiveRecordDependent
has_many :namespace_requesters, -> { where.not(requested_at: nil).unscope(where: %i[source_id source_type]) },
foreign_key: :member_namespace_id, inverse_of: :group, class_name: 'GroupMember'
has_many :members_and_requesters, as: :source, class_name: 'GroupMember'
has_many :namespace_members_and_requesters, -> { unscope(where: %i[source_id source_type]) },
foreign_key: :member_namespace_id, inverse_of: :group, class_name: 'GroupMember'
has_many :milestones
has_many :integrations
with_options class_name: 'GroupGroupLink' do
has_many :shared_group_links, foreign_key: :shared_with_group_id
with_options foreign_key: :shared_group_id do
has_many :shared_with_group_links
has_many :shared_with_group_links_of_ancestors, ->(group) do
unscope(where: :shared_group_id).where(shared_group: group.ancestors)
end
has_many :shared_with_group_links_of_ancestors_and_self, ->(group) do
unscope(where: :shared_group_id).where(shared_group: group.self_and_ancestors)
end
end
end
has_many :shared_groups, through: :shared_group_links, source: :shared_group
with_options source: :shared_with_group do
has_many :shared_with_groups, through: :shared_with_group_links
has_many :shared_with_groups_of_ancestors, through: :shared_with_group_links_of_ancestors
has_many :shared_with_groups_of_ancestors_and_self, through: :shared_with_group_links_of_ancestors_and_self
end
has_many :project_group_links, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :shared_projects, through: :project_group_links, source: :project
# Overridden on another method
# Left here just to be dependent: :destroy
has_many :notification_settings, dependent: :destroy, as: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :labels, class_name: 'GroupLabel'
has_many :variables, class_name: 'Ci::GroupVariable'
has_many :daily_build_group_report_results, class_name: 'Ci::DailyBuildGroupReportResult'
has_many :custom_attributes, class_name: 'GroupCustomAttribute'
has_many :boards
has_many :badges, class_name: 'GroupBadge'
# AR defaults to nullify when trying to delete via has_many associations unless we set dependent: :delete_all
has_many :crm_organizations, class_name: 'CustomerRelations::Organization', inverse_of: :group, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :contacts, class_name: 'CustomerRelations::Contact', inverse_of: :group, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_one :crm_settings, class_name: 'Group::CrmSettings', inverse_of: :group
# Groups for which this is the source of CRM contacts/organizations
has_many :crm_targets, class_name: 'Group::CrmSettings', inverse_of: :source_group, foreign_key: 'source_group_id'
has_many :cluster_groups, class_name: 'Clusters::Group'
has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster'
has_many :container_repositories, through: :projects
has_many :todos
has_many :import_export_uploads, dependent: :destroy, inverse_of: :group # rubocop:disable Cop/ActiveRecordDependent -- Previously was has_one association, dependent: :destroy to be removed in a separate issue and cascade FK will be added
has_many :import_failures, inverse_of: :group
has_one :import_state, class_name: 'GroupImportState', inverse_of: :group
has_many :bulk_import_exports, class_name: 'BulkImports::Export', inverse_of: :group
has_many :bulk_import_entities, class_name: 'BulkImports::Entity', foreign_key: :namespace_id, inverse_of: :group
has_many :group_deploy_keys_groups, inverse_of: :group
has_many :group_deploy_keys, through: :group_deploy_keys_groups
has_many :group_deploy_tokens
has_many :deploy_tokens, through: :group_deploy_tokens
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :dependency_proxy_setting, class_name: 'DependencyProxy::GroupSetting'
has_one :dependency_proxy_image_ttl_policy, class_name: 'DependencyProxy::ImageTtlGroupPolicy'
has_many :dependency_proxy_blobs, class_name: 'DependencyProxy::Blob'
has_many :dependency_proxy_manifests, class_name: 'DependencyProxy::Manifest'
has_one :deletion_schedule, class_name: 'GroupDeletionSchedule'
delegate :deleting_user, :marked_for_deletion_on, to: :deletion_schedule, allow_nil: true
scope :aimed_for_deletion, ->(date) { joins(:deletion_schedule).where('group_deletion_schedules.marked_for_deletion_on <= ?', date) }
scope :not_aimed_for_deletion, -> { where.missing(:deletion_schedule) }
scope :with_deletion_schedule, -> { preload(deletion_schedule: :deleting_user) }
scope :with_deletion_schedule_only, -> { preload(:deletion_schedule) }
scope :by_marked_for_deletion_on, ->(marked_for_deletion_on) do
joins(:deletion_schedule)
.where(group_deletion_schedules: { marked_for_deletion_on: marked_for_deletion_on })
end
has_one :harbor_integration, class_name: 'Integrations::Harbor'
# debian_distributions and associated component_files must be destroyed by ruby code in order to properly remove carrierwave uploads
has_many :debian_distributions, class_name: 'Packages::Debian::GroupDistribution', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :group_callouts, class_name: 'Users::GroupCallout', foreign_key: :group_id
has_many :protected_branches, inverse_of: :group, foreign_key: :namespace_id
has_one :group_feature, inverse_of: :group, class_name: 'Groups::FeatureSetting'
delegate :prevent_sharing_groups_outside_hierarchy, :new_user_signups_cap, :setup_for_company, :jobs_to_be_done, :seat_control, to: :namespace_settings
delegate :runner_token_expiration_interval, :runner_token_expiration_interval=, :runner_token_expiration_interval_human_readable, :runner_token_expiration_interval_human_readable=, to: :namespace_settings, allow_nil: true
delegate :subgroup_runner_token_expiration_interval, :subgroup_runner_token_expiration_interval=, :subgroup_runner_token_expiration_interval_human_readable, :subgroup_runner_token_expiration_interval_human_readable=, to: :namespace_settings, allow_nil: true
delegate :project_runner_token_expiration_interval, :project_runner_token_expiration_interval=, :project_runner_token_expiration_interval_human_readable, :project_runner_token_expiration_interval_human_readable=, to: :namespace_settings, allow_nil: true
delegate :force_pages_access_control, :force_pages_access_control=, to: :namespace_settings, allow_nil: true
delegate :model_prompt_cache_enabled, :model_prompt_cache_enabled=, to: :namespace_settings, allow_nil: true
delegate :require_dpop_for_manage_api_endpoints, :require_dpop_for_manage_api_endpoints=, to: :namespace_settings
delegate :require_dpop_for_manage_api_endpoints?, to: :namespace_settings
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :group_feature, update_only: true
validate :visibility_level_allowed_by_projects
validate :visibility_level_allowed_by_sub_groups
validate :visibility_level_allowed_by_organization, if: :should_validate_visibility_level?
validate :visibility_level_allowed_by_parent
validate :two_factor_authentication_allowed
validates :variables, nested_attributes_duplicates: { scope: :environment_scope }
validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :name,
html_safety: true,
format: {
with: Gitlab::Regex.group_name_regex,
message: Gitlab::Regex.group_name_regex_message
},
if: :name_changed?
validates :group_feature, presence: true
validate :top_level_group_name_not_assigned_to_pages_unique_domain, if: :path_changed?
add_authentication_token_field :runners_token,
encrypted: :required,
format_with_prefix: :runners_token_prefix,
require_prefix_for_validation: true
after_create :post_create_hook
after_create -> { create_or_load_association(:group_feature) }
after_update :path_changed_hook, if: :saved_change_to_path?
after_destroy :post_destroy_hook
after_commit :update_two_factor_requirement
scope :with_users, -> { includes(:users) }
scope :active, -> do
non_archived.not_aimed_for_deletion
end
scope :inactive, -> do
joins(:namespace_settings)
.left_joins(:deletion_schedule)
.where(<<~SQL)
#{reflections['namespace_settings'].table_name}.archived = TRUE
OR #{reflections['deletion_schedule'].table_name}.#{reflections['deletion_schedule'].foreign_key} IS NOT NULL
SQL
end
scope :with_non_archived_projects, -> { includes(:non_archived_projects) }
scope :with_non_invite_group_members, -> { includes(:non_invite_group_members) }
scope :with_request_group_members, -> { includes(:request_group_members) }
scope :by_id, ->(groups) { where(id: groups) }
scope :by_ids_or_paths, ->(ids, paths) do
return by_id(ids) unless paths.present?
ids_by_full_path = Route
.for_routable_type(Namespace.name)
.where('LOWER(routes.path) IN (?)', paths.map(&:downcase))
.allow_cross_joins_across_databases(url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
.select(:namespace_id)
Group.from_union([by_id(ids), by_id(ids_by_full_path), where('LOWER(path) IN (?)', paths.map(&:downcase))])
end
scope :excluding_groups, ->(groups) { where.not(id: groups) }
scope :by_visibility_level, ->(visibility) do
where(visibility_level: Gitlab::VisibilityLevel.level_value(visibility)) if visibility.present?
end
scope :for_authorized_group_members, ->(user_ids) do
joins(:group_members)
.where(members: { user_id: user_ids })
.where("access_level >= ?", Gitlab::Access::GUEST)
end
scope :for_authorized_project_members, ->(user_ids) do
joins(projects: :project_authorizations)
.where(project_authorizations: { user_id: user_ids })
end
scope :with_project_creation_levels, ->(project_creation_levels) do
where(project_creation_level: project_creation_levels)
end
scope :excluding_restricted_visibility_levels_for_user, ->(user) do
return all if user.can_admin_all_resources?
levels = Array.wrap(Gitlab::CurrentSettings.restricted_visibility_levels).sort
case levels
when [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::PUBLIC],
[Gitlab::VisibilityLevel::PRIVATE]
where.not(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
when [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL]
where.not(visibility_level: [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL])
when Gitlab::VisibilityLevel.values
none
else
all
end
end
scope :project_creation_allowed, ->(user) do
project_creation_levels_for_user = project_creation_levels_for_user(user)
with_project_creation_levels(project_creation_levels_for_user)
.excluding_restricted_visibility_levels_for_user(user)
end
scope :shared_into_ancestors, ->(group) do
joins(:shared_group_links)
.where(group_group_links: { shared_group_id: group.self_and_ancestors })
end
# Returns all groups that are shared with the given group (see :shared_with_group)
# and all descendents of the given group
# returns none if the given group is nil
scope :descendants_with_shared_with_groups, ->(group) do
return none if group.nil?
descendants_query = group.descendants.select(:id)
# since we're only interested in ids, we query GroupGroupLink directly instead of using :shared_with_group
# to avoid an extra JOIN in the resulting query
shared_groups_query = GroupGroupLink
.where(shared_group_id: group.id)
.select('shared_with_group_id AS id')
combined_query = Group
.from_union(descendants_query, shared_groups_query, alias_as: :combined)
.unscope(where: :type)
.select(:id)
id_in(combined_query)
end
# 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
#
# It's a replacement for `public_or_visible_to_user` that correctly
# supports subgroup permissions
scope :accessible_to_user, ->(user) do
if user
Preloaders::GroupPolicyPreloader.new(self, user).execute
select { |group| user.can?(:read_group, group) }
else
public_to_user
end
end
# .sorted_by_similarity_desc can generate poorly performing queries.
# Only apply this scope in combination with other filters, ideally on sets of
# less than 100,000 records.
scope :sorted_by_similarity_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 }
])
order = Gitlab::Pagination::Keyset::Order.build(
[
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'similarity',
column_expression: order_expression,
order_expression: order_expression.desc,
order_direction: :desc,
add_to_projections: true
),
# Tie-breaker for if two results have the same similarity score.
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id',
order_expression: Namespace.arel_table['id'].asc
)
])
order.apply_cursor_conditions(reorder(order))
end
scope :order_path_asc, -> { reorder(self.arel_table['path'].asc) }
scope :order_path_desc, -> { reorder(self.arel_table['path'].desc) }
scope :in_organization, ->(organization) { where(organization: organization) }
scope :by_min_access_level, ->(user, access_level) { joins(:group_members).where(members: { user: user }).where('members.access_level >= ?', access_level) }
class << self
def sort_by_attribute(method)
case method.to_s
when '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')
when 'path_asc'
order_path_asc
when 'path_desc'
order_path_desc
else
order_by(method)
end
end
# 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)
end
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')
.where(project_namespace: { share_with_group_lock: false })
.select("projects.id AS project_id", "LEAST(project_group_links.group_access, members.access_level) AS access_level")
else
super
end
end
def without_integration(integration)
integrations = Integration
.select('1')
.where("#{Integration.table_name}.group_id = namespaces.id")
.where(type: integration.type)
where('NOT EXISTS (?)', integrations)
end
def groups_user_can(groups, user, action, same_root: false)
DeclarativePolicy.user_scope do
groups.select { |group| Ability.allowed?(user, action, group) }
end
end
# This method can be used only if all groups have the same top-level
# group
def preset_root_ancestor_for(groups)
return groups if groups.size < 2
root = groups.first.root_ancestor
groups.drop(1).each { |group| group.root_ancestor = root }
end
# Returns the ids of the passed group models where the `emails_enabled`
# column is set to false anywhere in the ancestor hierarchy.
def ids_with_disabled_email(groups)
inner_groups = Group.where('id = namespaces_with_emails_disabled.id')
inner_query = inner_groups
.self_and_ancestors
.joins(:namespace_settings)
.where(namespace_settings: { emails_enabled: false })
.select('1')
.limit(1)
group_ids = Namespace
.from('(SELECT * FROM namespaces) as namespaces_with_emails_disabled')
.where(namespaces_with_emails_disabled: { id: groups })
.where('EXISTS (?)', inner_query)
.pluck(:id)
Set.new(group_ids)
end
def get_ids_by_ids_or_paths(ids, paths)
by_ids_or_paths(ids, paths).pluck(:id)
end
def descendant_groups_counts
left_joins(:children).group(:id).count(:children_namespaces)
end
def projects_counts
left_joins(:non_archived_projects).group(:id).count(:projects)
end
def group_members_counts
left_joins(:group_members).group(:id).count(:members)
end
def with_api_scopes
preload(:namespace_settings, :group_feature, :parent, :deletion_schedule)
end
# Handle project creation permissions based on application setting and group setting. The `default_project_creation`
# application setting is the default value and can be overridden by the `project_creation_level` group setting.
# `nil` value of namespaces.project_creation_level` means that allowed creation level has not been explicitly set by
# the group owner and is a placeholder value for inheriting the value from the ApplicationSetting.
def project_creation_levels_for_user(user)
project_creation_allowed_on_levels = [
::Gitlab::Access::DEVELOPER_PROJECT_ACCESS,
::Gitlab::Access::MAINTAINER_PROJECT_ACCESS,
::Gitlab::Access::OWNER_PROJECT_ACCESS,
nil
]
if user.can_admin_all_resources?
project_creation_allowed_on_levels << ::Gitlab::Access::ADMINISTRATOR_PROJECT_ACCESS
end
default_project_creation = ::Gitlab::CurrentSettings.default_project_creation
prevent_project_creation_by_default = prevent_project_creation?(user, default_project_creation)
# Remove nil (i.e. inherited `default_project_creation`) when the application setting is:
# 1. NO_ONE_PROJECT_ACCESS
# 2. ADMINISTRATOR_PROJECT_ACCESS and the user is not an admin
#
# To prevent showing groups in the namespaces dropdown on the project creation page that have no explicit group
# setting for `project_creation_level`.
project_creation_allowed_on_levels.delete(nil) if prevent_project_creation_by_default
project_creation_allowed_on_levels
end
def prevent_project_creation?(user, project_creation_setting)
return true if project_creation_setting == ::Gitlab::Access::NO_ONE_PROJECT_ACCESS
return false if user.can_admin_all_resources?
project_creation_setting == ::Gitlab::Access::ADMINISTRATOR_PROJECT_ACCESS
end
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.arel.as('authorized')
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.
def notification_settings(hierarchy_order: nil)
source_type = self.class.base_class.name
settings = NotificationSetting.where(source_type: source_type, source_id: self_and_ancestors_ids)
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)
end
def packages_feature_enabled?
::Gitlab.config.packages.enabled
end
def dependency_proxy_feature_available?
::Gitlab.config.dependency_proxy.enabled
end
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
def dependency_proxy_image_prefix
# The namespace path can include uppercase letters, which
# Docker doesn't allow. The proxy expects it to be downcased.
url = "#{Gitlab::Routing.url_helpers.group_url(self).downcase}#{DependencyProxy::URL_SUFFIX}"
# Docker images do not include the protocol
url.partition('//').last
end
def human_name
full_name
end
def to_human_reference(from = nil)
return unless cross_namespace_reference?(from)
human_name
end
def visibility_level_allowed_by_organization?(level = self.visibility_level)
return true unless organization
level <= organization.visibility_level
end
def visibility_level_allowed_by_parent?(level = self.visibility_level)
return true unless parent_id && parent_id.nonzero?
level <= parent.visibility_level
end
def visibility_level_allowed_by_projects?(level = self.visibility_level)
!projects.not_aimed_for_deletion.where('visibility_level > ?', level).exists?
end
def visibility_level_allowed_by_sub_groups?(level = self.visibility_level)
!children.where('visibility_level > ?', level).exists?
end
def visibility_level_allowed?(level = self.visibility_level)
visibility_level_allowed_by_organization?(level) &&
visibility_level_allowed_by_parent?(level) &&
visibility_level_allowed_by_projects?(level) &&
visibility_level_allowed_by_sub_groups?(level)
end
def lfs_enabled?
return false unless Gitlab.config.lfs.enabled
return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil?
self[:lfs_enabled]
end
def owned_by?(user)
return false unless user
non_invite_owner_members.exists?(user: user)
end
def add_members(users, access_level, current_user: nil, expires_at: nil)
Members::Groups::CreatorService.add_members( # rubocop:disable CodeReuse/ServiceClass
self,
users,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
def add_member(user, access_level, ...)
Members::Groups::CreatorService.add_member(self, user, access_level, ...) # rubocop:disable CodeReuse/ServiceClass
end
def add_guest(user, current_user = nil)
add_member(user, :guest, current_user: current_user)
end
def add_planner(user, current_user = nil)
add_member(user, :planner, current_user: current_user)
end
def add_reporter(user, current_user = nil)
add_member(user, :reporter, current_user: current_user)
end
def add_developer(user, current_user = nil)
add_member(user, :developer, current_user: current_user)
end
def add_maintainer(user, current_user = nil)
add_member(user, :maintainer, current_user: current_user)
end
def add_owner(user, current_user = nil)
add_member(user, :owner, current_user: current_user)
end
def member?(user, min_access_level = Gitlab::Access::GUEST)
return false unless user
max_member_access_for_user(user) >= min_access_level
end
def has_owner?(user)
return false unless user
members_with_parents.all_owners.exists?(user_id: user)
end
def blocked_owners
members.blocked.where(access_level: Gitlab::Access::OWNER)
end
def has_maintainer?(user)
return false unless user
members_with_parents.maintainers.exists?(user_id: user)
end
def has_container_repository_including_subgroups?
::ContainerRepository.for_group_and_its_subgroups(self).exists?
end
# Check if user is a last owner of the group.
# Excludes non-direct owners for top-level group
# Excludes project_bots
def last_owner?(user)
return false unless user
all_owners = member_owners_excluding_project_bots
all_owners.size == 1 && all_owners.first.user_id == user.id
end
# Excludes non-direct owners for top-level group
# Excludes project_bots
def member_owners_excluding_project_bots
members_from_hiearchy = if root?
members.non_minimal_access.without_invites_and_requests
else
members_with_parents(only_active_users: false)
end
owners = []
members_from_hiearchy.all_owners.non_invite.each_batch do |relation|
owners += relation.preload(:user, :source).load.reject do |member|
member.user.nil? || member.user.project_bot?
end
end
owners
end
def ldap_synced?
false
end
def post_create_hook
Gitlab::AppLogger.info("Group \"#{name}\" was created")
system_hook_service.execute_hooks_for(self, :create)
end
def post_destroy_hook
Gitlab::AppLogger.info("Group \"#{name}\" was removed")
system_hook_service.execute_hooks_for(self, :destroy)
end
# rubocop: disable CodeReuse/ServiceClass
def system_hook_service
SystemHooksService.new
end
# rubocop: enable CodeReuse/ServiceClass
# rubocop: disable CodeReuse/ServiceClass
def refresh_members_authorized_projects(
priority: UserProjectAccessChangedService::HIGH_PRIORITY,
direct_members_only: false
)
user_ids = if direct_members_only
users_ids_of_direct_members
else
user_ids_for_project_authorizations
end
UserProjectAccessChangedService
.new(user_ids)
.execute(priority: priority)
end
# rubocop: enable CodeReuse/ServiceClass
def users_ids_of_direct_members
direct_members.pluck_user_ids
end
def user_ids_for_project_authorizations
members_with_parents.pluck(Arel.sql('DISTINCT members.user_id'))
end
def self_and_hierarchy_intersecting_with_user_groups(user)
user_groups = GroupsFinder.new(user).execute.unscope(:order)
self_and_hierarchy.unscope(:order).where(id: user_groups)
end
def self_and_ancestors_ids
strong_memoize(:self_and_ancestors_ids) do
self_and_ancestors.pluck(:id)
end
end
def self_and_descendants_ids
strong_memoize(:self_and_descendants_ids) do
self_and_descendants.pluck(:id)
end
end
def self_and_ancestors_asc
self_and_ancestors(hierarchy_order: :asc)
end
strong_memoize_attr :self_and_ancestors_asc
# Only for direct and not requested members with higher access level than MIMIMAL_ACCESS
# It returns true for non-active users
def has_user?(user)
return false unless user
group_members.non_invite.exists?(user: user)
end
def direct_members
GroupMember.active_without_invites_and_requests
.non_minimal_access
.where(source_id: id)
end
def authorizable_members_with_parents
Members::MembersWithParents.new(self).all_members.authorizable
end
def members_with_parents(only_active_users: true)
Members::MembersWithParents
.new(self)
.members(active_users: only_active_users)
end
def members_from_self_and_ancestors_with_effective_access_level
members_with_parents.select([:user_id, 'MAX(access_level) AS access_level'])
.group(:user_id)
end
def members_with_descendants
GroupMember
.active_without_invites_and_requests
.where(source_id: self_and_descendants.reorder(nil).select(:id))
end
# Returns all members that are part of the group, it's subgroups, and ancestor groups
def hierarchy_members
GroupMember
.active_without_invites_and_requests
.where(source_id: self_and_hierarchy.reorder(nil).select(:id))
end
def hierarchy_members_with_inactive
GroupMember
.non_request
.non_invite
.where(source_id: self_and_hierarchy.reorder(nil).select(:id))
end
def descendant_project_members_with_inactive
ProjectMember
.with_source_id(all_projects)
.non_request
.non_invite
end
def users_with_descendants
User
.where(id: members_with_descendants.select(:user_id))
.reorder(nil)
end
def users_count
members.count
end
# Return the highest access level for a user
#
# A special case is handled here when the user is a GitLab admin
# which implies it has "OWNER" access everywhere, but should not
# officially appear as a member of a group unless specifically added to it
#
# @param user [User]
# @param only_concrete_membership [Bool] whether require admin concrete membership status
def max_member_access_for_user(user, only_concrete_membership: false)
return GroupMember::NO_ACCESS unless user
unless only_concrete_membership
return GroupMember::OWNER if user.can_admin_all_resources?
return GroupMember::OWNER if user.can_admin_organization?(organization)
end
max_member_access(user)
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
def 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
def highest_group_member(user)
GroupMember
.where(source_id: self_and_ancestors_ids, user_id: user.id)
.non_request
.order(:access_level)
.last
end
def bots
users.project_bot
end
def related_group_ids
[id,
*ancestors.pluck(:id),
*shared_with_group_links.pluck(:shared_with_group_id)]
end
def hashed_storage?(_feature)
false
end
def refresh_project_authorizations
refresh_members_authorized_projects
end
# 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
return unless allow_runner_registration_token?
ensure_runners_token!
end
def project_creation_level
super || ::Gitlab::CurrentSettings.default_project_creation
end
def subgroup_creation_level
super || ::Gitlab::Access::OWNER_SUBGROUP_ACCESS
end
def access_request_approvers_to_be_notified
members.owners.connected_to_user.order_recent_sign_in.limit(Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
end
def membership_locked?
false # to support project and group calling this as 'source'
end
def supports_events?
false
end
def import_export_upload_by_user(user)
import_export_uploads.find_by(user_id: user.id)
end
def export_file_exists?(user)
import_export_upload_by_user(user)&.export_file_exists?
end
def export_file(user)
import_export_upload_by_user(user)&.export_file
end
def export_archive_exists?(user)
import_export_upload_by_user(user)&.export_archive_exists?
end
def execute_hooks(data, hooks_scope)
# NOOP
# TODO: group hooks https://gitlab.com/gitlab-org/gitlab/-/issues/216904
end
def find_or_initialize_integration(integration)
Integration.find_or_initialize_non_project_specific_integration(integration, group_id: id)
end
def execute_integrations(data, hooks_scope)
integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend
integration.async_execute(data)
end
end
def preload_shared_group_links
ActiveRecord::Associations::Preloader.new(
records: [self],
associations: { shared_with_group_links: [shared_with_group: :route] }
).call
end
def first_owner
first_owner_member = all_group_members.all_owners.order(:user_id).first
first_owner_member&.user || parent&.first_owner || owner
end
def default_branch_name
namespace_settings&.default_branch_name
end
def access_level_roles
GroupMember.access_level_roles
end
def access_level_values
access_level_roles.values
end
def parent_allows_two_factor_authentication?
return true unless has_parent?
ancestor_settings = ancestors.find_top_level.namespace_settings
ancestor_settings.allow_mfa_for_subgroups
end
def has_project_with_service_desk_enabled?
::ServiceDesk.supported? && all_projects.service_desk_enabled.exists?
end
strong_memoize_attr :has_project_with_service_desk_enabled?
# rubocop: disable CodeReuse/ServiceClass
def open_issues_count(current_user = nil)
Groups::OpenIssuesCountService.new(self, current_user).count
end
# rubocop: enable CodeReuse/ServiceClass
# rubocop: disable CodeReuse/ServiceClass
def open_merge_requests_count(current_user = nil)
Groups::MergeRequestsCountService.new(self, current_user).count
end
# rubocop: enable CodeReuse/ServiceClass
def timelogs
Timelog.in_group(self)
end
def dependency_proxy_image_ttl_policy
super || build_dependency_proxy_image_ttl_policy
end
def dependency_proxy_setting
super || build_dependency_proxy_setting
end
def group_feature
super || build_group_feature
end
def crm_enabled?
crm_settings.nil? || crm_settings.enabled?
end
def shared_with_group_links_visible_to_user(user)
shared_with_group_links.preload_shared_with_groups.filter { |link| Ability.allowed?(user, :read_group, link.shared_with_group) }
end
def enforced_runner_token_expiration_interval
all_parent_groups = Gitlab::ObjectHierarchy.new(Group.where(id: id)).ancestors
all_group_settings = NamespaceSetting.where(namespace_id: all_parent_groups)
group_interval = all_group_settings.where.not(subgroup_runner_token_expiration_interval: nil).minimum(:subgroup_runner_token_expiration_interval)&.seconds
[
Gitlab::CurrentSettings.group_runner_token_expiration_interval&.seconds,
group_interval
].compact.min
end
def work_items_feature_flag_enabled?
feature_flag_enabled_for_self_or_ancestor?(:work_items)
end
def work_items_beta_feature_flag_enabled?
feature_flag_enabled_for_self_or_ancestor?(:work_items_beta, type: :beta)
end
def work_items_alpha_feature_flag_enabled?
feature_flag_enabled_for_self_or_ancestor?(:work_items_alpha)
end
def work_item_status_feature_available?
feature_flag_enabled_for_self_or_ancestor?(:work_item_status_feature_flag, type: :wip) &&
licensed_feature_available?(:work_item_status)
end
def continue_indented_text_feature_flag_enabled?
feature_flag_enabled_for_self_or_ancestor?(:continue_indented_text, type: :wip)
end
def glql_integration_feature_flag_enabled?
feature_flag_enabled_for_self_or_ancestor?(:glql_integration)
end
def glql_load_on_click_feature_flag_enabled?
feature_flag_enabled_for_self_or_ancestor?(:glql_load_on_click)
end
# Note: this method is overridden in EE to check the work_item_epics feature flag which also enables this feature
def namespace_work_items_enabled?
::Feature.enabled?(:namespace_level_work_items, self, type: :development)
end
def create_group_level_work_items_feature_flag_enabled?
::Feature.enabled?(:create_group_level_work_items, self, type: :wip)
end
def supports_lock_on_merge?
feature_flag_enabled_for_self_or_ancestor?(:enforce_locked_labels_on_merge, type: :ops)
end
def usage_quotas_enabled?
root?
end
def supports_saved_replies?
false
end
# Check for enabled features, similar to `Project#feature_available?`
# NOTE: We still want to keep this after removing `Namespace#feature_available?`.
override :feature_available?
def feature_available?(feature, user = nil)
# when we check the :issues feature at group level we need to check the `epics` license feature instead
feature = feature == :issues ? :epics : feature
if ::Groups::FeatureSetting.available_features.include?(feature)
group_feature.feature_available?(feature, user) # rubocop:disable Gitlab/FeatureAvailableUsage
else
super
end
end
def gitlab_deploy_token
strong_memoize(:gitlab_deploy_token) do
deploy_tokens.gitlab_deploy_token
end
end
def packages_policy_subject
::Packages::Policies::Group.new(self)
end
def dependency_proxy_for_containers_policy_subject
::Packages::Policies::DependencyProxy::Group.new(self)
end
def update_two_factor_requirement_for_members
hierarchy_members.find_each(&:update_two_factor_requirement)
end
def readme_project
projects.find_by(path: README_PROJECT_PATH)
end
strong_memoize_attr :readme_project
def notification_group
self
end
def group_readme
readme_project&.repository&.readme
end
strong_memoize_attr :group_readme
def hook_attrs
{
group_name: name,
group_path: path,
group_id: id,
full_path: full_path
}
end
def crm_group
Group.id_in_ordered(traversal_ids.reverse)
.joins(:crm_settings)
.where.not(crm_settings: { source_group_id: nil })
.first&.crm_settings&.source_group || root_ancestor
end
strong_memoize_attr :crm_group
def crm_group?
return true if root? && crm_settings&.source_group_id.nil?
crm_targets.present?
end
strong_memoize_attr :crm_group?
def has_issues_with_contacts?
CustomerRelations::IssueContact.joins(:issue).where(issue: { project_id: Project.where(namespace_id: self_and_descendant_ids) }).exists?
end
def delete_contacts
CustomerRelations::Contact.where(group_id: id).delete_all
end
def delete_organizations
CustomerRelations::Organization.where(group_id: id).delete_all
end
def cluster_agents
::Clusters::Agent.for_projects(all_projects)
end
def active?
self_and_ancestors.inactive.none?
end
def pending_delete?
return false unless deletion_schedule
deletion_schedule.marked_for_deletion_on.future?
end
private
def feature_flag_enabled_for_self_or_ancestor?(feature_flag, type: :development)
actors = [root_ancestor]
actors << self if root_ancestor != self
actors.any? do |actor|
::Feature.enabled?(feature_flag, actor, type: type)
end
end
def max_member_access(user)
Gitlab::SafeRequestLoader.execute(
resource_key: max_member_access_for_resource_key(User),
resource_ids: [user.id],
default_value: Gitlab::Access::NO_ACCESS
) do |_|
next {} unless user.active?
members_with_parents(only_active_users: false).where(user_id: user.id).group(:user_id).maximum(:access_level)
end.fetch(user.id)
end
def update_two_factor_requirement
return unless saved_change_to_require_two_factor_authentication? || saved_change_to_two_factor_grace_period?
Groups::UpdateTwoFactorRequirementForMembersWorker.perform_async(self.id)
end
def path_changed_hook
system_hook_service.execute_hooks_for(self, :rename)
end
def should_validate_visibility_level?
new_record? || changes.has_key?(:visibility_level)
end
def visibility_level_allowed_by_organization
return if visibility_level_allowed_by_organization?
errors.add(:visibility_level, "#{visibility} is not allowed since the organization has a #{organization.visibility} visibility.")
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
def two_factor_authentication_allowed
return unless has_parent?
return unless require_two_factor_authentication
return if parent_allows_two_factor_authentication?
errors.add(:require_two_factor_authentication, _('is forbidden by a top-level group'))
end
def runners_token_prefix
RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
end
def top_level_group_name_not_assigned_to_pages_unique_domain
return unless parent_id.nil?
return unless ProjectSetting.unique_domain_exists?(path)
# We cannot disclose the Pages unique domain, hence returning generic error message
errors.add(:path, _('has already been taken'))
end
# Overriding of Namespaces::AdjournedDeletable method
override :all_scheduled_for_deletion_in_hierarchy_chain
def all_scheduled_for_deletion_in_hierarchy_chain
self_and_ancestors(hierarchy_order: :asc).joins(:deletion_schedule)
end
end
Group.prepend_mod_with('Group')