.skills/discourse-acl-authoring/references/manager-and-plugin-integration.md
Use this reference when writing ACL rows, creating an ACL-backed target model, registering plugin targets, or serializing ACLs from controllers/services.
AccessControlListManager is the standard write path for replacing a target's ACLs.
AccessControlListManager.call(
guardian:,
params: {
target: board,
flattened_acl: flattened_acl,
owner: DiscourseKanban::PLUGIN_NAME,
},
) do |result|
on_success do |previous_permissions:, new_permissions:|
# optional history/event work
end
on_failure do
fail!(I18n.t("plugin.target.errors.acl_update_failed"))
end
end
Behavior:
target and ownerflattened_aclhas_no_banned_acl if any final flattened ACL entry matches the target class's banned_aclhas_at_least_one_acl if the final ACL list is emptyAclTarget#permission_acl is rebuilt on the caller's instanceImportant: the manager destructively replaces target ACL rows and does not currently authorize the actor itself. Authorize before calling it.
Service::Base, follow .skills/discourse-service-authoring.For new target records with mandatory ACLs:
AccessControlListManager with flattened_acl: raw["acl"] || [].This ensures mandatory ACL rows are inserted for API callers that omit acl.
For update flows, call the manager when ACL params are part of the contract. If ACL updates are optional, distinguish "key omitted" from "key present with empty array" deliberately.
Plugin target classes must include AclTarget and be registered after initialization:
after_initialize do
DiscoursePluginRegistry.register_acl_target_class(DiscourseKanban::Board, self)
end
The registry is declared as acl_target_classes in DiscoursePluginRegistry. Site#access_control combines loaded core AclTarget.target_classes with plugin-registered classes and exposes mandatory ACLs by acl_target_key.
String registrations are allowed and resolved with safe_constantize; class registrations are preferred when the constant is available. Unresolved strings and registered classes that do not respond to has_mandatory_acl? are skipped from site metadata, with unresolved strings logged for debugging.
SiteSerializer includes access_control.
Shape:
{
mandatory_acl: {
"DiscourseKanban::Board" => [
{ type: :group, id: Group::AUTO_GROUPS[:admins], permission: "manage" },
],
},
banned_acl: {
"DiscourseKanban::Board" => [
{ type: :group, id: Group::AUTO_GROUPS[:anonymous_users], permission: "edit" },
],
},
}
The key is target_class.acl_target_key, which defaults to the class name. If a frontend passes @aclTarget, it must match this key exactly.
mandatory_acl and banned_acl are both always present in the serialized access_control payload. Each map only includes target classes that define non-empty metadata for that ACL type.
Use AccessControlList.where(target: target).flattened_list when returning ACLs to a client that edits them.
Only include ACL details for actors who can manage that specific target. Avoid using a broad global capability when per-target ACL management exists.
def payload(target, include_acl:)
{
id: target.id,
can_manage: guardian.can_manage_target?(target),
acl: include_acl ? AccessControlList.where(target: target).flattened_list : nil,
}
end
Use TargetClass.with_acl_permission(guardian, permission) or TargetClass.with_any_acl_permissions(guardian, permissions) for index scopes where the user should see targets reachable through ACLs. Use guardian.target_ids_with_any_acl_permissions(TargetClass, permissions) only when the id array is the cleaner interface for a custom query.
Group#clear_acls enqueues Jobs::CleanupAclsForDeleted after a group is destroyed. The job removes the deleted group id from allowed_group_ids, updates updated_at, and deletes ACL rows with no remaining allowed_group_ids or allowed_user_ids.
User cleanup is not complete yet. Do not document deleted-user ACL cleanup as supported until the job handles user_id.
When migrating legacy permission columns into ACLs:
mandatory_values.Group::AUTO_GROUPS values or stable numeric IDs only when loading the Rails constants is unsafe in the migration context.AccessControlList.expand_list_for_bulk_insert is group-first. It does not fully handle { type: "user" } authoring yet.preload_allowed, expand_list, cleanup, and DAccessControl are still group-first even though lower-level matching scopes understand allowed_user_ids.flattened_list skips missing groups and unknown target classes defensively. Validate group IDs at the service/contract boundary where user input enters; do not rely on read-path skipping as data hygiene.