enhancement
major
minor
major
minor
major
#29564
SecurityConfigurationService: an application cannot add a role to a framework type's grant — a <class> grant is keyed by operation and replaces the earlier entry, silently revoking the framework's roles
Affects 8.0.0-SNAPSHOT (2026-09-11), tl-core (SecurityConfigurationService, AccessRightsConfig).
tl-element grants PersonalProfile the Read operation on tl.accounts:Person (personalProfileSecurity.config.xml). An application whose accountants must read every account (a work list of employees is a table of Person rows, and a TL-Script result is filtered for the reader's read rights) adds
{{{#!xml <class name="tl.accounts:Person">
<grant inherit="true" operation="Read" roles="expenses.accountant"/>
</class> }}}
Grants are a map keyed by the operation (AccessRightsConfig.java:27-36), so the application's entry **replaces** the framework's: PersonalProfile loses its read on the account and an ordinary user can no longer read their own account. Nothing warns. The application has to repeat the framework's roles (roles="PersonalProfile, expenses.accountant") and thereby hard-wires knowledge of another module's security configuration.
Expected: either the role lists of grants for the same operation are merged across configuration layers (additive, in line with the additive combination of type and module grants in section 2.4.2 of the spec), or the typed-configuration list-merge directives (config:operation="add" on the roles) are supported for grants, or the override is at least logged with both role sets.
Lösung
The grants of a target element (<class>, <module>, <part>, <singleton>) are no longer a map keyed by the operation but an ordered list of access rules that is applied in sequence. Because configuration layers append to a list, an application's entries follow the framework's entries and never replace them.
- <grant operation="…" roles="…" inherit="…"/> adds the given roles to the operation (as before; existing configuration files are unchanged).
- <revoke operation="…" roles="…" inherit="…"/> removes the given roles from the operation. Without roles, the whole entry of the operation is removed, whichever rule contributed its roles: for an attribute this means that the attribute no longer restricts the operation and the decision of the class applies; for a type the operation is denied for every role until a later rule grants it again. A later layer can grant again after a revoke.
- Order of application: the module entries of a class (the additive baseline) are applied first, then the class's own entries in configuration order. Classes are evaluated generalization-first: a class starts with the roles its supertypes pass on. A <grant inherit="true"> adds to the roles passed on to specializations, a <revoke inherit="true"> removes from them; with inherit="false" only the exact type is affected. Attributes and singletons have no inheritance and simply apply their sequence.
Technically, AccessRightsConfig#getGrants() is a list of the abstract AccessRule (properties operation, roles, inherit) with the tagged subtypes AccessGrant (grant) and AccessRevoke (revoke). SecurityConfigurationService builds its lookup tables by applying the rules in the order described above. The specification specs/model-based-access-rights.md (sections 2.4.1, 2.4.2) is updated accordingly.
Verification: TestAccessRulesConfig (tl-core) reads two configuration layers and checks that the rules of both survive and that <revoke> is parsed. TestTLScriptSecurity (tl-model-search) runs the service on a second configuration layer that adds roles to the Project read grant, revokes roles on a specialization, revokes an attribute entry without roles, re-grants after a revoke and checks the inherit flag in both directions; the original rights of the first layer are asserted to be unchanged.
Migration
An application that declared a <grant> for an operation that a framework module already grants on the same type relied on replacing the framework's roles. Since grants are now additive, such an application keeps the framework's roles in addition to its own. To remove a framework role, add a <revoke operation="…" roles="…"/> in the application configuration; to replace the whole rule sequence of a type, use config:override="true" on the <class> element.
Code that calls AccessRightsConfig#getGrants() receives a List<AccessRule> instead of a map keyed by CommandGroupReference.