Skip to content

Commit a9759fe

Browse files
committed
fix(security): ScopesController — add @NoAdminRequired + drop _multitenancy:false
`/api/scopes` is documented as a discovery endpoint any authed user should be able to call to drive frontend feature gates. But the docblock only carries `@NoCSRFRequired`, so the framework was demanding admin membership before the body ran — defeating the point of self-discovery. Two compounding issues: 1. Add `@NoAdminRequired` so non-admin callers reach the body. The body's anonymous + per-caller permission computation already handles authorization correctly from there. 2. Drop `_multitenancy: false` from the four register/schema lookups. With it set, discovery returned every register/schema slug across every tenant — a complete cross-tenant data-shape map for any tenant user. Only RBAC stays bypassed because the body reduces the actions per-caller downstream. Refs: #1419 review (concern 1) — discussion_r3187494453
1 parent ab3f7f2 commit a9759fe

1 file changed

Lines changed: 13 additions & 8 deletions

File tree

lib/Controller/ScopesController.php

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ public function __construct(
110110
*
111111
* @return JSONResponse The effective-scope envelope.
112112
*
113+
* @NoAdminRequired
113114
* @NoCSRFRequired
114115
*/
115116
public function index(?string $register=null, ?string $schema=null): JSONResponse
@@ -173,10 +174,12 @@ private function resolveRegisters(?string $filter): array
173174
{
174175
if ($filter !== null && $filter !== '') {
175176
try {
177+
// SECURITY: keep multitenancy filter on so the discovery
178+
// endpoint cannot enumerate registers across tenants. Body
179+
// already short-circuits on anonymous + admin paths.
176180
$register = $this->registerMapper->find(
177181
$filter,
178-
_rbac: false,
179-
_multitenancy: false
182+
_rbac: false
180183
);
181184
if ($register === null) {
182185
return [];
@@ -189,9 +192,11 @@ private function resolveRegisters(?string $filter): array
189192
}
190193

191194
try {
195+
// SECURITY: tenant-scope discovery via the default multitenancy
196+
// filter; only RBAC is bypassed because the body computes the
197+
// per-caller permission verdict downstream.
192198
return $this->registerMapper->findAll(
193-
_rbac: false,
194-
_multitenancy: false
199+
_rbac: false
195200
);
196201
} catch (\Throwable $e) {
197202
return [];
@@ -210,10 +215,10 @@ private function resolveSchemas(?string $filter): array
210215
{
211216
if ($filter !== null && $filter !== '') {
212217
try {
218+
// SECURITY: see resolveRegisters() — keep multitenancy on.
213219
$schema = $this->schemaMapper->find(
214220
$filter,
215-
_rbac: false,
216-
_multitenancy: false
221+
_rbac: false
217222
);
218223
if ($schema === null) {
219224
return [];
@@ -226,9 +231,9 @@ private function resolveSchemas(?string $filter): array
226231
}
227232

228233
try {
234+
// SECURITY: see resolveRegisters() — keep multitenancy on.
229235
return $this->schemaMapper->findAll(
230-
_rbac: false,
231-
_multitenancy: false
236+
_rbac: false
232237
);
233238
} catch (\Throwable $e) {
234239
return [];

0 commit comments

Comments
 (0)