You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 5f5be6a
Browse filesBrowse the repository at this point in the historyBrowse files
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)
**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>
0 commit comments