Conversation
…nto session Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… impersonation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ole to MPD leaders Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…impersonation Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bundle sizes [mpdx-react]Compared against d14c76a
|
wjames111
left a comment
There was a problem hiding this comment.
Multi-Agent Code Review — BLOCKERS FOUND
Mode: standard · Agents: 7 specialized reviewers + dependency-impact + 2 gap reviewers, with cross-examination debate · Risk: CRITICAL (10/10 — NextAuth session + impersonation authorization, 61 files)
Verdict: 1 high-priority blocker
The core of the PR is solid — the HMAC-signed scope trust chain is sound, all 15 contacts/tasks/tools sub-pages are correctly blocked, read-only guards are correctly placed with correct dependency arrays, i18n is complete, the dynamic-modal pattern is followed exactly, and the Standards checklist passes across the board. One gap must be fixed before merge (below), plus one strongly-recommended test gap and a handful of medium/suggestion items posted as inline comments.
Findings on Related Files (Not in This PR)
[High] pages/accountLists/[accountListId]/tools.page.tsx:46 — Tools landing page is not blocked during restricted impersonation
- Severity: 8.0/10 — BLOCKER (flagged by Dependency Impact, escalated by Security, supported by all 6 debating agents; verified twice at PR head)
- All 15 contacts/tasks/tools sub-pages got
blockRestrictedImpersonation, but the/toolslanding page still exportsgetServerSideProps = ensureSessionAndAccountList. The nav tab is hidden, but a bookmarked/typed URL renders the full Tools home during a restricted impersonation session.ToolsHomeunconditionally firesGetToolNotifications, which queries the impersonated staffer'scontacts/peopleaggregate counts (partner-status/address/email/phone filters) — exactly the data class the block exists to withhold. This contradicts the commit "Block contacts, tasks, and tools routes during restricted impersonation." - Fix (one line): swap
ensureSessionAndAccountList→blockRestrictedImpersonationintools.page.tsx, matching its 15 siblings. Consider also asserting the guard intools.page.test.tsxthe wayhrTools/mpdGoalAdmin/index.page.test.tsxasserts itsgetServerSideProps.
(This file is not in the PR diff, so it cannot carry a line comment — but it is counted as a blocker because it is a missing change required by the PR's stated goal: 16 routes in the access matrix, 15 guarded, 1 missed.)
Debate resolutions worth knowing
- Canonical scope value: the wire value is
impersonation_scope: 'mpd_leader'.mpdSupervisorAdminis the admin capability flag — a different concept. Test fixtures currently use'mpd_supervisor'and'mpd_leader'interchangeably for the scope (harmless today since only truthiness is checked, but worth a shared fixture before any allow-list lands). - TransfersTable unguarded handlers: debate verified MUI's disabled
IconButtongenuinely blocks the child-SVGonClickvia mouse AND keyboard — so this is a defense-in-depth/consistency gap, not a live bypass (down-revised 6.5 → 6.0 merged finding). - Server-side enforcement: confirmed posture — the API rejects restricted mutations; all client guards are defense-in-depth.
Pre-existing issues (informational, separate PR)
- 6.0
hrTools/mpdGoalAdmin/scenario/[scenarioGoalId]/index.page.tsxuses bareensureSessionAndAccountList— no admin gate or restriction block, now more meaningful since mpdGoalAdmin hosts the impersonate action. - 3.5
impersonateHelper.ts:118sets handoff cookies even when the token fetch fails (the PR's new scope cookie is correctly guarded). - Shared
Modallacks explicitaria-labelledby.
Verified clean
Scope trust chain (HMAC-signed cookie, tamper-rejection tested, JWT_SECRET server-only, no token logging) · mpdSupervisorAdmin exists in the schema, yarn gql will pass, id in selection set · guard placement precedes all optimistic state, dependency arrays correct · no financial calculation code touched · i18n byte-identical across all 7 read-only tooltip sites · no debug output / any / new Date() / empty catches.
Add test asserting handleDeleteRequest does not fire DeleteAdditionalSalaryRequest during restricted impersonation Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add test asserting form submit fires no mutations during restricted impersonation Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consolidate impersonation-scope checks into a shared isRestrictedImpersonation predicate with a documented ImpersonationScope union and access matrix Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use the useRestrictedImpersonation hook instead of deriving the flag inline from the session Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Guard handleAcceptCalendar and handleDeleteModalOpen with restrictedImpersonation early returns and add a test asserting row action buttons are disabled under impersonationScope Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Default mpdSupervisorAdmin to false in session callback so stale JWTs minted before deploy fail closed deterministically Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add restricted impersonation tests covering disabled Add Entry, disabled delete action, and blocked cell edits firing no mutations Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add GoalCard tests asserting the Delete button is disabled when deleteDisabled is true and enabled by default Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hide the Impersonate action when the session is already impersonating to prevent nested-impersonation UI Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop 'developer' from Admin Console grantedAccess since enforceAdminOrMpdLeader (like the old enforceAdmin) bounces developer-only accounts, leaving a dead nav link Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add unit test covering Submit-branch disableNext: Submit button disabled and disabledNextTooltip shown when isSubmission and disableNext are set Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swap ensureSessionAndAccountList for blockRestrictedImpersonation on the /tools landing page and assert the guard in its test Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mockSession persists across tests (clearMocks does not reset return values), so the leaked impersonation scope was failing the subsequent onSubmit tests; sibling suites place the restricted test last for the same reason Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
🤖 Fixed blocker in b499a65: |
wjames111
left a comment
There was a problem hiding this comment.
Multi-Agent Code Review — Round 2 (re-review after fixes) — APPROVED WITH SUGGESTIONS
Mode: standard · Agents: 7 specialized reviewers + dependency impact · Risk: CRITICAL (10/10 — auth/impersonation surface; human review always required at this risk level)
Verdict: 0 blockers — all 12 round-1 findings VERIFIED-FIXED
Every round-1 finding, including the 8.0 blocker, was independently re-verified by multiple agents:
- Blocker (tools.page.tsx): now
blockRestrictedImpersonation, identical to its 15 siblings, with an identity-assertion test. The 59-page route audit confirms contacts/tasks/tools coverage is complete. - Shared predicate (
src/lib/restrictedImpersonation.ts): fully adopted — whole-tree grep finds zero remaining inline!!impersonationScopechecks; both SSR helpers and the client hook route through it; behavior byte-identical; server/client import boundary safe (type-only next-auth import). Net tech debt reduced. - All 8 test-gap fixes: verified non-vacuous (each asserts the negative against a positive baseline); suites pass.
- Merge of origin/main: no auth-relevant, financial, or structural changes entered the PR diff; codegen and lint:ts now pass cleanly.
Remaining suggestions (non-blocking, inline comments)
- (5.0) Harden the
mockSessionleakage mitigation inuseAdditionalSalaryRequestForm.test.tsx— reset the session inbeforeEachinstead of relying on test order. - (3.5)
ImpersonationScopeunion is exported but unreferenced — forward-looking by design; optionally give it a live consumer. - (3.0) GoalsTable test could also assert View/Edit still renders while impersonating.
Informational / pre-existing (no action in this PR)
- (4.0) [Pre-existing] Two profile menus (
ProfileMenu.tsx:364,ProfileMenuPanel.tsx:166) still show the Admin Console link todeveloper-only users, who get bounced byenforceAdminOrMpdLeader— the same dead-link UX fixed inuseSettingsNavItems. Suggest a follow-up ticket to align all three surfaces. - (3.0) [Pre-existing, from main merge]
coaching/[coachingId]/nsGoalCalculator/index.page.tsxuses bareensureSessionAndAccountListwith no impersonation gate — authored on main (commit 9cda1b7), not by this PR. Confirm coaching-scoped calculators are intentionally outside the restricted-impersonation surface. - (3.0) The
?? falsedefault in the session callback is unreachable per the JWT type but correct as runtime defense for pre-deploy tokens — intentional, keep. - Known items carried forward: scenario page guard, impersonateHelper failure-path cookies, fixture value
'mpd_supervisor'vs wire value'mpd_leader'(truthiness-only today).
Standards
Full checklist PASS. Lint, type-check, codegen, and all touched test suites pass (verified by orchestrator).
…x:771 Reset session to the default fixture in the file-level beforeEach so mockSession overrides cannot leak between tests Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Export MPD_LEADER_SCOPE typed as ImpersonationScope and consume it in test fixtures so the union has a compile-time consumer pinning the wire value Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Assert the disabled View/Edit link still renders per row while impersonating Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Preview branch generated at https://MPDX-9771.d3dytjb8adxkk5.amplifyapp.com |
canac
left a comment
There was a problem hiding this comment.
It's a lot of code, so I focused on the hotspots and had Claude review the rest. I'm concerned about needing to check the impersonation scope in so many parts of the application and in lots of new code. That seems like a maintenance nightmare, so I'd like to think through alternatives before committing to that route.
The server blocks mutations, right? So what do you think allowing mutations, know that the server will reject them? We can just make sure that the server returns an understandable error message like, "Changes are not allowed during impersonation".
| // remaining HR tools must render read-only and never fire mutations. | ||
| // Shares its definition of "restricted" with the server-side page guards via | ||
| // isRestrictedImpersonation — see the access matrix documented there. | ||
| export const useRestrictedImpersonation = (): boolean => { |
There was a problem hiding this comment.
It's going to be annoying and error-prone to have to check this before every mutation in our application.
There was a problem hiding this comment.
I think your suggestion of just relying on the back-end to block it works better.
There was a problem hiding this comment.
🤖 Fixed in 5a88882: removed the per-mutation frontend guards and disabled states from the HR tools — the API's mutation allowlist is now the sole enforcement layer. Nav/route hiding for contacts/tasks/tools remains.
Posted by Claude Code.
| admin, | ||
| developer, | ||
| // Default to false for JWTs minted before mpdSupervisorAdmin existed | ||
| mpdSupervisorAdmin: mpdSupervisorAdmin ?? false, |
There was a problem hiding this comment.
If the frontend needs to check for one more role (4+), I'd consider making this an enum.
There was a problem hiding this comment.
Agreed — the plan is to finish the role column work in CruGlobal/mpdx_api#3285, migrate admins/developers/MPD supervisors to it, and have the frontend read a single role value instead of these booleans. Leaving the booleans as-is here so the swap happens once, in the role-migration PR.
Posted by Claude Code.
| id: 'admin', | ||
| title: t('Admin Console'), | ||
| grantedAccess: ['admin', 'developer'], | ||
| grantedAccess: ['admin', 'mpdSupervisorAdmin'], |
There was a problem hiding this comment.
Is this right? Why are we removing developer?
There was a problem hiding this comment.
Thanks for catching this!
There was a problem hiding this comment.
🤖 Fixed in 9ac10be: restored 'developer' in the Admin Console grantedAccess and admitted developers in enforceAdminOrMpdLeader so the nav item and page guard agree.
Posted by Claude Code.
|
@canac allowing mutations works for simplifying things a little but. but we still have the PII leaking everywhere across the app, so either way it's kind of a nightmare. Could we just give them access to SAA? Architecturally this is a really bad request |
Doesn't your backend PR redact PII? Or are you saying there are more fields that could include PII? |
Per review: drop the per-mutation restricted-impersonation guards and disabled states from the HR tools (autosave early-returns, handler guards, disabled create/delete/submit/transfer actions). The API's mutation allowlist is the enforcement layer; nav/route hiding for contacts/tasks/tools remains. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per review: keep 'developer' in the Admin Console grantedAccess and admit developers in enforceAdminOrMpdLeader so the nav item and page guard agree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eligibility to impersonate no longer comes from the mpdSupervisorAdmin flag: any user who coaches at least one account list is an MPD leader. The sign-in GetUserAccess query now derives a coach session field from coachingAccountLists.totalCount, and all nav/page/impersonation gating uses it. enforceAdminOrMpdLeader is renamed to enforceAdminOrCoach. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Summary
Frontend half of MPDX-9771: lets MPD leaders — any user who coaches an account list — impersonate the staff they coach under a restricted, server-enforced scope, so they can help with MPD forms without seeing contacts, tasks, or contact PII.
Companion API PR (deploy prerequisite): https://github.com/CruGlobal/mpdx_api/pull/3455 — the API mints the scoped JWT (
impersonation_scope: 'mpd_leader') and enforces every restriction server-side. This PR wires the scope through the frontend session and adapts the UI.Permission model (4 tiers)
blockRestrictedImpersonation, and the API independently blocks the data.Changes
GetUserAccessnow selectscoachingAccountLists(first: 1) { totalCount }and the NextAuth JWT/session derive acoachboolean from it (plusimpersonationScope); eligibility is coaching-based, not an admin-granted flag;impersonateHelperstores the API'simpersonation_scopein a signedmpdx-handoff.impersonationScopecookie whichsetUserInfoverifies and folds into the JWT.pagePropsHelpers): newenforceAdminOrCoach(admin console access for coaches) andblockRestrictedImpersonation(contacts/tasks/tools pages redirect during scoped impersonation); applied to 15 pages +admin.page.tsx.useNavPages); Admin settings nav item visible to coaches; Reset Account accordion stays admin-only.ImpersonateStaffModalon the MPD Goal Admin table (lazy-loaded via the Dynamic modal pattern, Formik/Yup reason field).useRestrictedImpersonationhook +isRestrictedImpersonationserver-side predicate (used for nav/route hiding). Per review, the earlier per-mutation frontend guards and disabled states were removed — the API is the enforcement layer for read-only tools.Testing
yarn lint,yarn lint:tsclean; fullyarn testgreen (16 suites that failed under parallel load all pass in isolation — known flakiness, verified with--onlyFailures).Deploy notes / follow-up
impersonation_scope.🤖 Generated with Claude Code