enhancement
major
minor
major
minor
major
#29798
Derived reference reads as empty in the commit check (no user): result filter of AttributeByExpression stays on
A derived reference (<storage-algorithm><query>) is computed with security disabled in its expression, but AttributeByExpression#getAttributeValue runs it through a QueryExecutor whose result filter for the reading user's rights stays active. Where no user is logged in - the commit check of ModelService that runs the <constraint-by-expression> checks, tests, background work - the derived reference reads as empty for every object that was not created in the current transaction. (TL 8.0.0-alpha10)
Reproduction
A class m:T with a derived reference derivedRef (<storage-algorithm><query><expr>t -> $t.get(`m:T#stored`)</expr></query></storage-algorithm>) and a stored reference stored, and a constraint on any attribute of m:T:
value -> obj -> if($obj.get(`m:T#derivedRef`) == null, "empty", null)
- Commit an object a` and an object `t with stored = a (as a user).
- In a context without a person (e.g. a JUnit test using KBSetup), modify t and commit.
Expected: the commit succeeds, derivedRef is a`. Actual: the constraint sees `derivedRef == null and rejects the commit. Reading t.tValueByName("derivedRef") without a person returns null, with the root person it returns a.
Context
Seen in E-Home with ehome.operational:ParameterValue#valueType (a derived reference to the parameter type): when TestEtsImport imported a second project that uses an already known application program, every enumeration setting was rejected with "The entry … is no value of the type null".
Workaround in the app: the constraint scripts call a TL-Script function implemented in Java (parameterValueType) that navigates the stored references, instead of reading the derived reference.
Expected behavior: a derived attribute that is computed with security disabled must also return its value unfiltered where no user is present (the commit check is a system operation), or the commit check should run the constraints as the system user.
Lösung
The root cause is that the engine had many ways to set up a thread context and several independent ways to switch off access checks (a system: context id, the super-user counter ThreadContext.pushSuperUser(), ModelAccessRights.uncheckedSecurity(), the admin flag of the context person), while a context without a person and without the system: id (every JUnit test, JMS consumers, a nested setup inside an existing interaction) denied everything.
The context setup is consolidated:
- Access checks depend on a single, job-scoped flag ThreadContext.isSecurityBypassed(). With the flag set, every access is allowed. Otherwise the person of the context is checked; without a person, access is denied (an unauthenticated request keeps no rights). The system: context id only labels the author of transactions.
- There are exactly two ways to set up a context. Both always take effect: without an interaction a new one is created for the job; inside an existing interaction the identity of the caller's sub-session is switched for the job and restored afterwards (also on exception).
- ThreadContext.inSystemContext(caller, job) (and inSystemContext(caller, name, job) for a named context id): no person, security checks bypassed.
- TLContextManager.inPersonContext(person, job): the given person, security checked.
- ThreadContext.withoutSecurityChecks(job) switches the checks off within the current context and keeps the person, so that transactions are still attributed to that person. ThreadContext.withSecurityChecks(enabled, job) switches them on or off explicitly.
- ThreadContext.isAdmin() is the one check whether the current context has administrative rights: true while the security checks are bypassed, or if the person of the current context is an administrator. Every check of the current user's administrative rights uses it (no more TLContext.isAdmin() spelling, no Person.isAdmin(currentUser) / currentPerson.isAdmin() for the current user). The person part is contributed by the context through the protected hook ThreadContext#isUserAdministrator() (overridden in TLContext).
- The commit check of ModelService evaluates the model constraints with the security checks bypassed, so model integrity does not depend on the read rights of the committing user. Interactive form validation is unchanged and still uses the rights of the user.
- JUnit tests run in the system context; tests that check access rights run in a person context (AbstractSearchExpressionTest.checksSecurity() / initialUser()).
- Call sites that switched the person by hand (ExportQueueManager, ReportQueue, MaintenanceWindowTimer, ScriptTask) use the two setups. LayoutBasedSecurity now always computes the security from the global layouts, also when triggered from a user request.
Migration
The following API is removed; replace usages as described:
- ThreadContextManager.inInteraction(String, ...), ThreadContextManager.inSystemInteraction(Class, ...) -> ThreadContext.inSystemContext(Class, ...).
- TLContextManager.inRootPersonContext(...) -> ThreadContext.inSystemContext(Class, ...), or TLContextManager.inPersonContext(PersonManager.getManager().getRoot(), ...).
- ThreadContext.pushSuperUser() / popSuperUser() / resetSuperUser() and ModelAccessRights.uncheckedSecurity(...) -> ThreadContext.withoutSecurityChecks(...) (keeps the person) or ThreadContext.inSystemContext(...) (no person).
- TLContext.setCurrentPerson(...) and switching the user by setPerson(...) / setContextId(...) -> TLContextManager.inPersonContext(person, ...).
- The protected hook ThreadContext#isAdminContext() is renamed to isUserAdministrator(); a context subclass overriding it must rename its override. Whether the current context has administrative rights is asked with ThreadContext.isAdmin() (also where TLContext.isAdmin() was written).
- ThreadContext.inSystemContext(...) / TLContextManager.inPersonContext(...) now also switch the identity when an interaction is already present. A job must not log in or set a person meant to outlive it: look up the account inside the job, return it, and continue outside.
- Setting a person on the current context no longer switches the security checks on or off. Tests asserting access denials must run the checked part in TLContextManager.inPersonContext(person, ...), since tests run in the system context.