ACL Management — integration guide#
The existing page in this section covers PalmyraPermissionEvaluator. This page is the how-do-I-stand-up-ACL-from-scratch walk-through — the schema, the AclController endpoints that ship with the extension, and how GroupHandler joins users to groups.
Step 1 — add the dependency#
implementation 'com.palmyralabs.palmyra.extn:palmyra-dbacl-mgmt:1.4.4'Maven group .extn; Java package com.palmyralabs.palmyra.ext.aclmgmt.
Import AclMgmtConfiguration alongside PalmyraSpringConfiguration in your @SpringBootApplication:
@SpringBootApplication
@Import({ PalmyraSpringConfiguration.class, AclMgmtConfiguration.class })
public class AppMain { ... }Step 2 — the ACL schema#
The extension manages five tables plus a user table you already have. Provision them via your migration tooling (Flyway / Liquibase / DevBootstrap SQL).
| Table | Managed by | Purpose |
|---|---|---|
xpm_user |
JPA entity (UserEntity) |
Users; augmented with random / salt / lock_expire columns if you also use palmyra-dbpwd-mgmt |
xpm_group |
JPA entity (GroupEntity) |
Groups |
xpm_acl_user |
JPA (@ManyToMany join on GroupEntity.users) |
User ↔ group membership |
xpm_menu |
JPA entity (MenuEntity) |
Side-menu tree (hierarchical via parent self-FK) |
xpm_acl_menu |
JPA (@ManyToMany join) |
Which groups can see which menu entries; carries a mask column |
xpm_acl_class |
JDBC only (no JPA entity) | Logical permission classes — one row per @Permission(value="...") name |
xpm_acl_permission |
JDBC only | Per-class operation codes (e.g. CUD, RS, Unlock) |
xpm_acl_group_permission |
JDBC only | group × permission → mask — the grant table the evaluator consults |
The JPA-backed tables are auto-created by Hibernate ddl-auto=update when the extension’s entities are loaded. The JDBC-only tables need explicit DDL — see Step 3.
Step 3 — seed the ACL tables#
DDL for the three JDBC-only tables (MariaDB / MySQL syntax; adjust for your dialect):
CREATE TABLE xpm_acl_class (
id int NOT NULL AUTO_INCREMENT,
class_code varchar(64) NOT NULL,
class_name varchar(128) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uq_xpm_acl_class_code (class_code)
);
CREATE TABLE xpm_acl_permission (
id int NOT NULL AUTO_INCREMENT,
class_id int NOT NULL,
code varchar(32) NOT NULL,
name varchar(128) NOT NULL,
display_order int DEFAULT 0,
PRIMARY KEY (id),
UNIQUE KEY uq_xpm_acl_permission (class_id, code)
);
CREATE TABLE xpm_acl_group_permission (
id bigint NOT NULL AUTO_INCREMENT,
group_id int NOT NULL,
permission_id int NOT NULL,
mask int DEFAULT 0,
created_by varchar(50) NOT NULL,
last_upd_by varchar(50) NOT NULL,
created_on datetime NOT NULL,
last_upd_on datetime NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uq_xpm_acl_group_permission (group_id, permission_id)
);Seed rows — one xpm_acl_class row per domain type that a handler enforces @Permission("...") on, one xpm_acl_permission row per operation code. Example:
INSERT INTO xpm_acl_class (class_code, class_name) VALUES
('XpmUser', 'User Management'),
('XpmGroup', 'Group & ACL Management'),
('MrcpPatient', 'Patient');
INSERT INTO xpm_acl_permission (class_id, code, name, display_order) VALUES
((SELECT id FROM xpm_acl_class WHERE class_code='XpmUser'), 'CUD', 'Create/Update/Delete', 1),
((SELECT id FROM xpm_acl_class WHERE class_code='XpmUser'), 'RS', 'Read/List', 2),
((SELECT id FROM xpm_acl_class WHERE class_code='XpmUser'), 'Unlock', 'Unlock User', 3),
((SELECT id FROM xpm_acl_class WHERE class_code='XpmGroup'), 'CUD', 'Create/Update/Delete', 1),
((SELECT id FROM xpm_acl_class WHERE class_code='XpmGroup'), 'RS', 'Read/List', 2),
((SELECT id FROM xpm_acl_class WHERE class_code='MrcpPatient'), 'CUD', 'Create/Update/Delete', 1),
((SELECT id FROM xpm_acl_class WHERE class_code='MrcpPatient'), 'RS', 'Read/List', 2);Then create a default admin group and grant every permission with mask=1:
INSERT INTO xpm_group (name, description, active, created_by, last_upd_by, created_on, last_upd_on)
VALUES ('AppAdmin', 'Admin group', 1, 'system', 'system', NOW(), NOW());
INSERT INTO xpm_acl_group_permission (group_id, permission_id, mask, created_by, last_upd_by, created_on, last_upd_on)
SELECT g.id, p.id, 1, 'system', 'system', NOW(), NOW()
FROM xpm_group g
CROSS JOIN xpm_acl_permission p
WHERE g.name = 'AppAdmin';Finally map a user into the admin group:
INSERT INTO xpm_acl_user (group_id, user_id, active, created_by, last_upd_by, created_on, last_upd_on)
VALUES (
(SELECT id FROM xpm_group WHERE name = 'AppAdmin'),
(SELECT id FROM xpm_user WHERE login_name = 'admin'),
1, 'system', 'system', NOW(), NOW()
);Step 4 — declare permissions on your handlers#
Once the classes / permissions exist in the DB, any handler annotated with @Permission gets gated by the evaluator:
@Component
@Permission(
value = "MrcpPatient",
create = "CUD",
update = "CUD",
delete = "CUD",
query = "RS",
read = "RS"
)
@CrudMapping(mapping = "/patient", type = PatientModel.class, secondaryMapping = "/patient/{id}")
public class PatientHandler extends AbstractAclHandler
implements QueryHandler, ReadHandler, CreateHandler, UpdateHandler, DeleteHandler {
// Framework calls evaluator.hasPermission("MrcpPatient", "CUD") on write paths,
// ("MrcpPatient", "RS") on reads, against the current user's groups.
}Every non-matching call returns HTTP 403. Class codes must match the xpm_acl_class.class_code column — a typo is a silent “access denied”.
Endpoints shipped by the extension#
palmyra-dbacl-mgmt auto-registers a handful of handlers. Available endpoints (mounted under palmyra.servlet.prefix-path, so typically /api/palmyra/…):
| Endpoint | Handler | Purpose |
|---|---|---|
GET /acl/menu/listAll |
AclController.getSideMenu |
Side menu for the current user — returns rows with id, name, display_label, path, code, action, parent, CSV-joined children, display_order. Built for AsyncTreeMenu on the frontend. |
GET /acl/permission |
AclController.getUserPermission |
Flat list of permissions (classCode, code, mask) for the logged-in user — handy to cache on the client for UI-level grant checks. |
GET /admin/acl/group/{groupId}/menuList |
AclController.getMenuList |
Tree of all menus with the mask each carries for the given group — for the group-permission editor UI. |
PUT /admin/acl/group/{groupId} |
AclController.saveMenuGroup |
Persist changes from the menu editor. Gated on hasPermission('XpmGroup', 'CUD'). |
PUT /admin/acl/permission/group/{groupId} |
AclController.saveGroupPermission |
Toggle permission grants on a group. Gated on hasPermission('XpmGroup', 'AclEdit'). |
GET /admin/acl/permission/group/{groupId} |
AclController.getGroupPermission |
Read grants for a group. Gated on hasPermission('XpmGroup', 'AclRead'). |
GET /admin/acl/group + CRUD on {id} |
GroupHandler |
Palmyra @CrudMapping handler — create / read / query / delete groups. No UpdateHandler — update goes through saveMenuGroup. |
GET /admin/acl/user/{userId}/groups |
QueryGroupsByUserHandler |
Groups the given user belongs to. |
GET /admin/acl/group/{groupId}/users |
QueryUsersByGroupHandler |
Users in the given group. |
GET /admin/acl/user/{userId}/groups/lookup |
QueryGroupsLookupByUserHandler |
Lookup-only variant (id + name) — for ServerLookup dropdowns. |
GET /admin/acl/group/{groupId}/users/lookup |
QueryUsersLookupByGroupHandler |
Same, other direction. |
No user CRUD is shipped. The extension provides groups but not users — you wire your own
@CrudMappingover thexpm_usertable (orUserManagementHandlerif porting from ada_project).
Permission evaluation path#
- Handler is invoked;
@Permission(value="X", create="CUD")says “for writes, the user needs X:CUD”. PalmyraHandlerAclEvaluatorresolves the current user → their groups → the group_permission rows → looks for(class_code="X", code="CUD", mask > 0).- On match, the request proceeds. On miss, a
EndPointForbiddenExceptionis thrown → mapped to HTTP 403.
@PreAuthorize("hasPermission('X', 'CUD')") on a Spring @RestController goes through the same evaluator — so declarative ACL on controllers and on @CrudMapping handlers share one policy surface.
See also#
@PermissionannotationPalmyraPermissionEvaluator- User Management extension — pairs with this one for full auth + ACL