0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 13:08:40 +08:00
discourse/app/services/access_control_list_manager.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.3 KiB
Ruby
Vendored

# frozen_string_literal: true
class AccessControlListManager
include Service::Base
params do
attribute :target
attribute :flattened_acl, :array
attribute :owner, :string
validates :target, presence: true
validates :owner, presence: true, length: { maximum: 100 }
end
# NOTE (martin): Maybe we need some way of defining a policy here to see
# if the guardian has the permission to change ACLs for the target?
# For now, we assume that the caller of this service has already done the
# necessary guardian checks (e.g. can_manage_board? for a kanban board)
model :previous_permissions, optional: true
model :flattened_acl_with_mandatory, optional: true
policy :has_no_banned_acl
policy :has_at_least_one_acl
transaction do
step :destroy_acls
step :insert_acls
step :log_permission_changes
end
step :reload_target
private
def fetch_previous_permissions(params:)
AccessControlList.where(target: params.target)
end
def destroy_acls(previous_permissions:, params:)
@context[:previous_permissions] = previous_permissions.target_acl(
params.target,
).permission_lookup
previous_permissions.destroy_all
end
def fetch_flattened_acl_with_mandatory(params:)
AccessControlList.inject_mandatory_acl(params.flattened_acl, params.target)
end
def has_no_banned_acl(params:, flattened_acl_with_mandatory:)
return true if !params.target.class.has_banned_acl?
flattened_acl_with_mandatory.none? { |acl| params.target.class.acl_is_banned?(acl) }
end
def has_at_least_one_acl(flattened_acl_with_mandatory:)
flattened_acl_with_mandatory.any?
end
def insert_acls(params:, flattened_acl_with_mandatory:)
bulk_insert_list =
AccessControlList.expand_list_for_bulk_insert(
flattened_acl_with_mandatory,
params.target,
params.owner,
)
@context[:new_permissions] = Acl::Target.new(flattened_acl_with_mandatory).permission_lookup
AccessControlList.insert_all!(bulk_insert_list)
end
def log_permission_changes(guardian:, previous_permissions:, new_permissions:, params:)
StaffActionLogger.new(guardian.user).log_access_control_list_permission_change(
params.target,
previous_permissions,
new_permissions,
)
end
def reload_target(params:)
params.target.reload
end
end