Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/skills/fix-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ cases for these three BSON types — they fell to `default: return nil`.
| `mdl/executor/cmd_microflows_format_action.go` | Added 3 formatter cases |
| `mdl/executor/cmd_microflows_format_listop_test.go` | Added 4 formatter tests |
| `sdk/mpr/parser_listoperation_test.go` | New file, 4 parser tests |
| `UPDATE SECURITY`, `CREATE ASSOCIATION` or any `GRANT` silently strips **inherited** members from a specialized entity's access rules; `GRANT` naming an inherited member reports success and persists nothing, so REVOKE+GRANT cannot repair it. `mx check` shows only CE0066, hiding the CE2729 "No read access to attribute" cascade until Studio Pro's Update security is clicked | Mendix inheritance is multi-table: all of a parent's attributes are members of the child, so an access rule needs a MemberAccess entry for every member, own **and** inherited, each qualified against the entity that **declares** it. Both the GRANT builder and `ReconcileMemberAccesses` enumerated only `entity.Attributes`, so an inherited reference matched nothing and was deleted as stale — and reconciliation runs **immediately after every GRANT**, deleting what the grant had just written correctly | `mdl/executor/entity_hierarchy.go` (`EntityMembers`), `mdl/executor/cmd_security_write.go` (`execGrantEntityAccess`), `mdl/backend/modelsdk/domainmodel_security_write.go` (`ReconcileMemberAccesses`, `attrRefBelongsTo`), `sdk/mpr/writer_security.go` (legacy engine) | Walk the generalization chain and qualify each member against its declaring entity; in the reconciler, only strip a reference qualified to **this** entity — an ancestor may live in another module or System, neither loaded there, so preserve what cannot be validated. **Two facts must be established against `mx check`, never inferred**: (a) the child-qualified form is CE1613 "attribute no longer exists" while the declaring-entity form validates clean; (b) `System.User`'s members are the exception — entities specialising it are *user entities* whose platform members Mendix manages, and listing them turns a clean rule into CE0066, while omitting `System.FileDocument`'s six members is CE0066 until all are present. **Generalisable**: when a post-write reconcile pass validates against a narrower model than the writer used, it will quietly undo correct writes — check what runs *after* a write before concluding the writer is at fault. Repro `mdl-examples/bug-tests/758-inherited-member-access.mdl`. Issues #758, #765 (umbrella; #451 is the same declaring-entity rule in the change-object writer) |
| `describe` (and `context` / `diff-local`) renders a Retrieve's XPath with only its **first** predicate group — `where A/B[EndDate = $X];` when the BSON holds `[A/B[EndDate = $X]][Status != 'Completed'][CompletionDate = empty]`. No warning; the output reads as a complete but materially *less restrictive* query, so correct defensive code looks buggy | The grammar's `xpathConstraint` rule matches ONE bracket group, and Mendix concatenates siblings. `ParseXPathConstraint` removes the error listeners, so ANTLR parsed group 1, left the rest on the token stream, and **still returned ok=true**; `enrichXPathConstraintForDescribe` treated that as a full parse and re-rendered only what came back. The `if !ok { return original }` fallback never fired | `mdl/visitor/visitor_xpath_public.go` (`ParseXPathConstraint`), `mdl/visitor/xpath_groups.go` (`SplitXPathPredicateGroups`), `mdl/executor/cmd_microflows_format_action.go` (`enrichXPathGroups`, and the render-path split) | Two layers. (1) Reject a partial parse — after the rule, require `stream.LA(1) == antlr.TokenEOF`; that alone stops the loss, since the caller then falls back to the stored string. (2) Split into top-level groups and enrich each, so enrichment still reaches groups after the first. The splitter must track **nesting depth and quoting**: a naive `][` split mangles a nested `[A/B[x = 1]]` and a literal containing `]`. **Generalisable**: a parser that silently accepts a prefix is worse than one that fails — any `ok` returned by a rule that can match less than its input must be checked against EOF before callers treat it as lossless. Repro `mdl-examples/bug-tests/772-xpath-constraint-groups.mdl`; A/B against a pre-fix binary on the same project shows the two dropped groups. Issue #772 |

