.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 and User#clear_acls enqueue Jobs::CleanupAclsForDeleted after a grantee is destroyed. The job removes the deleted group id from allowed_group_ids and/or the deleted user id from allowed_user_ids, updates updated_at, and deletes ACL rows only when both arrays are empty.
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, flattened_list, preload_allowed, lookup objects, matching scopes, and cleanup jobs now handle both group and user ACL rows.DAccessControl remains group-first: it does not yet provide user picking or complete user ACL editing, even though backend rows can contain allowed_user_ids.flattened_list skips missing groups, missing users, and unknown target classes defensively. Validate group and user IDs at the service/contract boundary where user input enters; do not rely on read-path skipping as data hygiene.