0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 13:08:40 +08:00
discourse/app/models/concerns/acl_target.rb
Martin Brennan e590d2a2d5
DEV: Add banned_acl concept to AccessControlList (#41254)
Introduces a server-side definition of banned_acl,
similar to mandatory_acl from 5823e4e3b2

This allows AclTarget implementing classes to define
which ACLs cannot be used for certain types. For example:

```
def self.banned_acl
  [{ type: :group, id: Group::AUTO_GROUPS[:anonymous_users], permission: "edit" }]
end
```

This prevents anonymous users group from being able to have the Edit
permission on the target, which is practical because the anonymous
user is not logged in and generally cannot create/edit anything.

This restriction is passed to the DAccessControl component via the Site
serializer, same as mandatory_acl, and is used to prevent the user from
selecting the banned ACLs in the UI.

Then, server-side this rule is enforced in `AccessControlListManager`
service as a policy.

**Before**

<img width="971" height="287" alt="image"
src="https://github.com/user-attachments/assets/af4ace0f-7b11-4005-8344-774d554693f0"
/>


**After**

<img width="914" height="194" alt="image"
src="https://github.com/user-attachments/assets/b756b976-f6e6-4331-b27d-c9a2d13997aa"
/>
2026-06-29 16:22:30 +10:00

81 lines
2.1 KiB
Ruby
Vendored

# frozen_string_literal: true
module AclTarget
extend ActiveSupport::Concern
def self.acl_matches?(acl_a, acl_b)
acl_a[:type].to_sym == acl_b[:type].to_sym && acl_a[:id] == acl_b[:id] &&
acl_a[:permission].to_s == acl_b[:permission].to_s
end
def self.target_classes
loaded_target_classes
end
def self.loaded_target_classes
@loaded_target_classes ||= []
end
included do
AclTarget.loaded_target_classes << self if !AclTarget.loaded_target_classes.include?(self)
has_many :access_control_lists,
as: :target,
class_name: "AccessControlList",
dependent: :destroy
scope :with_acl_permission,
->(guardian, permission) do
where(
id:
AccessControlList
.matching_user(guardian.user)
.where(target_type: polymorphic_name, permission: permission)
.select(:target_id),
)
end
scope :with_any_acl_permissions,
->(guardian, permissions) do
where(
id:
AccessControlList
.matching_user(guardian.user)
.where(target_type: polymorphic_name, permission: Array.wrap(permissions))
.select(:target_id),
)
end
def reload(options = nil)
@permission_acl = nil
super
end
end
def permission_acl
@permission_acl ||= AccessControlList.where(target: self).target_acl(self)
end
class_methods do
def acl_target_key
name
end
def has_mandatory_acl?
defined?(mandatory_acl).present? && mandatory_acl.length.positive?
end
def acl_is_mandatory?(acl)
has_mandatory_acl? &&
mandatory_acl.any? { |mandatory_acl| AclTarget.acl_matches?(acl, mandatory_acl) }
end
def has_banned_acl?
defined?(banned_acl).present? && banned_acl.length.positive?
end
def acl_is_banned?(acl)
has_banned_acl? && banned_acl.any? { |banned_acl| AclTarget.acl_matches?(acl, banned_acl) }
end
end
end