**Key insight:** `microflows$ListRange` stores offset/limit inside a nested
Expand Down
9 changes: 9 additions & 0 deletions .claude/skills/mendix/generate-domain-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,15 @@ create persistent entity Module.Photo (

**Note:** `mxcli syntax entity` output may show EXTENDS after `)` — this is misleading. Always place EXTENDS before `(`.

**Security follows inheritance.** Mendix inheritance is multi-table: all of the
parent's attributes are members of the child, so a specialized entity's access rule
must cover them. Grant an inherited member exactly like one of the entity's own —
`grant Module.Viewer on Module.Attachment (read (AttachmentDescription, "Name", Size));`
— and `read *` / `write *` cover them too. Skipping them is Mendix CE0066 "Entity
access is out of date". The one exception is entities extending `System.User`, whose
inherited platform members Mendix manages and which must not be granted. See
`manage-security.md`.

#### System Attributes (Auditing)

Mendix supports four built-in auditing properties on persistent entities. Declare them as regular attributes using pseudo-types (like `autonumber`):
Expand Down
52 changes: 52 additions & 0 deletions .claude/skills/mendix/manage-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,58 @@ revoke MyModule.User on MyModule.Customer (write (Email));
revoke MyModule.User on MyModule.Customer (delete);
```

#### 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
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 Docs.Viewer on Docs.Contract (read (DocName, ContractNumber));

-- Attachment inherits the file members from System.FileDocument
create persistent entity Docs.Attachment extends System.FileDocument (
Category: String(50)
);
grant Docs.Viewer on Docs.Attachment (read (Category, "Name", Size));
```

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.

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
create persistent entity Docs.Employee extends System.User (
EmployeeNo: String(20)
);
grant Docs.Viewer on Docs.Employee (read (EmployeeNo)); -- not Name/Blocked
```

### User Roles

```sql
Expand Down
20 changes: 18 additions & 2 deletions cmd/mxcli/syntax/features_security.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,24 @@ func init() {
"entity access", "grant", "revoke", "read", "write",
"create", "delete", "xpath", "row-level security",
},
Syntax: "GRANT <role> ON <module>.<entity> (<rights>) [WHERE '<xpath>'];\nREVOKE <role> ON <module>.<entity>;\nREVOKE <role> ON <module>.<entity> (<rights>);\n\nRights: CREATE, DELETE, READ *, READ (<attr>,...), WRITE *, WRITE (<attr>,...)",
Example: "GRANT Shop.Admin ON Shop.Customer (CREATE, DELETE, READ *, WRITE *);\nGRANT Shop.User ON Shop.Customer (READ *) WHERE '[Active = true()]';",
Syntax: "GRANT <role> ON <module>.<entity> (<rights>) [WHERE '<xpath>'];\n" +
"REVOKE <role> ON <module>.<entity>;\n" +
"REVOKE <role> ON <module>.<entity> (<rights>);\n\n" +
"Rights: CREATE, DELETE, READ *, READ (<attr>,...), WRITE *, WRITE (<attr>,...)\n\n" +
"Inherited members:\n" +
" Mendix inheritance is multi-table — a child adds attributes to its\n" +
" parent's, and ALL the parent's members belong to the child. Name them\n" +
" in a GRANT exactly like the entity's own; READ */WRITE * covers them\n" +
" too. A name that matches no member is an error, not a silent skip.\n\n" +
" Exception: entities extending System.User are user entities, whose\n" +
" platform members (Name, Password, Blocked, ...) Mendix manages. Do not\n" +
" grant those; mxcli leaves them out of the rule automatically.",
Example: "GRANT Shop.Admin ON Shop.Customer (CREATE, DELETE, READ *, WRITE *);\n" +
"GRANT Shop.User ON Shop.Customer (READ *) WHERE '[Active = true()]';\n\n" +
"-- Contract extends DocumentBase: DocName is inherited, ContractNumber is own\n" +
"GRANT Docs.Viewer ON Docs.Contract (READ (DocName, ContractNumber));\n\n" +
"-- Attachment extends System.FileDocument: Name and Size are inherited\n" +
"GRANT Docs.Viewer ON Docs.Attachment (READ (Category, \"Name\", Size));",
SeeAlso: []string{"security.module-role", "security.microflow-access"},
})

Expand Down
39 changes: 39 additions & 0 deletions docs-site/src/reference/security/grant.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,45 @@ GRANT Shop.Viewer ON Shop.Customer (READ (Phone));
-- Result: READ (Name, Email, Phone)
```

## 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. Name an inherited member in a
GRANT exactly like one of the entity's own; `READ *` and `WRITE *` cover them too.

```sql
CREATE PERSISTENT ENTITY Docs.DocumentBase (DocName: String(200));
CREATE PERSISTENT ENTITY Docs.Contract EXTENDS Docs.DocumentBase (ContractNumber: String(50));

