Two Sets: What You May Read, What You May Manage
Opening read access across departments without loosening writes: readable and manageable module sets as two separate fail-closed sets.
TL;DR
KotaPortal treated the module query parameter as a write gate, hiding all content from other departments. The fix splits access into two sets: ModulesFor for writable modules and ReadableModules for readable ones, with fail-closed checks moved earlier in the chain. The frontend keeps full navigation while write affordances hide per permission, marked by a read-only banner.
A tourism-department staff member opens the KotaPortal admin panel and looks for the creative-economy content list. The screen answers with an empty list. The client requirement has been clear from the start: every department may read all content, but may write only inside its own department. The wrong assumption was equating read scope with write scope. For months the API treated the query parameter ?module= as a gate. A module outside the requester's department got an empty list back, fail-closed, and legitimate work stalled behind it.
Two Module Sets per Department
The fix separates access into two explicitly different sets. One department now carries two module sets. The first is the set it may manage, handled by ModulesFor, per-department and unchanged. The second is the set it may read, handled by ReadableModules, which returns every module for any valid department.
The function names carry the distinction into the code. The ?module= parameter stopped being a gate that rejects requests and became a display filter. This two-sets split is the foundation that separates identity validation from visibility validation.
Where Fail-Closed Went
Fail-closed did not disappear from the system. It moved earlier in the processing chain, to the layer where it belongs. An unknown department still resolves to nil, and the handler answers with an empty list before the database is touched at all. Following the OWASP deny-by-default guidance, when no access rule matches, the application must still make an explicit decision to deny or to permit [4].
A valid department combined with any ?module= value is a permitted read. The nil check sits before the ?module= branch, so a request from an unknown department cannot smuggle a module value past the first security check.
The Frontend Consequence
The server-side change produces a clean result on the client. The system now has a single source of truth: a mapping from department to module slugs plus one canManageModule function. The main navigation keeps showing every content menu in full. Write affordances hide or disable per module according to the user's rights.
A read-only banner marks views outside the writer's department, making it obvious why the edit buttons are missing. The read path opens wide without splitting the interface into two navigations that would confuse people and double the maintenance load.
Access control that knows only one word for "allowed" eventually blinds its users or overexposes its data. Separating what may be read from what may be managed makes both answers precise and auditable.
Sources