Permission model for a multi-site editorial workflow
A publisher runs one Drupal 10 codebase serving three sites via a multisite configuration. Editors on site A must not see content from site B, but a shared "newsroom" role must be able to publish to all three. Content types differ per site. The team currently uses a single editor role with broad permissions and a newsroom role, and has just discovered that an editor on site A can view unpublished nodes on site B by guessing node IDs.
1. What is the root cause of the cross-site visibility and what module-level approach fixes it without splitting the codebase?
Read the worked answer
The root cause is that Drupal's node access is per-database, and in a shared-database multisite (or a single site with a site field) the editor role's view own unpublished content / access content permissions apply globally. If the sites share a database, node IDs are global and an editor's access content grants access to any published node; unpublished access depends on view own unpublished content or bypass node access. The fix is to add a site-association field to content and enforce it with a node access module such as node_access-based custom grants, or use the Domain Access module (or Workbench Access with a site taxonomy) to grant per-site view/edit. The newsroom role gets a grant across all site domains; the editor role gets a grant only for its own site's domain. This keeps one codebase and one set of content types, with permissions enforced at query time by node_access. Note that node_access grants require a rebuild and that views must not use accessCheck(FALSE) for these listings.
2. How do you verify the fix actually blocks guessing node IDs, and what pitfall should you warn about?
Read the worked answer
Verification: create an editor user assigned to site A, then request a site B unpublished node by direct URL and by node/% load in a test; both must return 403. Also test a views listing and a JSON:API request, since JSON:API respects node access but only if the resource is not filtered with accessCheck(FALSE). Pitfall: the node_access grant system writes to the node_access table and the module must implement hook_node_access_records and hook_node_grants; if the site field is added but existing nodes have no value, the default grant may fall back to all, silently re-opening access. Backfill the field before enabling grants, and rebuild permissions (drush php:eval calling node_access_rebuild()).