From f51aeabbda0292c7fe00196e48dc4599ba6c602b Mon Sep 17 00:00:00 2001 From: delchev Date: Thu, 13 Aug 2026 15:30:31 +0300 Subject: [PATCH] fix(security): act-as - the armed state expires, is visible everywhere, and never stamps the acted-as person on a role-addressed task (#6694) An act-as (delegated entry) arming worked as designed except for the one thing nobody arms it for: being FORGOTTEN. An arming made for a single delegated timesheet entry was still armed hours later, and from then on the Inbox served the acted-as person's world - a live approval step looked like a broken process, and the steps that were worked through the armed session were completed with assignee = the acted-as person, attributing decisions to an employee who never made them. - Expiry: ActAsFacade stamps the arming time and drops the override after DIRIGIBLE_ACT_AS_TTL_SECONDS (default 1800). The window is ABSOLUTE - a sliding one would be renewed forever by the Inbox's own 30s poll - and the decision fails closed on an arming it cannot date. /services/core/actas now reports expiresAt, and both shells render the deadline. - Visibility: GET /services/inbox/act-as reports the armed identity and how many of the REAL user's tasks the arming hides; the shared Inbox renders a persistent strip + exit and says whose list it is showing in its empty state. The Applications shell gains the banner, exit and "Stop acting as" entry that only the Personal shell had - the armed identity follows the session into the back office. - Attribution: a CLAIM assigns to the acting identity ONLY for a task addressed to that person as a candidate user. A task reached through the real user's own roles is claimed by the real user, whatever is armed; stamping the acted-as person there corrupts the audit trail and strands the task on an identity the claim just removed from every group query. ActAsFacadeTest covers the expiry decision; ActAsSessionIT (HTTP, in the PR smoke gate) covers the reported deadline, an arming expiring with nobody disarming it, and - over a real ADMINISTRATOR-addressed user task - a claim from an armed session landing on the real user while the armed Inbox reports it as hidden. Co-Authored-By: Claude Opus 5 --- PERSONALIZATION_PLAN.md | 1 + .../components/api/security/ActAsFacade.java | 81 +++++- .../api/security/ActAsFacadeTest.java | 59 ++++ .../endpoint/ActAsEndpoint.java | 19 +- .../engine/bpm/flowable/TaskService.java | 9 + .../bpm/flowable/config/TaskServiceImpl.java | 8 + .../flowable/endpoint/BpmInboxEndpoint.java | 51 +++- .../bpm/flowable/service/BpmService.java | 5 + .../application-core/i18n/bg-BG/shell.json | 8 +- .../application-core/i18n/en-US/shell.json | 8 +- .../shell/js/components/pages/inboxPage.js | 23 ++ .../application-core/shell/views/_inbox.html | 20 +- .../META-INF/dirigible/application/index.html | 26 +- .../dirigible/application/js/appShell.js | 20 +- .../META-INF/dirigible/personal/index.html | 1 + .../dirigible/personal/js/appShell.js | 13 +- .../commons/config/DirigibleConfig.java | 9 +- .../integration/tests/api/ActAsSessionIT.java | 254 ++++++++++++++++++ 18 files changed, 589 insertions(+), 26 deletions(-) create mode 100644 components/api/api-security/src/test/java/org/eclipse/dirigible/components/api/security/ActAsFacadeTest.java create mode 100644 tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/ActAsSessionIT.java diff --git a/PERSONALIZATION_PLAN.md b/PERSONALIZATION_PLAN.md index 08a8be40793..f853095d297 100644 --- a/PERSONALIZATION_PLAN.md +++ b/PERSONALIZATION_PLAN.md @@ -69,6 +69,7 @@ Semantics: | **E** | Collection-driven generation: `schedules[].generate.children[]` — one child per element of a source collection (`forEach: {entity, match}` over a LOCAL entity, or `forEach: {days: workingDays}` + `dayField`, defaults incl. typed numeric literals), `parent:` back-FK, nested one more level (line → allocations, depth ≤ 2). Pre-rendered in glue (the expansions convention); the Job template stays shape-only | **done** | | **F** | Document item dialog honors `readOnly`: read-only columns render as values, not controls (saves already ignored them - the input was fake editability) | **done** | | **G** | **Act as (delegated entry)**: an entitled user (ADMINISTRATOR) arms an acting identity for the session (`/services/core/actas`, `ActAsFacade`); the generated personal controllers resolve `me()` against `User.getEffectiveName()` and the Inbox assignee query serves the acting identity's tasks, so a manager fills/submits in a worker's name using the worker's OWN surfaces. Roles, security and audit stamping stay the REAL user's (CreatedBy shows who really entered it); `sensitive`/`personalReadOnly` hold unchanged. Shell UX: Personal-shell banner + switcher, Applications-shell "Enter data as..." entry point | **done** | +| **G2** | **A forgotten arming can no longer hide the real identity** (#6694). The armed state **expires** on its own after `DIRIGIBLE_ACT_AS_TTL_SECONDS` (30 min; the window is absolute — activity never renews it, or an Inbox poll would keep it alive forever). A **claim** stamps the acting identity only for a task addressed to that person as a candidate user; a back-office group task reached through the REAL user's roles is claimed by the real user, so an approval is never attributed to someone who never made it (and never stranded on an identity the group query has just lost sight of). The **Inbox** states who is armed and how many of the real user's own tasks the arming hides (`GET /services/inbox/act-as`) instead of rendering an indistinguishable empty list, and the **Applications shell** carries the same banner + exit the Personal shell has — the armed identity follows the session into the back office, so its absence there is what let an arming be forgotten | **done** | ## Flagship application (the consuming suite's timesheets flow) diff --git a/components/api/api-security/src/main/java/org/eclipse/dirigible/components/api/security/ActAsFacade.java b/components/api/api-security/src/main/java/org/eclipse/dirigible/components/api/security/ActAsFacade.java index 10fac547782..d9b15551a1a 100644 --- a/components/api/api-security/src/main/java/org/eclipse/dirigible/components/api/security/ActAsFacade.java +++ b/components/api/api-security/src/main/java/org/eclipse/dirigible/components/api/security/ActAsFacade.java @@ -9,6 +9,7 @@ */ package org.eclipse.dirigible.components.api.security; +import org.eclipse.dirigible.commons.config.DirigibleConfig; import org.eclipse.dirigible.components.api.http.HttpSessionFacade; import org.eclipse.dirigible.components.base.http.roles.Roles; import org.slf4j.Logger; @@ -31,6 +32,12 @@ *
  • Only the personal-identity resolution and the Inbox assignee filter read the override.
  • *
  • The override lives in the server-side HTTP session, never in a client-supplied header, and * the entitlement is re-checked on EVERY read - a revoked role kills the override mid-session.
  • + *
  • The armed state EXPIRES on its own after {@link DirigibleConfig#ACT_AS_TTL_SECONDS} (30 + * minutes by default). The window is absolute - it starts at arming and no activity renews it - + * because the failure mode this guards against is a state that is never exited: while armed, the + * personal surfaces and the Inbox's assignee query serve the acting identity's world instead of the + * real user's, and a forgotten override makes the real user's own tasks look like they were never + * raised.
  • * */ public final class ActAsFacade { @@ -40,6 +47,9 @@ public final class ActAsFacade { /** The HTTP-session attribute carrying the acting identity's username. */ private static final String SESSION_ATTRIBUTE = "dirigible-act-as-user"; + /** The HTTP-session attribute carrying the epoch-milliseconds the identity was armed at. */ + private static final String SESSION_ATTRIBUTE_ARMED_AT = "dirigible-act-as-armed-at"; + private ActAsFacade() {} /** @@ -52,9 +62,10 @@ public static boolean isEntitled() { } /** - * The armed acting identity, or null when none is armed, the session is not valid, or the real user - * is not (or no longer) entitled. The entitlement re-check on every read is what makes a - * mid-session role revocation effective immediately. + * The armed acting identity, or null when none is armed, the session is not valid, the real user is + * not (or no longer) entitled, or the delegated-entry window has elapsed. The entitlement re-check + * on every read is what makes a mid-session role revocation effective immediately; the expiry check + * is what makes a forgotten arming harmless. * * @return the acting username or null */ @@ -63,7 +74,64 @@ public static String actingAs() { return null; } String acting = HttpSessionFacade.getAttribute(SESSION_ATTRIBUTE); - return acting == null || acting.isBlank() ? null : acting; + if (acting == null || acting.isBlank()) { + return null; + } + if (isExpired(HttpSessionFacade.getAttribute(SESSION_ATTRIBUTE_ARMED_AT), System.currentTimeMillis())) { + clear(); + logger.info("Act-as EXPIRED: [{}] no longer acts as [{}] - the {}s delegated-entry window has elapsed", UserFacade.getName(), + acting, ttlSeconds()); + return null; + } + return acting; + } + + /** + * When the currently armed state expires, or null when nothing is armed. + * + * @return the expiry as epoch milliseconds, or null + */ + public static Long expiresAt() { + if (actingAs() == null) { + return null; + } + Long armedAt = armedAt(HttpSessionFacade.getAttribute(SESSION_ATTRIBUTE_ARMED_AT)); + return armedAt == null ? null : armedAt + ttlSeconds() * 1000L; + } + + /** + * Whether an arming stamped at the given attribute value has run out. Fails CLOSED: a missing or + * unparsable stamp is treated as expired, because an armed state we cannot date is an armed state + * we cannot trust to end. + * + * @param armedAtAttribute the raw session attribute value + * @param now the current epoch milliseconds + * @return true when the state must be dropped + */ + static boolean isExpired(String armedAtAttribute, long now) { + Long armedAt = armedAt(armedAtAttribute); + return armedAt == null || now - armedAt >= ttlSeconds() * 1000L; + } + + private static Long armedAt(String armedAtAttribute) { + if (armedAtAttribute == null || armedAtAttribute.isBlank()) { + return null; + } + try { + return Long.valueOf(armedAtAttribute.trim()); + } catch (NumberFormatException e) { + logger.warn("Act-as arming timestamp [{}] is not a number - treating the state as expired", armedAtAttribute, e); + return null; + } + } + + private static int ttlSeconds() { + return DirigibleConfig.ACT_AS_TTL_SECONDS.getIntValue(); + } + + private static void clear() { + HttpSessionFacade.removeAttribute(SESSION_ATTRIBUTE); + HttpSessionFacade.removeAttribute(SESSION_ATTRIBUTE_ARMED_AT); } /** @@ -95,7 +163,8 @@ public static void arm(String username) { } String acting = username.trim(); HttpSessionFacade.setAttribute(SESSION_ATTRIBUTE, acting); - logger.info("Act-as ARMED: [{}] now acts as [{}] for this session", UserFacade.getName(), acting); + HttpSessionFacade.setAttribute(SESSION_ATTRIBUTE_ARMED_AT, Long.toString(System.currentTimeMillis())); + logger.info("Act-as ARMED: [{}] now acts as [{}] for the next {}s", UserFacade.getName(), acting, ttlSeconds()); } /** Disarms the acting identity for the current session. Audit-logged. */ @@ -104,7 +173,7 @@ public static void disarm() { return; } String acting = HttpSessionFacade.getAttribute(SESSION_ATTRIBUTE); - HttpSessionFacade.removeAttribute(SESSION_ATTRIBUTE); + clear(); if (acting != null && !acting.isBlank()) { logger.info("Act-as DISARMED: [{}] no longer acts as [{}]", UserFacade.getName(), acting); } diff --git a/components/api/api-security/src/test/java/org/eclipse/dirigible/components/api/security/ActAsFacadeTest.java b/components/api/api-security/src/test/java/org/eclipse/dirigible/components/api/security/ActAsFacadeTest.java new file mode 100644 index 00000000000..820605fe510 --- /dev/null +++ b/components/api/api-security/src/test/java/org/eclipse/dirigible/components/api/security/ActAsFacadeTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2010-2026 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.components.api.security; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.eclipse.dirigible.commons.config.DirigibleConfig; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +/** + * The expiry decision behind the act-as (delegated entry) window: an arming that is never exited + * must stop being honoured on its own (#6694). + */ +class ActAsFacadeTest { + + private static final long NOW = 1_800_000_000_000L; + + @AfterEach + void restoreTheDefaultWindow() { + DirigibleConfig.ACT_AS_TTL_SECONDS.setStringValue(DirigibleConfig.ACT_AS_TTL_SECONDS.getDefaultValue()); + } + + @Test + void a_fresh_arming_is_honoured() { + assertThat(ActAsFacade.isExpired(Long.toString(NOW - 60_000), NOW)).isFalse(); + } + + @Test + void an_arming_older_than_the_window_is_dropped() { + long defaultWindowMillis = Integer.parseInt(DirigibleConfig.ACT_AS_TTL_SECONDS.getDefaultValue()) * 1000L; + + assertThat(ActAsFacade.isExpired(Long.toString(NOW - defaultWindowMillis - 1), NOW)).isTrue(); + } + + @Test + void the_window_follows_the_configuration() { + DirigibleConfig.ACT_AS_TTL_SECONDS.setStringValue("60"); + + assertThat(ActAsFacade.isExpired(Long.toString(NOW - 59_000), NOW)).isFalse(); + assertThat(ActAsFacade.isExpired(Long.toString(NOW - 61_000), NOW)).isTrue(); + } + + /** Fails closed: an armed state we cannot date is an armed state we cannot trust to end. */ + @Test + void an_undatable_arming_is_dropped() { + assertThat(ActAsFacade.isExpired(null, NOW)).isTrue(); + assertThat(ActAsFacade.isExpired(" ", NOW)).isTrue(); + assertThat(ActAsFacade.isExpired("not-a-timestamp", NOW)).isTrue(); + } + +} diff --git a/components/core/core-configurations/src/main/java/org/eclipse/dirigible/components/configurations/endpoint/ActAsEndpoint.java b/components/core/core-configurations/src/main/java/org/eclipse/dirigible/components/configurations/endpoint/ActAsEndpoint.java index 4c8879e84e3..7fff98d8908 100644 --- a/components/core/core-configurations/src/main/java/org/eclipse/dirigible/components/configurations/endpoint/ActAsEndpoint.java +++ b/components/core/core-configurations/src/main/java/org/eclipse/dirigible/components/configurations/endpoint/ActAsEndpoint.java @@ -31,8 +31,15 @@ @RequestMapping(BaseEndpoint.PREFIX_ENDPOINT_CORE + "actas") public class ActAsEndpoint extends BaseEndpoint { - /** The state the shells render from: may this user arm at all, and who is armed right now. */ - public record ActAsState(boolean entitled, String actingAs) { + /** + * The state the shells render from: may this user arm at all, who is armed right now, and when that + * arming expires on its own (epoch milliseconds; null when nothing is armed). + */ + public record ActAsState(boolean entitled, String actingAs, Long expiresAt) { + + static ActAsState current() { + return new ActAsState(ActAsFacade.isEntitled(), ActAsFacade.actingAs(), ActAsFacade.expiresAt()); + } } /** The arm request: the acting identity's username (e.g. the employee's e-mail). */ @@ -42,11 +49,11 @@ public record ArmRequest(String username) { /** * The current session's act-as state. * - * @return entitled + the armed acting identity (null when none) + * @return entitled + the armed acting identity (null when none) + its expiry */ @GetMapping public ResponseEntity state() { - return ResponseEntity.ok(new ActAsState(ActAsFacade.isEntitled(), ActAsFacade.actingAs())); + return ResponseEntity.ok(ActAsState.current()); } /** @@ -64,7 +71,7 @@ public ResponseEntity arm(@RequestBody ArmRequest request) { } catch (IllegalArgumentException e) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); } - return ResponseEntity.ok(new ActAsState(ActAsFacade.isEntitled(), ActAsFacade.actingAs())); + return ResponseEntity.ok(ActAsState.current()); } /** @@ -75,6 +82,6 @@ public ResponseEntity arm(@RequestBody ArmRequest request) { @DeleteMapping public ResponseEntity disarm() { ActAsFacade.disarm(); - return ResponseEntity.ok(new ActAsState(ActAsFacade.isEntitled(), ActAsFacade.actingAs())); + return ResponseEntity.ok(ActAsState.current()); } } diff --git a/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/TaskService.java b/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/TaskService.java index 184334c4937..6455c97ab6e 100644 --- a/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/TaskService.java +++ b/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/TaskService.java @@ -172,6 +172,15 @@ public interface TaskService { List findTasks(PrincipalType type); + /** + * Counts the tenant's tasks assigned to the given user, whoever is asking - unlike + * {@link #findTasks(PrincipalType)}, which serves the caller's own (act-as aware) world. + * + * @param assignee the assignee to count for + * @return the number of tasks assigned to that user + */ + long countTasksByAssignee(String assignee); + void completeTask(String taskId, Map variables); List getTaskIdentityLinks(String taskId); diff --git a/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/TaskServiceImpl.java b/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/TaskServiceImpl.java index 6328ee98187..f2e3ed660fe 100644 --- a/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/TaskServiceImpl.java +++ b/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/config/TaskServiceImpl.java @@ -230,6 +230,14 @@ public List findTasks(PrincipalType type) { return taskQuery.list(); } + @Override + public long countTasksByAssignee(String assignee) { + return flowableTaskService.createTaskQuery() + .taskTenantId(getTenantId()) + .taskAssignee(assignee) + .count(); + } + @Override public List getTaskIdentityLinks(String taskId) { flowableArtefactsValidator.validateTask(taskId); diff --git a/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/endpoint/BpmInboxEndpoint.java b/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/endpoint/BpmInboxEndpoint.java index ef5e84323f5..f38ede58098 100644 --- a/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/endpoint/BpmInboxEndpoint.java +++ b/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/endpoint/BpmInboxEndpoint.java @@ -128,9 +128,7 @@ public ResponseEntity executeTaskAction(@PathVariable("id") String taskI if (CLAIM.getActionName() .equals(actionData.getAction())) { - // under act-as (delegated entry) a claim assigns the task to the ACTING identity, so - // the flow's record of who owns the step matches whose work it is - bpmService.claimTask(taskId, ActAsFacade.effectiveUser()); + bpmService.claimTask(taskId, claimantFor(taskId)); } else if (UNCLAIM.getActionName() .equals(actionData.getAction())) { bpmService.unclaimTask(taskId); @@ -145,6 +143,53 @@ public ResponseEntity executeTaskAction(@PathVariable("id") String taskI .build(); } + /** + * Who a claim assigns the task to. Under act-as (delegated entry) that is the ACTING identity, but + * ONLY for a task addressed to that person - one where they are a candidate user, i.e. their own + * work the delegate is entering on their behalf. A task the caller reached through their OWN roles + * (a back-office group task: approve, issue, send) is claimed by the REAL user, whatever is armed: + * stamping the acted-as person there attributes a decision to someone who never made it, and + * strands the task on an identity whose group-candidate visibility the claim just removed it from. + * + * @param taskId the task about to be claimed + * @return the username to claim for + */ + private String claimantFor(String taskId) { + String realUser = UserFacade.getName(); + String acting = ActAsFacade.actingAs(); + if (acting == null) { + return realUser; + } + boolean addressedToActingIdentity = bpmService.getTaskIdentityLinks(taskId) + .stream() + .map(IdentityLinkInfo::getUserId) + .anyMatch(acting::equals); + if (addressedToActingIdentity) { + return acting; + } + logger.info("Act-as: task [{}] is not addressed to [{}] - claiming it for the real user [{}]", taskId, acting, realUser); + return realUser; + } + + /** + * What act-as is currently doing to this Inbox: who is armed, and how many of the REAL user's own + * assigned tasks the armed state is hiding. An armed session's assignee query serves the acting + * identity's world, so the real user's tasks silently vanish from their own Inbox - this is what + * lets the Inbox say so instead of rendering an indistinguishable empty state. + * + * @return the acting identity (null when none) and the hidden-task count + */ + @GetMapping(value = "/act-as") + public ResponseEntity getActAsState() { + String acting = ActAsFacade.actingAs(); + long hidden = acting == null ? 0 : bpmService.countTasksByAssignee(UserFacade.getName()); + return ResponseEntity.ok(new ActAsInboxState(acting, hidden)); + } + + /** The Inbox's act-as state: the armed acting identity and the real user's tasks it hides. */ + public record ActAsInboxState(String actingAs, long hiddenTasks) { + } + private void verifyCurrentUserHasPermissionForTask(String id) { Set userTaskIds = getUserTaskIds(); if (!userTaskIds.contains(id)) { diff --git a/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/service/BpmService.java b/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/service/BpmService.java index 5e50bb23b41..54af922b05c 100644 --- a/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/service/BpmService.java +++ b/components/engine/engine-bpm-flowable/src/main/java/org/eclipse/dirigible/components/engine/bpm/flowable/service/BpmService.java @@ -428,6 +428,11 @@ public List findTasks(PrincipalType type) { .findTasks(type); } + public long countTasksByAssignee(String assignee) { + return bpmProviderFlowable.getTaskService() + .countTasksByAssignee(assignee); + } + public long processDefinitionsCount() { return bpmProviderFlowable.processDefinitionsCount(); } diff --git a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/i18n/bg-BG/shell.json b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/i18n/bg-BG/shell.json index 72970764e84..2f6127f55de 100644 --- a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/i18n/bg-BG/shell.json +++ b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/i18n/bg-BG/shell.json @@ -29,7 +29,8 @@ "startShort": "Започни", "bannerActing": "Действате като", "bannerNote": "записите се водят на името на този човек; журналът пази вашето", - "exit": "Изход" + "exit": "Изход", + "bannerUntil": "до {{time}}" }, "notifications": { "title": "Известия", @@ -57,7 +58,10 @@ "mine": "Моя", "available": "Свободна", "assignedToMe": "Възложена на мен", - "ref": "Реф. {{key}}" + "ref": "Реф. {{key}}", + "actAsHidden": "Действате като {{person}} — {{count}} от вашите собствени задачи са скрити", + "actAsServing": "Действате като {{person}} — тази пощенска кутия показва неговите задачи, не вашите", + "noTasksActAsHint": "Действате като {{person}} и този списък е негов. Излезте, за да видите своите задачи." }, "documents": { "newFolder": "Нова папка", diff --git a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/i18n/en-US/shell.json b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/i18n/en-US/shell.json index d47f776b3da..15694810804 100644 --- a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/i18n/en-US/shell.json +++ b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/i18n/en-US/shell.json @@ -29,7 +29,8 @@ "startShort": "Start", "bannerActing": "Acting as", "bannerNote": "entries are recorded in this person's name; the log keeps yours", - "exit": "Exit" + "exit": "Exit", + "bannerUntil": "until {{time}}" }, "notifications": { "title": "Notifications", @@ -57,7 +58,10 @@ "mine": "Mine", "available": "Available", "assignedToMe": "Assigned to me", - "ref": "Ref {{key}}" + "ref": "Ref {{key}}", + "actAsHidden": "Acting as {{person}} — {{count}} of your own tasks are hidden", + "actAsServing": "Acting as {{person}} — this Inbox serves their tasks, not yours", + "noTasksActAsHint": "You are acting as {{person}} and this list is theirs. Exit to see your own tasks." }, "documents": { "newFolder": "New folder", diff --git a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/pages/inboxPage.js b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/pages/inboxPage.js index cb3709901b7..73fd965c4ed 100644 --- a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/pages/inboxPage.js +++ b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/js/components/pages/inboxPage.js @@ -27,6 +27,12 @@ document.addEventListener('alpine:init', () => { _timer: null, _onMessage: null, + // Act-as (delegated entry): while armed, this Inbox serves the ACTING identity's assigned + // tasks, so the real user's own tasks are not in the list. Rendering that as a plain empty + // list is indistinguishable from "nothing to do" — a forgotten arming looked exactly like a + // broken process (#6694) — so the page states who is armed and how much it is hiding. + actAs: { acting: null, hiddenTasks: 0 }, + init() { // The store self-loads at startup; refresh on entry so the list is current. this.refresh(); @@ -78,7 +84,24 @@ document.addEventListener('alpine:init', () => { } }, + async loadActAs() { + try { + const state = await App.services.api.get('/services/inbox/act-as', { baseUrl: '' }); + this.actAs = { acting: state.actingAs || null, hiddenTasks: state.hiddenTasks || 0 }; + } catch (e) { + console.error('inbox: unable to load the act-as state', e); + } + }, + + // Exiting reloads: the whole shell (banner, personal pages, hosted apps) resolves under the + // armed identity, so a partial in-place update would leave half the UI acting as someone else. + async exitActAs() { + const res = await fetch('/services/core/actas', { method: 'DELETE', credentials: 'same-origin' }); + if (res.ok) window.location.reload(); + }, + async refresh() { + await this.loadActAs(); await Alpine.store('processTasks').refresh(); this.lastUpdated = new Date(); // A completed task drops out of the list — clear a stale selection. diff --git a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/views/_inbox.html b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/views/_inbox.html index a9a3f9a05c0..b66c35ae0eb 100644 --- a/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/views/_inbox.html +++ b/components/resources/application-core/src/main/resources/META-INF/dirigible/application-core/shell/views/_inbox.html @@ -28,6 +28,22 @@ + +
    + + +
    + +
    +
    @@ -44,7 +60,9 @@
    -
    +
    diff --git a/components/resources/resources-application/src/main/resources/META-INF/dirigible/application/index.html b/components/resources/resources-application/src/main/resources/META-INF/dirigible/application/index.html index 26f8077a89b..6a610e1d9dc 100644 --- a/components/resources/resources-application/src/main/resources/META-INF/dirigible/application/index.html +++ b/components/resources/resources-application/src/main/resources/META-INF/dirigible/application/index.html @@ -238,11 +238,16 @@

    -