pluto
PLUTO-57
UX: pre-filter practica list-row links by viewer scope so out-of-scope rows aren't clickable (avoids working-as-designed scope-deny on /practicas/[id])
Done low
cpcoder-pluto-cc
Activity
-
From PLUTO-55 Part B (coder root-cause). access-denied (79 ev/9 users) is NOT a bug: app-sidebar gates every item on can(cap); 100% of denials are /practicas/[id] scope-denials (docente opening a practica outside comision scope) — guard resolveDataAccessScope/canAccessComision/denyAccess works as designed (defense-in-depth). Entirely STALE: Mar+Apr only, ZERO since May 1 (April persona/impersonation testing). Optional nicety, low priority: pre-filter the practica list-row links by viewer scope so out-of-scope rows aren't clickable in the first place. Not urgent.
-
coder-pluto-cc
-
Closing as not-reproducible: the defect the WI proposes a fix for cannot occur on the current code, and the residual denials are not the class it targets. No code change. STRUCTURAL ARGUMENT (the detail gate is a SUPERSET of the list, on the same primitive). The main list at /practicas is already scope-filtered - practicas/page.tsx calls resolveTeacherLikeScope(user, periodo.id) with NO options, so includeComisionDay defaults to false and getPracticas receives jefe scopes only. The detail gate at practicas/[id]:139 calls activeTeacherScopes(accessScope, user.id, true, true) - includeComisionDay=TRUE, i.e. jefe + ayudante - and grants on matchesJtp OR canAccessComision. Same function, strictly wider argument, plus an OR arm. A row rendered in the list is therefore always openable. That alignment is PLUTO-77's doing (it replaced the ungated effectiveJtpIds.includes check); the WI was filed 2026-06-12, before 77 landed. Building a second scope filter in the link layer now would duplicate the gate in a place that can drift from it - the failure mode is worse than the one being fixed. MEASURED (db-pluto-cc, appEvents action=accessDenied, route ILIKE '/practicas/%'; it is the only action name that hits this route). All-time 46. Monthly: Mar=7, Apr=32, May=0, Jun=1, Jul=5, Aug=1. Since 2026-05-01: 7 rows, and 6 of the 7 are Elazar's own persona accounts - d0000000-0000-0000-0002-000000000004 = 'Elazar (Alumno)' (4 rows incl. the most recent, 08-10) and ...0003 = 'Elazar (JTP)' (1 row), both seeded by migration 032-demo-reseed.sql at @pluto-persona.local, which is not a deliverable domain and cannot be an organic login. Rows 2 and 3 are 1.5ms apart on one route - one request, not two events. THE ONE ROW THAT IS ORGANIC IS A DIFFERENT CLASS. 2026-06-24, actor 727ceb10-..., reason 'Student is not a participant in this practica' - that is the !isTeacher && !isParticipant branch at [id]/page.tsx:90, reached by a student following a direct link (notification, comentario, email), NOT by clicking a list row: students never see the /practicas list at all, page.tsx:79 redirects them to /mi-cursada. Pre-filtering list rows would not have prevented it, and the denial is correct behaviour. Exactly ONE row since May 1 is in PLUTO-57's actual class ('Scoped: practica outside per-comision slice', 2026-07-17) and it is the JTP PERSONA - persona testing, post-PLUTO-77, zero organic instances. WI PREMISE CORRECTION: the founding note cites '79 ev/9 users'. This predicate yields 46 all-time and 39 for Mar+Apr. The 79 does not reconcile against this table/action/route and appears to have come from a wider predicate. Recording it rather than reusing it - the number was load-bearing for 'this is worth fixing'. NOT EXERCISED: no observation of link surfaces outside the main list (anomaly-card, student-practicas-audit-log, comentarios/linkified-text, support-thread). Those render /practicas/[id] hrefs without their own scope filter and are the surface the one organic row came through - but they deny correctly at the gate, which is the intended defense-in-depth, so there is nothing to fix there either unless a UX complaint arrives.
task
2026-06-12 by wi-cli-venus
7w ago
2026-08-22 13:52