From db566efe068d06047ea3b8b8a34eb076230d65de Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 29 Sep 2026 07:50:46 +0000 Subject: [PATCH] feat(lint): expose the user role's security flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A user role carries four security-governance settings that no rule could read: Studio Pro's per-role "Check security" flag, and the three user-management grants (ManageAllRoles, ManageUsersWithoutRoles, ManageableRoles). All four are already modelled on sdk/security.UserRole and already read from BSON by security_read.go — UserRoleInfo simply did not carry them, so they stopped at the linter boundary. This adds the fields and exposes them as check_security, manage_all_roles, manage_users_without_roles and manageable_roles. Motivating rules, neither expressible today: - "User roles with a certain amount of module roles should be checked for security" — asserts check_security is on for any role above a module-role threshold. Without the field a rule can only count module roles, which is a different and much weaker claim. - "Only 'admin' roles should manage other users" — needs the user-management grants. No catalog change, so no CatalogSchemaVersion bump: these come from the MPR reader via GetProjectSecurity, not from a catalog table. --- .../skills/mendix/write-lint-rules/SKILL.md | 4 ++++ mdl/linter/context.go | 20 ++++++++++++++++--- mdl/linter/starlark.go | 14 ++++++++++--- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/.claude/skills/mendix/write-lint-rules/SKILL.md b/.claude/skills/mendix/write-lint-rules/SKILL.md index 4f2457759..2f8d51d4b 100644 --- a/.claude/skills/mendix/write-lint-rules/SKILL.md +++ b/.claude/skills/mendix/write-lint-rules/SKILL.md @@ -497,6 +497,10 @@ Returned by `permissions()` (all types) or `permissions_for()` (entity-specific) | `name` | string | `"Administrator"` | | `is_anonymous` | bool | True if this is the anonymous/guest role | | `module_roles` | list of string | `["Sales.Admin", "HR.Viewer"]` | +| `check_security` | bool | Studio Pro's per-role "Check security" flag | +| `manage_all_roles` | bool | The role may hand out every user role | +| `manage_users_without_roles` | bool | The role may manage users that have no role | +| `manageable_roles` | list of string | The user roles this role may hand out, when not all | ### module_role | Property | Type | Example | diff --git a/mdl/linter/context.go b/mdl/linter/context.go index e27ef4fbf..41b8f986e 100644 --- a/mdl/linter/context.go +++ b/mdl/linter/context.go @@ -485,6 +485,16 @@ type UserRoleInfo struct { Name string IsAnonymous bool ModuleRoles []string + // CheckSecurity is Studio Pro's per-role "Check security" flag. With it on, + // the Modeler verifies that the role's access rules actually grant what its + // pages and microflows need — the setting a rule of the form "security + // should be checked for each project role" asserts. + CheckSecurity bool + // ManageAllRoles, ManageUsersWithoutRoles and ManageableRoles are the + // user-management grants: which other roles this role may hand out. + ManageAllRoles bool + ManageUsersWithoutRoles bool + ManageableRoles []string } // UserRoles returns the user roles from project security. @@ -501,9 +511,13 @@ func (ctx *LintContext) UserRoles() []UserRoleInfo { var roles []UserRoleInfo for _, ur := range ps.UserRoles { roles = append(roles, UserRoleInfo{ - Name: ur.Name, - IsAnonymous: ur.Name == ps.GuestUserRole, - ModuleRoles: ur.ModuleRoles, + Name: ur.Name, + IsAnonymous: ur.Name == ps.GuestUserRole, + ModuleRoles: ur.ModuleRoles, + CheckSecurity: ur.CheckSecurity, + ManageAllRoles: ur.ManageAllRoles, + ManageUsersWithoutRoles: ur.ManageUsersWithoutRoles, + ManageableRoles: ur.ManageableRoles, }) } return roles diff --git a/mdl/linter/starlark.go b/mdl/linter/starlark.go index a854bfa0c..50e827512 100644 --- a/mdl/linter/starlark.go +++ b/mdl/linter/starlark.go @@ -1045,10 +1045,18 @@ func userRoleToStarlark(ur UserRoleInfo) starlark.Value { for _, mr := range ur.ModuleRoles { moduleRoles = append(moduleRoles, starlark.String(mr)) } + var manageableRoles []starlark.Value + for _, mr := range ur.ManageableRoles { + manageableRoles = append(manageableRoles, starlark.String(mr)) + } return starlarkstruct.FromStringDict(starlark.String("user_role"), starlark.StringDict{ - "name": starlark.String(ur.Name), - "is_anonymous": starlark.Bool(ur.IsAnonymous), - "module_roles": starlark.NewList(moduleRoles), + "name": starlark.String(ur.Name), + "is_anonymous": starlark.Bool(ur.IsAnonymous), + "module_roles": starlark.NewList(moduleRoles), + "check_security": starlark.Bool(ur.CheckSecurity), + "manage_all_roles": starlark.Bool(ur.ManageAllRoles), + "manage_users_without_roles": starlark.Bool(ur.ManageUsersWithoutRoles), + "manageable_roles": starlark.NewList(manageableRoles), }) }