-- DocName inherited, ContractNumber own — no distinction at the call site
GRANT Docs.Viewer ON Docs.Contract (READ (DocName, ContractNumber));
```

An access rule must carry an entry for **every** member, own and inherited. mxcli
writes the members you did not grant with rights `None`; omitting them entirely is
Mendix **CE0066** *"Entity access is out of date"*, which masks the CE2729
*"No read access to attribute"* errors beneath it until Studio Pro's
**Update security** is clicked.

A member name that matches nothing is rejected rather than skipped:

```
Error: entity Docs.Contract has no member(s) DocNam; grant only names members
of the entity or of an entity it inherits from
```

### User entities are the exception

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 access rule — listing them is CE0066. Grant only the entity's own
members; mxcli excludes the platform ones automatically.

```sql
CREATE PERSISTENT ENTITY Docs.Employee EXTENDS System.User (EmployeeNo: String(20));
GRANT Docs.Viewer ON Docs.Employee (READ (EmployeeNo));
```

## See Also

[REVOKE](revoke.md), [CREATE MODULE ROLE](create-module-role.md), [CREATE USER ROLE](create-user-role.md)
2 changes: 1 addition & 1 deletion docs/01-project/MDL_QUICK_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ Nested folders use `/` separator: `'Parent/Child/Grandchild'`. Missing folders a
| Revoke nanoflow access | `revoke execute on nanoflow Mod.NF from Mod.Role, ...;` | |
| Grant page access | `grant view on page Mod.Page to Mod.Role, ...;` | |
| Revoke page access | `revoke view on page Mod.Page from Mod.Role, ...;` | |
| Grant entity access | `grant Mod.Role on Mod.Entity (create, delete, read *, write *);` | Additive — merges with existing |
| Grant entity access | `grant Mod.Role on Mod.Entity (create, delete, read *, write *);` | Additive — merges with existing. Inherited members are named like the entity's own (`read *` covers them); an unknown name is an error. Entities extending `System.User` are the exception — their platform members must not be granted |
| Revoke entity access | `revoke Mod.Role on Mod.Entity;` | Full revoke — removes entire rule |
| Revoke entity access (partial) | `revoke Mod.Role on Mod.Entity (read (attr));` | Partial — downgrades specific rights |
| Set security level | `alter project security level off\|prototype\|production;` | |
Expand Down
70 changes: 70 additions & 0 deletions mdl-examples/bug-tests/758-inherited-member-access.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
-- Bug #758 / #765: security reconciliation stripped inherited members, and GRANT
-- could not put them back
--
-- Mendix models inheritance across multiple tables: a child adds attributes to the
-- parent's, and ALL the parent's attributes are available on the child. An access
-- rule must therefore carry a MemberAccess entry for every member — own AND
-- inherited — or Mendix reports CE0066 "Entity access is out of date".
--
-- Two facts established against `mx check`, not inferred:
--
-- 1. An inherited member's reference is qualified against the entity that
-- DECLARES it. `Sec758.Base.SharedField` validates clean; the child-qualified
-- `Sec758.Item.SharedField` is CE1613 "The selected attribute no longer
-- exists". mxcli wrote the child form.
--
-- 2. Members inherited from System.User are the exception: entities specialising
-- it are user entities whose platform members Mendix manages. Listing them
-- turns a clean rule into CE0066 — confirmed on Mendix's own
-- Administration.Account and on a fresh specialisation. Every other ancestor's
-- members are required: omitting the six System.FileDocument members from a
-- specialising entity's rule is CE0066 until all are present.
--
-- What went wrong. Both the GRANT path and reconciliation enumerated only
-- entity.Attributes:
--
-- * GRANT naming an inherited member produced no entry, and reported success.
-- * ReconcileMemberAccesses — which runs immediately after every GRANT, and on
-- any write touching the module — saw the inherited entry's ancestor
-- qualification, failed to match it against the child's own attributes, and
-- deleted it as stale. So the grant wrote the right thing and the reconcile in
-- the same command removed it. That is why REVOKE + GRANT never repaired a
-- damaged rule.
--
-- The damage was masked: `mx check` reports CE0066 and stops, hiding the CE2729
-- "No read access to attribute" cascade underneath until Studio Pro's
-- "Update security" is clicked.
--
-- Verify:
-- 1. mxcli exec 758-inherited-member-access.mdl -p app.mpr
-- 2. mxcli -p app.mpr -c "describe entity Sec758.Item"
-- -> grant ... (read (OwnField, SharedField)); both members present
-- 3. mxcli docker check -p app.mpr -> 0 errors (no CE0066)
-- 4. Repeat step 3 after any further ALTER on the module: the inherited entry
-- must still be there.

create module Sec758;
create module role Sec758.Editor;

create persistent entity Sec758.Base (
SharedField: String(100)
);

-- Same-module ancestor: mixed own + inherited members.
create persistent entity Sec758.Item extends Sec758.Base (
OwnField: String(100)
);

-- System ancestor whose members ARE required in the child's rule.
create persistent entity Sec758.Attachment extends System.FileDocument (
"Caption": String(200)
);

-- User entity: System.User's platform members must NOT appear in the rule.
create persistent entity Sec758.Employee extends System.User (
EmployeeNo: String(20)
);

grant Sec758.Editor on Sec758.Item (read (SharedField, OwnField));
grant Sec758.Editor on Sec758.Attachment (read ("Caption"));
grant Sec758.Editor on Sec758.Employee (read (EmployeeNo));
35 changes: 35 additions & 0 deletions mdl/backend/modelsdk/attr_ref_owner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// SPDX-License-Identifier: Apache-2.0

package modelsdkbackend

import "testing"

// TestAttrRefBelongsTo pins the distinction reconciliation depends on: only a
// reference qualified against THIS entity names one of its own attributes and can
// be validated from this domain model. An inherited reference carries an
// ancestor's name — possibly from another module or System, neither loaded here —
// and deleting it as "stale" is mendixlabs/mxcli#758.
func TestAttrRefBelongsTo(t *testing.T) {
tests := []struct {
ref string
module string
entity string
want bool
}{
{"Sec.Item.OwnField", "Sec", "Item", true},
{"sec.item.OwnField", "Sec", "Item", true}, // Mendix names are case-insensitive
{"Sec.Base.SharedField", "Sec", "Item", false},
{"System.FileDocument.Name", "Sec", "Attachment", false},
{"Other.Base.Field", "Sec", "Item", false},
{"NoDots", "Sec", "Item", false},
{"", "Sec", "Item", false},
// A same-named entity in another module is NOT this entity.
{"Other.Item.OwnField", "Sec", "Item", false},
}
for _, tc := range tests {
if got := attrRefBelongsTo(tc.ref, tc.module, tc.entity); got != tc.want {
t.Errorf("attrRefBelongsTo(%q, %q, %q) = %v, want %v",
tc.ref, tc.module, tc.entity, got, tc.want)
}
}
}
34 changes: 32 additions & 2 deletions mdl/backend/modelsdk/domainmodel_security_write.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package modelsdkbackend
import (
"fmt"
"sort"
"strings"

"github.com/mendixlabs/mxcli/mdl/backend"
"github.com/mendixlabs/mxcli/mdl/types"
Expand Down Expand Up @@ -412,15 +413,30 @@ func (b *Backend) ReconcileMemberAccesses(unitID model.ID, moduleName string) (i
}
switch attrRef, assocRef := ma.AttributeQualifiedName(), ma.AssociationQualifiedName(); {
case attrRef != "":
if attrSet[attrRef] {
switch {
case attrSet[attrRef]:
covAttr[attrRef] = true
if calcSet[attrRef] {
if r := ma.AccessRights(); r == "ReadWrite" || r == "WriteOnly" {
ma.SetAccessRights("ReadOnly")
changed = true
}
}
} else {
case !attrRefBelongsTo(attrRef, moduleName, entityName):
// An attribute reference is qualified against the entity that
// DECLARES it, so an inherited member carries an ancestor's name
// rather than this entity's. attrSet holds only this entity's own
// attributes, so every inherited entry looked stale and was
// deleted — silently, on any write touching the module, and
// immediately after the GRANT that had just written it correctly
// (mendixlabs/mxcli#758). The ancestor may live in another module
// or in System, neither loaded here, so an inherited reference
// cannot be validated at this layer at all. Preserve what cannot
// be checked instead of dropping it.
covAttr[attrRef] = true
default:
// Genuinely stale: the reference claims to be this entity's own
// attribute and the entity no longer has it.
rule.RemoveMemberAccesses(i)
changed = true
}
Expand Down Expand Up @@ -489,3 +505,17 @@ func newMemberAccess(rights, qualifiedName string, isAttr bool) *genDm.MemberAcc
assignID(ma)
return ma
}

// attrRefBelongsTo reports whether a MemberAccess attribute reference
// ("Module.Entity.Attribute") names one of the given entity's OWN attributes,
// rather than one inherited from an ancestor.
//
// Only an own reference can be validated from a single domain model: an ancestor
// may live in another module or in System, neither of which is loaded here.
func attrRefBelongsTo(attrRef, moduleName, entityName string) bool {
idx := strings.LastIndex(attrRef, ".")
if idx < 0 {
return false
}
return strings.EqualFold(attrRef[:idx], moduleName+"."+entityName)
}
Loading
Loading