Repository navigation
fix: The site layout editor refuses a draft it can't tell apart from another user's, and a deleted user's draft sites are removed EXO-91427 - #604
Conversation
Backport evidenceSource: #590, squash-merged into Picks: Adaptations: none. The pick applied cleanly, and no commit was added. Verification
Level: N1, restated from the source. A backport from |
…another user's, and a deleted user's draft sites are removed EXO-91427 (#590) **Symptom**: some draft site names also read as another user's draft: a site name holding `_draft_` or ending with `_draft`, or a username starting with `draft_`. On such a name, opening the site layout editor removed and re-created the draft by name, which could drop the other user's draft. Once portal refuses such a name (Meeds-io/portal#1345), the editor stopped on a generic error instead. A deleted user's draft sites stayed, and a user whose name fits a leftover name could open them. **Cause**: `SiteLayoutService#createDraftSite` builds `<type>_<name>_draft_<user>` and removes any site of that name before creating it, without checking that the user may edit that name. Nothing removed a user's draft sites when the user was deleted. **Fix**: - `createDraftSite` checks `LayoutAclService#canEditSite(PortalConfig, String)`, a new overload, on the draft it built in memory, before removing or creating anything. When the check fails it refuses with `IllegalArgumentException("layout.siteDraftNameConflict")`. `SiteLayoutRest` answers 400 with that message code, and the editor shows its text. Administrators pass the check. - `SiteLayoutService#deleteUserDraftSites` (`@ContainerTransactional`) removes every `DRAFT` site whose name ends with `_draft_<username>`, read in one unpaged listing. A removal that fails is logged and skipped. Two listeners call it off the deleting thread: `DraftSiteOwnerAccountDeletedListener` on social's `social.account.deleted` (`@Asynchronous`), and `DraftSiteOwnerUserDeletedListener` on `UserEventListener#postDelete` (`CompletableFuture.runAsync`). - Tests: - `SiteLayoutServiceTest`: the refusal leaves the existing draft untouched; the removal by suffix; a failed removal; a blank username; the container binding. - `LayoutAclServiceTest`: a site not stored yet. - `SiteLayoutRestTest`: the 400. - Both listener tests. **Known limits** (accepted): - an administrator passes the check; - resuming a draft through the user's co-editing revision shows the generic error, not the conflict message; - an owner whose draft name collides with another existing user's is refused that draft; - page and navigation-node drafts (`<name>_draft_<user>`) share the name ambiguity and are saved over by name; - the draft sites stay for users deleted before this change, and for a user deleted through social's `DELETE /v1/social/users/{id}`, which fires no event; - the removals run in one persistence context; - a co-editing revision can still name a removed draft; - removing a deleted user's draft sites also removes a draft whose name another user could own: while the deleted user existed, that name was refused to every non-administrator, and an administrator's draft of that name is removed too. Stacked on #581. Merge after it and after Meeds-io/portal#1345. Knowledge: Meeds-io/eng-standards#572, #522 This change is classified N1 (a new permission check on the draft site's write path): its approver must be an Archi/Dev who knows it is N1, not an approval on AI review alone, and author ≠ approver. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> (cherry picked from commit f5a9180)
The merge-base changed after approval.
ea6c223 to
e38a075
Compare
|



Symptom: some draft site names also read as another user's draft: a site name holding
_draft_or ending with_draft, or a username starting withdraft_. On such a name, opening the site layout editor removed and re-created the draft by name, which could drop the other user's draft. Once portal refuses such a name (Meeds-io/portal#1345), the editor stopped on a generic error instead. A deleted user's draft sites stayed, and a user whose name fits a leftover name could open them.Cause:
SiteLayoutService#createDraftSitebuilds<type>_<name>_draft_<user>and removes any site of that name before creating it, without checking that the user may edit that name. Nothing removed a user's draft sites when the user was deleted.Fix:
createDraftSitechecksLayoutAclService#canEditSite(PortalConfig, String), a new overload, on the draft it built in memory, before removing or creating anything. When the check fails it refuses withIllegalArgumentException("layout.siteDraftNameConflict").SiteLayoutRestanswers 400 with that message code, and the editor shows its text. Administrators pass the check.SiteLayoutService#deleteUserDraftSites(@ContainerTransactional) removes everyDRAFTsite whose name ends with_draft_<username>, read in one unpaged listing. A removal that fails is logged and skipped. Two listeners call it off the deleting thread:DraftSiteOwnerAccountDeletedListeneron social'ssocial.account.deleted(@Asynchronous), andDraftSiteOwnerUserDeletedListeneronUserEventListener#postDelete(CompletableFuture.runAsync).SiteLayoutServiceTest: the refusal leaves the existing draft untouched; the removal by suffix; a failed removal; a blank username; the container binding.LayoutAclServiceTest: a site not stored yet.SiteLayoutRestTest: the 400.Known limits (accepted):
<name>_draft_<user>) share the name ambiguity and are saved over by name;DELETE /v1/social/users/{id}, which fires no event;Stacked on #581. Merge after it and after Meeds-io/portal#1345.
Knowledge: Meeds-io/eng-standards#572, #522
This change is classified N1 (a new permission check on the draft site's write path): its approver must be an Archi/Dev who knows it is N1, not an approval on AI review alone, and author ≠ approver.
🤖 Generated with Claude Code