--- name: manage-security description: "Configure Mendix security in MDL — module and user roles, access to microflows, pages and entities, project security level, demo users, and guest access. Use when setting up or changing who can see or do what in an app." --- # Security Management Skill This skill covers Mendix security configuration via MDL: module roles, user roles, access control (microflows, pages, entities), project security settings, and demo users. ## When to Use This Skill Use when the user asks to: - Set up security for a module or project - Create or manage module roles / user roles - Grant or revoke access to microflows, pages, or entities - Configure project security level or demo users - Review existing security configuration ## Security Concepts - **Module Roles** define permissions within a single module (e.g., `Shop.Admin`, `Shop.Viewer`) - **User Roles** aggregate module roles from multiple modules (e.g., `Administrator` includes `Shop.Admin` + `System.Administrator`) - **Access Rules** control CRUD rights on entities per module role - **Microflow/Page Access** controls which module roles can execute/view specific elements - **Project Security Level** determines enforcement: `off`, `prototype`, or `production` ## The System-module ceiling — decide the role model around this FIRST **No project module can widen access to `System.User`, `System.Workflow` or `System.WorkflowUserTask`.** Access to System entities comes from the System module's own roles, and a `grant` in your module cannot raise it. This is a design constraint, not a detail: it decides what your screens can be, and finding it late means rebuilding them (ako/mxcli-maintenance-2 designed a technician picker, built it, tested it, and tore it out). Every consequence is **silent** — the page renders, the data is simply missing, and `mx check`, `mxcli lint` and `mxcli report` all pass: | What you build | What a non-Administrator sees | |---|---| | A combo box over `System.User` (e.g. "pick a technician") | **The current user only** | | A grid over `System.Workflow` / `System.WorkflowUserTask` | **Empty** | | Either of those, re-sourced from a **microflow** | **Every row present, every field blank** | ### The rule: a microflow data source moves the ROWS, not the MEMBERS Learn this as a rule rather than as a symptom, because the obvious workaround only *looks* like it worked, and the same trap is waiting on the next screen. A microflow does not apply entity access, so its retrieve returns every row. But the runtime **re-applies entity access when it serializes those objects to the client, XPath constraint included** — so a row the role may not read arrives with every member empty. The list comes out the right length and the cards come out blank. Measured in a browser on Mendix 11.14.0 — one page, two microflow-sourced lists over the *same* `System.User` retrieve, opened by two users: | list | as Administrator | as a plain User | |---|---|---| | A — the `System.User` objects themselves | `probe_admin`, `probe_viewer` | **(blank)**, `probe_viewer` | | B — a module-owned copy, `Name` read *inside* the microflow | `probe_admin`, `probe_viewer` | `probe_admin`, `probe_viewer` | Both lists hold two rows for both users, so the microflow really did carry the rows past entity access. Only A loses the values, and it loses them **per object**: `System.User`'s own rule grants read where `[id = '[%CurrentUser%]']`, which is why a user picker shows you yourself and nobody else rather than showing nothing. `System.WorkflowUserTask` has no such escape hatch for an ordinary role, so a workflow inbox built this way comes out *entirely* blank — the reported case (ako/mxcli#587): the inbox drew the right number of cards, every one of them empty, with `mxcli check`, `lint`, `report` and `docker check` all at 0 errors. ### What to do instead 1. **Record, don't pick.** Have the workflow stamp itself against a row your own module owns — an `ON CREATED MICROFLOW` writing an association plus a plain status string — and list *those* objects. Target a task at a *role*, let whoever opens it do the work, and record who acted when they act. 2. **Read it inside a microflow, return your OWN object.** Entity access does not apply to the retrieve *or* to the member read, only to what crosses to the client — so copy the values you need onto an entity your module owns (persistent or non-persistent) and bind the page to that. This is list B above, and it is the same shape the reporter arrived at independently for user pickers: `Engineer` is a module-owned entity rather than `System.User`. 3. **Split the page by role.** Keep raw System grids on an Administrator-only page. Mendix hides a button to a page the user may not view, so the link simply does not appear. Related: `grant … on System.User` is refused by `exec` — the System module's domain model is not stored in the project, so it has no access rules to add to. The refusal is at execution, not at `mxcli check`, so a script carrying one passes `check` and then stops part-way through. ## Syntax Reference ### Show Commands (Read-Only) ```sql -- Project-wide security overview describe app security; -- Module roles (all or filtered) list module roles; list module roles in MyModule; -- User roles and demo users list user roles; list demo users; -- Access on specific elements list access on microflow MyModule.ProcessOrder; list access on page MyModule.CustomerOverview; list access on entity MyModule.Customer; list access on MyModule.Customer; -- a bare name means the entity -- Full security matrix describe security matrix; describe security matrix in MyModule; ``` ### Describe Commands ```sql -- Describe individual roles and users (MDL output) describe module role MyModule.Admin; describe user role Administrator; describe demo user 'demo_admin'; ``` `describe demo user` never prints the password. It emits `create or modify demo user 'demo_admin' password '***' …`, where `'***'` means *keep the stored password*: replaying the output on the same project leaves the password as it is, and on a project without that user the statement is refused until you replace `'***'` with a real password. ### Catalog Queries (SQL) Security data is available in catalog tables for advanced querying. Use `refresh catalog full` to populate permissions and role mappings. ```sql -- All permissions (entity, microflow, page, OData access) select * from CATALOG.PERMISSIONS where ModuleRoleName = 'MyModule.Admin'; -- Filter by type select ElementName, AccessType from CATALOG.PERMISSIONS where ElementType = 'ENTITY' and ModuleName = 'MyModule'; select ElementName from CATALOG.PERMISSIONS where ElementType = 'MICROFLOW' and AccessType = 'EXECUTE'; -- User role to module role mappings select * from CATALOG.ROLE_MAPPINGS; select ModuleRoleName from CATALOG.ROLE_MAPPINGS where UserRoleName = 'Administrator'; -- Which user roles have access to a module? select distinct UserRoleName from CATALOG.ROLE_MAPPINGS where ModuleName = 'MyModule'; -- Describe catalog table schema describe CATALOG.PERMISSIONS; describe CATALOG.ROLE_MAPPINGS; ``` **Catalog tables:** | Table | Contents | Build mode | |-------|----------|------------| | `CATALOG.PERMISSIONS` | Entity CRUD, microflow EXECUTE, page VIEW, OData ACCESS | `refresh catalog full` | | `CATALOG.ROLE_MAPPINGS` | User role → module role assignments | `refresh catalog` | ### Module Roles ```sql mdl 1; -- Create module roles create module role MyModule.Admin description 'Full administrative access'; create module role MyModule.User; create module role MyModule.Viewer description 'Read-only access'; -- `or modify` updates an existing role's description instead of failing, so the -- whole security script stays re-runnable rather than needing a run-once file. create or modify module role MyModule.ApiUser description 'API consumer'; -- Remove a module role (`if exists` makes it a no-op when the role is gone) drop module role MyModule.Viewer; drop module role if exists MyModule.Legacy; ``` ### Microflow Access ```sql mdl 1; -- Grant execute access (multiple roles supported) grant execute on microflow MyModule.ACT_Customer_Create to MyModule.User, MyModule.Admin; -- Revoke from specific roles revoke execute on microflow MyModule.ACT_Customer_Create from MyModule.User; ``` ### Nanoflow Access ```sql mdl 1; -- Grant execute access (same syntax as microflows) grant execute on nanoflow MyModule.NF_ValidateCart to MyModule.User, MyModule.Admin; -- Revoke from specific roles revoke execute on nanoflow MyModule.NF_ValidateCart from MyModule.User; -- Show current access list access on nanoflow MyModule.NF_ValidateCart; ``` > **Note:** Security roles persist through DROP+CREATE of the same nanoflow name within a session (by design, for refactor-in-place workflows). ### Page Access ```sql mdl 1; -- Grant view access grant view on page MyModule.Customer_Overview to MyModule.User, MyModule.Admin; -- Revoke from specific roles revoke view on page MyModule.Customer_Overview from MyModule.User; ``` ### What a New Document Starts With — and Why GRANT Does Not Narrow It Document grants (page, microflow, nanoflow) are **additive**, like entity grants: GRANT adds roles, REVOKE removes them, nothing replaces the list. What a newly **created** document starts with depends on its module: | Module has… | New page / microflow / nanoflow gets | |---|---| | no module roles | an auto-created `.User` role (created on first use), granted to every new document while it is the module's only role. `exec` prints `access: granted to auto-created role .User (…)` under the create | | module roles of its own | **no** allowed roles — grant them in the same script | Consequences: - A stub page created early (module still role-less) is open to `.User`. A later `grant view on page M.Stub to M.Admin;` **adds** Admin; every user role mapped to `M.User` can still open it. Narrow it explicitly: `revoke view on page M.Stub from M.User;` - **Drop + create in separate runs loses access.** `create or modify`, and drop + create of the same name within one run, keep the stored roles. A `create` in a later run than the `drop` is a new document; in a module with its own roles it has none, and if a page, snippet, nanoflow or navigation item uses the flow MxBuild fails with **CE0106** at security level Prototype/Production. `mxcli check -p` reports it as **MDL-SEC21** before exec. Prefer `create or modify` to rebuild a flow; otherwise grant in the same script. ### Always Qualify a Module Role A module role is always `Module.Role`. The grammar makes the module part optional, so a bare `Admin` parses — and then either fails at exec (after every earlier statement has already been written) or, in `create user role`, is stored as `.Admin` and refused by MxBuild with **CE1613**. `mxcli check` reports it as **MDL-GRANT02** without needing a project. ```text grant read * on entity MyModule.Customer to Admin; -- ✗ MDL-GRANT02 ``` ```sql mdl 1; grant read * on entity MyModule.Customer to MyModule.Admin; -- ✓ ``` ### Entity Access (CRUD) GRANT is **additive** — it merges with existing access, never removes permissions. Rights rank `None < ReadOnly < ReadWrite` and a merge takes the higher, so use `revoke` to take access away; a narrower `grant` will not do it. A rule is identified by its role set **and** its `where` constraint. Two grants with the same constraint update one rule; with different constraints they make two, which Mendix combines at runtime (a role's access is the union of every rule naming it). So adding a `where` to an existing grant creates a second rule — it does not narrow the first. ```sql mdl 1; -- Full access (all CRUD + all members) grant create, delete, read *, write * on entity MyModule.Customer to MyModule.Admin; -- Read-only (all members) grant read * on entity MyModule.Customer to MyModule.Viewer; -- Selective member access grant read (Name, Email), write (Email) on entity MyModule.Customer to MyModule.User; -- Additive: adds Phone to existing read access (Name, Email preserved) grant read (Phone) on entity MyModule.Customer to MyModule.User; -- With XPath constraint: in [ ] like every XPath, quotes written once. -- The old `grant MyModule.User on MyModule.Order (…) where '[…]'` still parses -- but warns MDL-DEPR030; `mxcli fmt --upgrade` rewrites it. grant read *, write * on entity MyModule.Order to MyModule.User where [Status = 'Open']; -- Revoke entity access entirely revoke all on entity MyModule.Customer from MyModule.Viewer; -- Partial revoke: remove read on specific attribute revoke read (Phone) on entity MyModule.Customer from MyModule.User; -- Partial revoke: downgrade write to read-only revoke write (Email) on entity MyModule.Customer from MyModule.User; -- Partial revoke: remove structural permission revoke delete on entity MyModule.Customer from MyModule.User; ``` #### Members added later A rule also carries a default for members added **after** it was written, and MDL derives it from the grant: `write *` → ReadWrite, `read *` → ReadOnly, and a grant written **purely as member lists** leaves it at **None**. So `alter entity … add attribute` gives the new attribute None on a member-listed rule. Nothing is broken — every rule gets an entry, the build is clean — but the attribute renders blank for that role. `alter entity` warns and prints the grant that widens it. What decides this is the rule's default, **not** how narrow its member list is: `read *, write (Email)` is narrower than `read *, write *` and still picks up new members, because `read *` set its default to ReadOnly. Give a rule `read *` and narrow with `revoke` when the role should follow the entity as it grows. #### Inherited members Mendix inheritance is multi-table: a child adds attributes to its parent's, and **all** the parent's members belong to the child. Grant them exactly like the entity's own — `read *` / `write *` cover them too: ```sql mdl 1; create persistent entity Docs.DocumentBase ( DocName: String(200), Confidential: Boolean ); create persistent entity Docs.Contract extends Docs.DocumentBase ( ContractNumber: String(50) ); -- DocName is inherited, ContractNumber is Contract's own — name both the same way grant read (DocName, ContractNumber) on entity Docs.Contract to Docs.Viewer; -- Attachment inherits the file members from System.FileDocument create persistent entity Docs.Attachment extends System.FileDocument ( Category: String(50) ); grant read (Category, "Name", Size) on entity Docs.Attachment to Docs.Viewer; ``` An access rule must carry an entry for **every** member, own and inherited — mxcli writes the ones you did not grant with rights `None`. Omitting them is Mendix **CE0066** "Entity access is out of date", which masks the CE2729 "No read access to attribute" errors underneath until Studio Pro's *Update security* is clicked. ### `UPDATE SECURITY` — reconciling rules that have gone stale mxcli reconciles an entity's access rules whenever it writes the entity, so a rule normally cannot go stale through mxcli. `UPDATE SECURITY` is the repair for one that did — a model edited elsewhere, or an older mxcli: ```sql mdl 1; update security; -- every module the project owns update security RestLab; -- one module (IN is optional) update security in RestLab; -- the same thing ``` Three things worth knowing, each of which was a defect until [mendixlabs/mxcli#1047](https://github.com/mendixlabs/mxcli/issues/1047): - **`System` is skipped.** Its entities are the platform's, its access rules are Mendix's rather than the project's, and its domain model is not stored in the `.mpr` at all. Naming it explicitly is an error, not a silent no-op. - **One unreadable module does not end the run.** It is reported and stepped over, so the rest are still reconciled. Previously the first failure returned, and System guaranteed one on every project — which made the command inert. - **The scope is honoured.** `update security RestLab` used to parse cleanly and run project-wide, because the name reached the parser's error recovery. A run that skipped something says so. `All entity access rules are up to date` means every module was looked at. The commonest source of a stale rule is a module that arrived from outside Studio Pro. `mxcli marketplace install` and `mxcli marketplace update` copy the incoming units in verbatim, so a package whose rules do not cover their entities' members used to land CE0066 in the project with nothing said about it ([mendixlabs/mxcli#1085](https://github.com/mendixlabs/mxcli/issues/1085)); both now run this reconcile for the module they copy in and report the count. Run it by hand for a module installed some other way, or by an older mxcli: ```bash mxcli -p app.mpr -c "update security UserCommons" ``` A member name that matches nothing is now an error rather than a silent skip: ``` Error: entity Docs.Contract has no member(s) DocNam; grant only names members of the entity or of an entity it inherits from ``` **Exception — user entities.** An entity extending `System.User` is a *user entity*, and Mendix manages its inherited platform members (`Name`, `Password`, `Blocked`, …). Those must **not** appear in the rule; listing them is CE0066. Grant only the entity's own members — mxcli leaves the platform ones out automatically: ```sql mdl 1; create persistent entity Docs.Employee extends System.User ( EmployeeNo: String(20) ); grant read (EmployeeNo) on entity Docs.Employee to Docs.Viewer; -- not Name/Blocked ``` ### User Roles ```sql mdl 1; -- Create with module roles create user role RegularUser ( ModuleRoles: (MyModule.User, OtherModule.Reader) ); -- Create with manage all roles permission create user role SuperAdmin ( ModuleRoles: (MyModule.Admin), ManageAllRoles: true ); -- Every property is optional: Description, CheckSecurity, ManageableRoles, -- ManageUsersWithoutRoles. `create user role Guest;` has no module roles. -- `create or modify` adds the listed module roles and sets only the stated -- properties. The positional `create user role R (M.A) manage all roles` -- is deprecated (MDL-DEPR710); `mxcli fmt --upgrade` rewrites it. create or modify user role Manager ( ModuleRoles: (MyModule.Manager), Description: 'Approves orders', ManageableRoles: (RegularUser), CheckSecurity: true ); -- Add/drop module roles alter user role RegularUser add module roles (MyModule.Viewer); alter user role RegularUser drop module roles (MyModule.Viewer); -- Remove user role drop user role RegularUser; ``` ### Project Security Settings ```sql mdl 1; -- Set security level alter app security ( SecurityLevel: off ); alter app security ( SecurityLevel: prototype ); alter app security ( SecurityLevel: production ); -- Enable/disable demo users alter app security ( EnableDemoUsers: true ); alter app security ( EnableDemoUsers: false ); ``` ### Guest (Anonymous) Access Anonymous access is what makes part of an app public — a product catalogue anyone can browse without signing in. It is one flag plus a user role, and the role is the important half: **whatever that role can read is the app's public surface.** ```sql mdl 1; -- The role anonymous visitors are given. System.User is what lets an -- unauthenticated session exist at all. create user role Anonymous ( ModuleRoles: (Shop.Viewer, System.User) ); alter app security ( EnableGuestAccess: true, GuestUserRole: Anonymous ); -- Now grant exactly what should be public — and nothing else. Entity access -- goes to the module role the Anonymous user role carries. grant read * on entity Shop.Product to Shop.Viewer; -- Re-enabling later does not need the role retyped; the stored one is used. alter app security ( EnableGuestAccess: false ); alter app security ( EnableGuestAccess: true ); ``` Three things worth knowing: - **The role is mandatory.** Mendix fails the build with **CE0133** ("No user role for anonymous users selected even though the feature anonymous users is enabled") when access is on with no role. `EnableGuestAccess: true` is refused unless a role is given or one is already stored. - **Mendix does not check the role exists**, so mxcli does. A misspelled role would otherwise build with zero errors and leave anonymous visitors with no access at all — a broken public site that passes every check. - **`off` keeps the stored role**, so toggling access while testing does not lose it. Guest access off with a role set is valid Mendix. Review anonymous entity access the way lint rule **SEC004** asks you to: any unconstrained `read *` granted to the anonymous role is readable by the whole internet (DIVD-2022-00019). Add an XPath constraint or do not grant it. ### Demo Users ```sql mdl 1; -- Create demo user (auto-detects entity that generalizes System.User) create demo user 'demo_admin' ( Password: 'Admin123!', UserRoles: (Administrator, SuperAdmin) ); -- Create demo user with explicit entity create demo user 'demo_admin' ( Password: 'Admin123!', Entity: Administration.Account, UserRoles: (Administrator, SuperAdmin) ); -- Remove demo user drop demo user 'demo_admin'; ``` The ENTITY clause specifies which entity (generalizing `System.User`) to use. If omitted, it auto-detects the unique System.User subtype in the project. If multiple subtypes exist, you must specify ENTITY explicitly. ## Starlark Lint Rule APIs Security data is available in Starlark lint rules (`.star` files): | Function | Returns | Description | |----------|---------|-------------| | `permissions()` | list of permission | All permissions across all element types | | `permissions_for(qn)` | list of permission | Permissions for a specific entity | | `user_roles()` | list of user_role | User roles with module role assignments | | `module_roles()` | list of module_role | Distinct module roles | | `role_mappings()` | list of role_mapping | User role → module role mappings | | `project_security()` | struct or None | Security level, guest access, password policy | See `write-lint-rules` for object property details. ## Common Workflow: Setting Up Module Security A typical security setup follows this order: ```sql mdl 1; -- 1. Create module roles create module role Shop.User description 'Regular user access'; create module role Shop.Admin description 'Administrative access'; create module role Shop.Viewer description 'Read-only access'; -- 2. Grant entity access grant create, delete, read *, write * on entity Shop.Customer to Shop.Admin; grant read (Name, Email), write (Email) on entity Shop.Customer to Shop.User; grant read * on entity Shop.Customer to Shop.Viewer; -- 3. Grant microflow access grant execute on microflow Shop.ACT_Customer_Create to Shop.User, Shop.Admin; grant execute on microflow Shop.ACT_Customer_Delete to Shop.Admin; -- 4. Grant page access grant view on page Shop.Customer_Overview to Shop.User, Shop.Admin, Shop.Viewer; grant view on page Shop.Customer_Edit to Shop.User, Shop.Admin; -- 5. Create user roles (project-level) create user role AppUser ( ModuleRoles: (Shop.User) ); create user role AppAdmin ( ModuleRoles: (Shop.Admin), ManageAllRoles: true ); -- 6. Verify describe security matrix in Shop; describe user role AppAdmin; ``` ### What counts as a member of an entity An access rule has to cover **every** member of the entity, or Mendix fails the build with `CE0066 "Entity access is out of date"` — a partially covered rule is worse than no rule at all. `read *` / `write *` cover them all, including the ones that are easy to overlook: | Member | Named in a GRANT as | Notes | |--------|--------------------|-------| | Attributes (own and inherited) | the attribute name | see "Inherited members" above | | Association, `OWNER Default` | the association name | **on the FROM entity only** — adding it to the TO side is itself a CE0066 | | Association, `OWNER Both` | the association name | on **both** entities — each end owns it | | `createdDate` / `changedDate` | the member name | covered by the rule's **default** only; Mendix stores no per-member access for them | | `owner` / `changedBy` | — | emitted automatically as `System.owner` / `System.changedBy` | Audit members are the one case where naming a member cannot change its rights: `grant write *, read (createdDate) on entity M.E to R` is refused, because Mendix has no member access to write for it and a rule that carries one fails CE0066. Let the rule's default cover it, or change the default. ## Common Mistakes 1. **Creating module roles before the module exists** — `create module` must come first 2. **Referencing non-existent roles in GRANT** — create the module role before granting access 3. **Forgetting qualified names** — roles use `Module.Role` format in GRANT/REVOKE 4. **User roles without System module roles** — in Production security, user roles need at least one System module role (CE0156) 5. **Entity access without proper member rights** — use `read *` for all members or `read (Attr1, Attr2)` for specific ones 6. **Expecting a GRANT to replace a default** — a new document in a role-less module is granted to the auto-created `.User`; a later grant adds to it. `revoke … from .User` to narrow 7. **Rebuilding a flow with drop + create across runs** — the later create starts with no access in a module that has roles (CE0106 when the flow is used from a page/nanoflow/navigation). Use `create or modify`, or grant in the same script ## Validation After setting up security, verify with: ```bash # check security matrix mxcli -p app.mpr -c "describe security matrix in MyModule" # Validate with Mendix mxcli docker check -p app.mpr ```