Repository navigation
fix(geo): reject foreign relay IDs in mutations, and don't cache private tiles - #472
Merged
Merged
Conversation
…ate tiles `get_obj_from_relay_id` resolves a public ID through DocumentId and returns the object of ANY registered model. The geo mutations used it without a type check, so a user holding only the geo model permissions could reach other blocks' rows: - geoFeatureDelete called obj.delete() on whatever came back. A User's (or Comment's, Follow's, Rate's) relay ID deleted that row. - geoFeatureUpdate passed it to GeoJSONFeatureForm(instance=obj). With all four optional inputs supplied, construct_instance wrote `name` and `description` onto the foreign instance and form.save() persisted it. Both now resolve through _get_feature(), which type-checks and raises `not_found`. The resolver's own exceptions are caught and logged rather than surfaced, since they leak which IDs exist. geoFeatureCreate rejects an unresolvable targetObjectId the same way, instead of failing later with an IntegrityError on the NOT NULL target_document_id. Also: - Tiles sent `Cache-Control: public` unconditionally, even though the view's permission is overridable precisely so projects can hide features from anonymous users. Visibility is now derived from whether an anonymous user may view, and Vary: Authorization is set so token-auth deployments aren't shared-cached on the session cookie alone. - baseapp/geo/graphql/__init__.py re-exported the ObjectTypes, so importing the FilterSet from the REST tile view pulled in graphene, graphene_django, query_optimizer and baseapp_auth.graphql — making the REST surface depend on the `graphql` extra and defeating the laziness filters.py documents. Emptied, matching baseapp/files and baseapp/content_feed. Not addressed here: object-level authorization on update/delete (no owner column exists on AbstractGeoJSONFeature yet) and the generic `GeometryObjectType` type name. baseapp/geo/tests: 114 passed. black, isort and flake8 clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fczpSCpN3kCRs3UBjbity
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
nossila
added a commit
that referenced
this pull request
Oct 9, 2026
Still-valid findings from the Copilot and CodeRabbit reviews. Four others were already fixed by #472 / #473 and are answered on the threads rather than here. - Permission codenames were hardcoded to `geojsonfeature`. The model is swappable, so a project swapping in `MapFeature` gets `add_mapfeature` and none of the checks would ever match. All of them now derive the codename from `_meta.model_name`, across permissions, mutations, object types and the REST view/permission. - `geoFeatures` never enforced the read permission. DjangoConnectionField resolves edges straight from the queryset and never calls `get_node`, so a project that overrides the public-read backend still had every feature listable. Added `get_queryset`, returning `skip_ast_walker(queryset.none())` so the optimizer isn't run over an empty queryset. - The legacy base64 branch of the target filter validated the type name but never checked the row exists, so a well-formed ID for a deleted object returned an empty list instead of the documented validation error. It now mirrors the public-id branch. - README: `baseapp_geo` -> `baseapp.geo` in the INSTALLED_APPS instruction (the label is not importable), the auth-backend slot is `baseapp_geo` not `baseapp.geo`, and the factory lives at `baseapp.geo.tests.factories`. baseapp/geo/tests: 124 passed. black, isort and flake8 clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015fczpSCpN3kCRs3UBjbity
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.
Group 1 of the geo code-review findings — the ones that need no schema change or product decision.
The core problem
get_obj_from_relay_id→PublicIdResolverStrategy.resolve_id→DocumentId.get_object_by_public_id(id)with nomodel_class, which returns thecontent_objectof any registered model. The geo mutations used the result with no type check, and the permission check above it had already run withobj=None, so nothing narrowed it.Reachable by any user holding the geo model permissions (
GeoPermissionsBackendreturnsFalsefor change/delete, so those fall through toModelBackend's model-level grant):geoFeatureDeletecalledobj.delete()on whatever came back. Pass aUser's relay ID —AbstractUsercarriesDocumentIdMixin+RelayModel, so it has one — and that row is deleted.geoFeatureUpdatepassed it toGeoJSONFeatureForm(instance=obj). With all four optional inputs supplied thegetattrfallback never runs,construct_instancewrites every form field the instance shares (name,description), andform.save()persists it.baseapp/files/graphql/mutations.pyalready does exactly the missing check (isinstance(file_obj, File)→not_found), so this is a deviation from the reference package, not a new convention.Changes
_get_feature()type-checks the resolved object and raisesnot_found; used by update and delete. Resolver exceptions are caught and logged rather than surfaced — they leak which IDs exist.geoFeatureCreaterejects an unresolvabletargetObjectIdwithnot_foundinstead of failing later with anIntegrityErroron the NOT NULLtarget_document_id(a 500 to the client).Cache-Controlfrom whether an anonymous user may view, rather than hardcodingpublic. The view's permission is overridable precisely so projects can hide features from anonymous users; under such a backend a CDN was free to serve one user's tile to the next requester.Vary: Authorizationis set too, since token auth never touches the session cookie.baseapp/geo/graphql/__init__.pyemptied. It re-exported the ObjectTypes, so importing the FilterSet fromrest_framework/views.pypulled in graphene, graphene_django, query_optimizer andbaseapp_auth.graphql— making the REST tile endpoint depend on thegraphqlextra and defeating the lazinessfilters.pydocuments.baseapp/filesandbaseapp/content_feedboth ship a 0-line one.Testing
New tests assert the victim survives, not just that an error is returned:
not_found, and the target's field is unchanged afterrefresh_from_db()not_found, and the row still existsnot_found, no rows writtennot_foundprivate, max-age=300baseapp/geo/tests: 114 passed. black, isort, flake8 clean.Deliberately not in this PR
AbstractGeoJSONFeaturehas nocreated_by, so there is no owner to authorize against — that needs a model field, a migration, and a permissions-backend change modelled onFilesPermissionsBackend._is_owner_or_has_global. A product decision, not a patch.GeometryObjectType(finding 8). It collides with the namegraphql_geojson's converter produces, but renaming is a public schema change for anything already consuming it.🤖 Generated with Claude Code
https://claude.ai/code/session_015fczpSCpN3kCRs3UBjbity