teqo-backup/docs/plans/c96-ical-feed-intersect-scope-read.md
Francisco Solla 29ed733190 fix(C92): calendar feed creation stripped by admin-only system fields
createCalendarFeedLinkRecord failed with `overrideAccess:false` +
`user: campaignUser` because Payload stripped the required
`secretSlug`/`createdBy` fields (create access was admin-only) -> 400
behind the generic message; revokeCalendarFeedRecord silently no-opped
(revokedAt update access also admin-only).

- canCreateCalendarFeed now enforces advisor filterMunicipality scope
  on data (canCreateSupporter precedent) + canSetCalendarFeedSecret /
  canSetCalendarFeedRevocation permit staff to mint the secret on create
  and mark a feed revoked; secret read stays `() => false` and update
  admin-only.
- CalendarFeed stamps createdBy via stampCampaignCreatedBy; secret
  create + revokedAt update use the new staff field accesses.
- Action refactored to `*Record` (payload, actor) cores + thin wrappers;
  assertFilterMunicipalityInScope (tour precedent); console.error keeps
  the real error on the server while the friendly message stays.
- 10 int tests for create/scope/revoke/leader/secret; fixture tracks
  calendarFeed cleanup.

Closes #436
2026-08-08 23:13:30 -03:00

36 lines
2.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# C96 — Feed iCal: interseptar escopo do criador no read (defesa em profundidade)
Status: rascunho
Atualizado em: 2026-08-08
Issue: #455
Priority: P3
Model: composer-2.5
Model-local: deepseek-v4-flash-high
Impeccable: A — N/A sem UI
Appetite: ~0,25–0,5 dia eng
Responsável: —
## Intenção
Débito do `/simplify` do C92 (#436), aprovado no gate humano. O write-side do feed (`canCreateCalendarFeed` + `assertFilterMunicipalityInScope`) já impede um assessor de criar feed pinado a município fora do escopo. Mas o read-side (`buildFeedWhere`, `src/utilities/calendarFeed.ts:104-112`) **confia no `feed.filterMunicipality` sem interseptar os ids acessíveis do criador** quando `accessibleMunicipalityIds` é não-nulo. Consequência: se um assessor é **removido** de um município depois de criar o feed, o feed continua servindo atividades daquele município até o feed ser revogado.
## Aceite
- No GET `/campanha/agenda/ical/[secret]`, quando o criador do feed é advisor e `feed.filterMunicipality` está setado, a consulta intersepta o município do filtro com os municípios atualmente administrados pelo criador — município fora do escopo atual → feed não inclui (404/feed vazio, alinhado ao fail-closed do C16).
- Coordinator/candidate (unrestricted) e admin: comportamento inalterado.
- Sem mudança no write; sem migration (config/consulta only).
## Direção no codebase
- `src/utilities/calendarFeed.ts` → `buildFeedWhere`: quando `feed.filterMunicipality` setado E `accessibleMunicipalityIds` não-nulo (advisor), exigir `{ id: { equals: filterMunicipality } } AND { id: { in: accessible } }` (ou validar antes e 404). `resolveFeedCreatorAccess` já calcula `accessibleMunicipalityIds` no read.
- Precedente: `advisorMunicipalityScopeWhere` (`src/utilities/access/shared.ts`).
- Teste int: `tests/int/calendarFeed.int.spec.ts` — feed criado por advisor em município X, depois advisor removido de X (`assignMunicipalityAdvisors(mun, [])`), GET do feed não lista atividades de X.
## Fora de escopo
- Remodelar o modelo de acesso do feed (maior refactor).
- UI (C94) / sem-filtros (C93), que já são Issues separadas.
## Questões em aberto
- Nenhuma — fail-closed orientado pelo C16 e pelo C92.