major
#29529
Stellvertreter als explizite Sicherheitsaktion im Einstellungsdialog der TL Views benennen (mit Passwortbestätigung, auch für die Passwortänderung)
Goal
Follow-up to #29525, which offers the account area of the app bar as a core view of the com.top_logic.layout.view layer. What a user settles about their own account there is the language, the theme, where they start, the timezone, the country and their second factor. Naming a deputy ("Stellvertreter") - somebody who acts with exactly this account's rights while the user is away - is not among them, although the engine has carried the concept for as long as it has carried accounts.
Where it stands
The model has it. Every tl.accounts:Person owns a representativeGroup, a tl.accounts:RepresentativeGroup created together with the account, whose members are that account's deputies. BoundedRole grants those members the roles of the account they stand in for, so membership of that group is what actually delegates the rights. The inverse, Person#deputyFor, says which accounts one stands in for.
The classic UI offers it. The account editor puts the group's members in front of the user through com.top_logic.layout.formeditor.accounts.RepresentativeField, a form-editor element that borrows the foreign attribute RepresentativeGroup#members and labels it "Stellvertreter"; the contact person editor does the same by hand (EditPersonContactComponent.PARAM_REPRESENTATIVES, applied by ContactApplyHandler).
The view layer does not. settings-tabs.view.xml holds the timezone and the country on the account tab and the second factor on the security tab, and the account menu holds the one-decision preferences. Nothing anywhere in the layer reaches the representative group. admin/access-control/accounts.view.xml offers an administrator the representativeGroup reference and deputyFor - the group as an object, not the people in it - so even there the deputies of an account cannot be named.
Solution
An explicit action, not a form field. Naming a deputy hands over every role the account carries, so it is not entered as one more field of the account form. It joins the security tab of the settings dialog beside the password change and the second factor: a status line names the current deputies (or says that none is named), a second line - shown only when there is something to say - names the accounts the user stands in for, and a button "Name deputies" ("Stellvertreter benennen") opens a dialog of its own. The lines follow a change without reopening the dialog: the group sits on a channel of its own, and a channel observes the object it holds.
The deputies dialog (deputies.view.xml) is bound to the account's representative group. It states the consequence in plain words, offers one multi-select field over RepresentativeGroup#members - the candidates are the ones the model offers, i.e. the options annotation of the contact module (every account except the owner), which an application narrows further through its own model overlay - and applies through a button-bar command: identity verification (below), confirmation, then the change inside a transaction, then the dialog closes. Both guards stand outside the transaction.
Identity verification as a reusable guard. The view layer gains <verify-identity/> (VerifyIdentityAction), an interruptible action placed in a command chain like <confirm>. It proves that the person at the keyboard is the account holder the way the session was established:
- A session logged in through an external identity provider (OpenID Connect via pac4j) is sent back to that provider. A dialog in the main window offers the login method's own button; it opens the provider's login in a second browser window with the re-authentication URL of the login method (LoginMethod#getReauthenticationUrl). The provider authenticates the user afresh (max_age=0, prompt=login), the callback returns to the authentication servlet in the same HTTP session carrying a single-use token (ExternalAuthenticationServlet.VERIFICATION_PARAM), and the servlet - without logging in, since the session already exists - completes the PendingIdentityVerification registered under that token (IdentityVerifications, session-scoped, ten minutes, bound to the expected account): the dialog in the main window closes by itself and the chain continues in that window. The second window is a window of the page that opened it, so the confirmation page closes it; where a browser refuses to close it, the page says that the identity was confirmed and the window can be closed. What the chain does after the confirmation - the password change, the confirmation dialog of the deputies - runs in the window that asked; a failure of it is shown there like the failure of any command, and the request that brought the confirmation answers for the identity alone. A different user, an expired or unknown token leave the chain waiting; the result page in the second window says so.
- Any other session is asked for its password in a small dialog (PasswordPromptDialogControl), checked through the account's AuthenticationDevice - the same check a login makes, so LDAP accounts verify against the directory. A wrong password keeps the prompt open with the error under the emptied input.
- Neither available - no session account, the anonymous account, a headless context - the chain aborts: the guard fails closed.
Optional title and message name what is being confirmed. The guard also protects the voluntary password change (change-password.view.xml); the forced password change after a login with an expired password keeps its flow, since the account authenticated a moment ago.
Only a password the application holds can be changed. An account authenticated by a device that keeps the password elsewhere - a directory service, an identity provider - cannot change it here, so the settings dialog offers "Change password" only where it can be applied: the TL-Script function accountPasswordChangeAllowed() (AccountFunctions, asking the account's device for allowPwdChange()) gates the button through the generic <visible-if> rule, and ChangePasswordApplyAction refuses such an account with a message of its own before anything is asked of the device. A password changed from the settings dialog closes the dialog once it is applied.
pac4j. For every configured OpenID Connect client the application registers a derived client named <client>-reauthentication with the same settings plus max_age=0 and prompt=login, so ordinary logins keep single sign-on; the login method answers a re-authentication URL only for the session it established and drops the stored pac4j profile so that the security filter goes to the provider. The callback filter keeps the session (renewSession=false): a login creates a session of its own afterwards, a confirmation belongs to the running one. The login URL selects its client with the parameter the security filter reads (force_client).
React layer, no model change. The representative group, its members and the options restriction stay as they are. The reverse direction ("you stand in for") is computed in the view by asking the accounts whose representative group the user is a member of, so it does not rely on the derivation of deputyFor, which only the contact module supplies. The React demo gains the pac4j module and a Keycloak client configuration overridable through the environment.
Read access to accounts is an application decision. Every value a view computes is filtered by read access, and the engine grants an account read access to its own account only (PersonalProfile), so a user who is not an administrator sees neither the representative group nor the other accounts. The React demo grants every member of its users group read access to all accounts and groups (role demo.react.AccountReader, granted through role rules in demoReactConf.config.xml); an application decides for itself whether and which accounts a user may see.
Repaired on the way
- TLObjectOverlay, the editing buffer of a view form, answered every back-reference lookup (tReferers) with nothing, so an options expression navigating backwards from the edited object - the contact module's owner-excluding filter on RepresentativeGroup#members - filtered nothing and offered the account as its own deputy. The overlay now answers back-references from the object it stands for.
- WithTransactionAction did not report that its nested actions apply the form state, so a form command written as <with-transaction><store-form-state/></with-transaction> - the Save of the settings dialog among them - lost the rule that keeps it disabled while the form is invalid.
- ReactButtonControl can open its navigation target in a browser window of its own; ReactWindowReplay.inWindow is the shared way of acting on a window from a thread serving another one.
- The change-password dialog opened from the settings dialog stayed open after the password was changed.
Observed, not changed
- TL-Script referers evaluated in a <derived-channel> is filtered by the read access to the base object: a user who is merely a member of a group cannot navigate from the group to its owner, while all(...) and forward get(...) are not filtered.
- A re-authentication completed by a different user at the provider does not touch the application session, but it switches the provider's own single-sign-on session to that user; the application's logout does not end the provider's session.
- An account that authenticates neither by password nor through an external provider cannot pass the guard.
Out of scope
Delegation limited to a period, and switching into another account's session. This ticket is about naming the deputies of one's own account from the account area, with the rights the engine already derives from that.