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

2.2 KiB
Raw Permalink Blame History

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.