Repository navigation
fix(api): deny out-of-scope single-record access to outbreak people - #24
Draft
lbrunofidelis wants to merge 2 commits into
Draft
lbrunofidelis wants to merge 2 commits into
lbrunofidelis wants to merge 2 commits into
Conversation
Reading, updating or deleting a case, contact, contact of contact or event by id through the outbreak relation endpoints now checks the same geographic restriction query used to build the person lists, and refuses the request with ACCESS_DENIED (403) when the record falls outside it. Hiding the link in the UI was not enough since the record was still reachable by id.
Relationship.maskPersonName now sets masked: true next to the nulled name fields, so the front end can tell a withheld person apart from one that simply has no name, instead of inferring it from empty strings.
This was referenced Sep 24, 2026
lbrunofidelis
marked this pull request as draft
September 24, 2026 18:49
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Por que
Na aba de relacionamento (Exposições e Contatos) e no diálogo aberto por "Número de contatos" da lista de casos, o usuário com restrição geográfica vê a pessoa de fora da área com o nome escondido, mas o ID continuava sendo um link. Clicando, ele abria o caso completo, com todos os dados, e conseguia até editar. Esconder o link no front não basta, porque o registro continuava acessível pelo id.
Causa
Os endpoints de registro único das relações do surto (
GET,PUTeDELETEem/outbreaks/{id}/cases/{fk},contacts,contactsOfContactseevents) não aplicavam a restrição geográfica que as listas aplicam. E a API não dizia ao front quando uma pessoa tinha sido mascarada: ela só anulava os campos de nome.O que muda
common/controllers/outbreak.js:beforeRemoteemprototype.__findById__,__updateById__e__destroyById__decases,contacts,contactsOfContactseevents. O where é montado comPerson.addGeographicalRestrictions, o mesmo usado pelas listas, então a regra de escopo (residência ou notificação) é uma só. Registro fora do escopo recebeACCESS_DENIEDcom status 403, no mesmo formato deserver/middleware/authenticationError.js. Quando não há restrição aplicável (admin, ou surto sem restrição geográfica), nada muda.common/models/relationship.js:Relationship.maskPersonNamepassa a marcarmasked: truejunto dos campos de nome anulados, para o front distinguir uma pessoa mascarada de uma pessoa sem nome.server/scripts/checkRelationshipNameMasking.jspassa a conferir essa marcação.Como validar
GET /api/outbreaks/{outbreakId}/cases/{id}de um caso fora da área devolve 403; de um caso da área, 200.PUTeDELETEfora da área devolvem 403.GET /api/outbreaks/{outbreakId}/contacts/{id}/relationships/exposurescomincludedepeople, como usuário restrito: a pessoa de fora da área vem com os nomes nulos emasked: true; as da área vêm sem a marcação.Validado
teste2): 0 divergências. Fora da área 403, dentro da área 200,PUTeDELETEfora da área 403 para o restrito, tudo 200 para o admin.server/scripts/checkGeoVisibilityQueryShapes.js: 8/8 PASS.server/scripts/checkRelationshipNameMasking.js: 10/10.masked: truesó nas pessoas fora do escopo.relationship.jsjá usa CRLF em todo o arquivo e a linha nova segue o mesmo padrão).Fora do escopo
Outbreak.prototype.exportExistingEmptyCaseInvestigationchamathis.__findById__casesdireto no código, sem passar pelo remoting, então não passa pela verificação nova. É uma exportação pouco usada e fica para outra entrega.GET /outbreaks/{id}/relationshipsno nível do surto continua sem restrição geográfica, comportamento original do Go.Data.maskedemRelationship.maskPersonName. A mudança é a mesma nas duas branches; a que entrar por último precisa de um rebase simples.Why (English)
In the relationship tabs (Exposures and Contacts) and in the dialog opened from the cases list "Number of contacts", a user restricted to a geographic area sees an out-of-scope person with the name withheld, but the ID was still a link that opened the full record and even allowed editing it. Hiding the link alone is not enough because the record stayed reachable by id.
Root cause
The outbreak single-record relation endpoints (
GET,PUTandDELETEfor cases, contacts, contacts of contacts and events) did not apply the geographic restriction the lists apply, and the API gave the front end no signal that a person had been masked.What changes
common/controllers/outbreak.js:beforeRemotehooks on__findById__,__updateById__and__destroyById__for the four person relations, building the where clause withPerson.addGeographicalRestrictions(the same helper the lists use) and answeringACCESS_DENIED(403) when the record is out of scope. No restriction applies to admins or to outbreaks without geographic restrictions.common/models/relationship.js:Relationship.maskPersonNamesetsmasked: truenext to the nulled name fields; the masking check script asserts it.How to validate
As a restricted user, read, update and delete a case outside the area (403) and inside the area (200); as the admin everything stays 200; relationship lists flag only out-of-scope people with
masked: true.Validated
Single-record probe with 0 mismatches for both users,
checkGeoVisibilityQueryShapes.js8/8,checkRelationshipNameMasking.js10/10, eslint clean on the change, plus a manual create-then-read check described below.Out of scope
exportExistingEmptyCaseInvestigationcalls__findById__casesdirectly and bypasses the new hook.GET /outbreaks/{id}/relationshipsremains unrestricted, as in upstream Go.Data.maskedflag inRelationship.maskPersonName; whichever lands last needs a trivial rebase.