Repository navigation
OpenConceptLab/ocl_online#224 | Access-control fixes for content in repos the requester can't view - #943
Conversation
… cache only public sources Restores e1f3138 (reverted in 7bfb6c9). The lookup view accepts any source, not only the public OCL lookup repos, so it needs the view check; and its page cache is keyed on the URL, so only public sources are cached. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d source names `owner` and `source` in the concepts index are lowercase-normalized, but owner names that differ only in case belong to different users or orgs. The visible-repo filter and the target repo filter now use the exact-case owner_text.keyword and source_text.keyword fields, and a `target_repo` is searched by the stored names of the repos that were checked, so a `version` key or a lowercase `owner_type` now works too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ncepts the requester can view The same filter the includeParentConcepts/includeChildConcepts fields use (get_viewable_concepts), so a concept's parents or children in another repo show only when the requester can view that repo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s that include parent or child concepts includeParentConcepts and includeChildConcepts show only concepts the requester can view, so these responses differ per requester and can't come from, or go into, the shared list cache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uester can view A concept or mapping version's collection_versions (version detail, and versions lists with includeCollectionVersions) and the /collection-versions/ lists now show only public collection versions and those of the requester and their orgs (staff: all), using the collection version's own access. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mappings whose saver can view it Mappings to a concept or source that didn't exist yet keep the target as an external code and URL. When the concept or source is created, those mappings are linked only if the user who last saved them can view it, the same rule as when a mapping is saved. Also re-indexes the from-side mappings a new source links (the query filtered on to_source). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r can't view $cascade's omitIfExistsIn, and a collection reference's omit_if_exists_in, now apply only when the requester (for a reference, its creator) can view the repo; otherwise the expression is ignored, as if it didn't resolve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…when a new parent isn't viewable Updating a concept with parent_concept_urls read its previous version's parents (2 queries) on every update, even with no parents or with parents the user can view, which bulk imports pay per concept. They're now read only when some parent isn't viewable, to keep parents that are already linked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oncepts the requester can view A source-scoped query checks view access on the source version, and one the requester cannot view gets the same error as a source that does not exist. A global query (conceptIds or text) and the mappings listed for each concept use the same rule as the REST concept and mapping lists (apply_user_criteria; staff see everything), and the Elasticsearch search behind `query` applies get_visible_repo_criteria, so totalCount counts only viewable hits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… treat code systems the requester can't view as missing $lookup and $validate-code resolve `system`/`url` to the first code system version the requester can view (with its HEAD, which governs its concepts), skipping the rest as if they didn't exist. $lookup gives the same not-found response for a code in a code system the requester can't view as for a code system that doesn't exist, and $validate-code returns result false for signed-in requesters too, not only anonymous ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lue sets the requester can view $validate-code reads only the value sets the request names: the `url` value set and the route's collection. Query params no longer pick another owner or collection; a `version` param only narrows the route's collection. Each value set, its HEAD and every version read must be viewable by the requester; one that doesn't resolve or can't be viewed gives result false, the same as a code that isn't in it. The owner-less route reads the collections with that id the requester can view. Without `url`, the global route still checks the code system alone. A `display` with a code that isn't in the value set now gives result false instead of a server error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…vate events go to staff followers only A user's private events are about orgs and repos the follower may not be able to view, so the following scope now shows a followed user's public events only, unless the follower is staff. Private events of a followed org or repo still go to followers who can view it, and org members still get their orgs' private events in the orgs scope. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…llows of objects the requester can view /users/<user>/following/ and the includeFollowing field of user details and verbose user lists now leave out follows of orgs, repos and repo content the requester can't view. The user themselves and staff still get every follow. UserProfile.can_user_view(user, obj) answers can_view for any requester, anonymous included, and can_view delegates to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an't view are private Followed and Unfollowed events now get public=False when the followed org, repo or repo content isn't publicly viewable, like Created and Joined events already do. The user themselves, staff, and followers who can view the object still see them. Migration events/0011 makes existing Followed/Unfollowed events private when they reference a non-public org, or a non-public HEAD source or collection (or content under one). Reverse is a no-op. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fore filters apply only for staff Last login is staff-only account data, so the two filters are now ignored for non-staff and anonymous requests, the same way the staff-only isSuperuser/isStaff/isAdmin filters already are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… extras field The extras.<key> filter value goes into an ES query_string, where a `field:value` clause can address any indexed field. Escape backslash and colon in the value so it only ever matches extras.<key>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ly orgs the requester can view; pins only for self and staff includeSubscribedOrgs now lists the user's public orgs plus the orgs the requester belongs to (all for staff), using the same rule as the /users/<user>/orgs/ list, now Organization.get_visible_to. includePins is returned only to the user themselves and staff, like the other private user fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…se org and resource the requester can view GET /users/<user>/pins/, /orgs/<org>/pins/ and the pin detail return only pins whose pinned source, collection or org the requester can view, and, for org pins, whose org they can view (the same check as pinning). The owner can still change or remove every pin. includePins on the user detail applies the same rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s requester and store it under that requester ?csv=true no longer reuses an existing export file looked up by URL path alone. Every export is built from the view's own queryset, so the view's access checks and visibility filters apply, and is written to a downloads folder of the requester's own (user id, or "anonymous"), so one requester's file is never served to another. The ?csv&type= path uses the same rule. Removes the unused export-reuse helpers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ilter as reads An update returns the pinned resource, so updating a pin whose org or resource the requester can't view is refused like reading it (404). The pin's owner can still delete it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is judged by its own access A Followed/Unfollowed event of a repo version that isn't public is made private too, even when its HEAD is public; anything else under a repo is still judged by the HEAD. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…requester can't view gives false, with or without a code Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…versions their requester can view A $cascade bundle serializes its entries without the request, so it now passes its requester, and verbose mapping entries show members and staff the private collection versions they can view. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s objects they can no longer view Only staff get every follow; the user themselves sees a follow only while they can view the followed object, like everyone else. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uesters who can view them now An event's public flag is set when it's recorded, but the objects it shows are serialized live. An object the requester can't view now (for example a repo made private after the event) is shown as null. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hable A search on them took the list search path, which lists the repo's concepts, not the concept's parents or children, and skips the view's own checks. They now always list the concept's hierarchy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ope excludes other owners' private content A search of a repo the requester owns or is a member of includes private content, but the owner and source filters match names in any case, and owner names that differ only in case belong to other users or orgs. Their private content is now excluded: concepts by exact-case owner name; for mappings, which have no exact-case owner field, private content is left out when such an owner exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ons method, fewer one-caller helpers - CollectionVersionsMixin replaces four copies of get_collection_versions; the unused collection_versions property goes, and the user is required. - GraphQL's global queryset reuses get_viewable_concepts. - get_viewable_parent_uris takes linked_uris as a callable only; its empty guard and CodeSystem $validate-code's earlier empty check were redundant. - Organization.get_visible_to takes the queryset its callers always pass. - The CSV export test's export service no longer stubs the removed lookup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… on the repo they run over The root concept is resolved from the merged URL kwargs and query params, and its own repo is checked, but the repo the cascade or clone runs over comes from the URL. That repo now needs view access too (404 otherwise). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e too; new org names are unique regardless of case Source, collection and repo list searches in the requester's own scope match owner names in any case, like concept and mapping searches: when another owner of the same type has the same name in a different case, only public repos are searched. New orgs can't take a name that differs from an existing org's only in case, so no new such owners can be created. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ve-object check; no CSV export of events An event's description names its referenced object, so it's only the event type when the requester can't view that object now. Event lists can't be exported as CSV, which would skip the serializer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… match exact URL prefixes The concept and collection URLs were matched as case-insensitive substrings, so a collection whose URL differed only in case (another owner's) contributed its mappings. They now match by exact prefix, which can also use the URL indexes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… lists, which search all the requester's orgs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gh /user/collections/ resolve the collection's owner The /user/ routes carry no owner in the URL kwargs, so the collection URL the mappings are matched against now names the requester, as the exact prefix match needs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…se name differs from an existing one only in case Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
snyaggarwal
left a comment
There was a problem hiding this comment.
Reviewed commit fb7f85494233c4c958a35e72439f1c2526333319.
No actionable defects found in this review. I checked the changed access-control paths, including exact-case search scoping, FHIR repository selection, GraphQL visibility, deferred mapping links, hierarchy and collection membership, CSV exports, pins/follows/events, and the data migration.
Verification in an isolated checkout inside the existing API container:
- GraphQL, CodeSystem, ValueSet, pins, and events suites: 104 of 105 tests passed initially.
test_fetch_concepts_global_searchfailed because the shared Elasticsearch index returned IDs outside the test database while the fixture concepts were absent from the results. A temporary diagnostic explicitly indexed/refreshed those fixture concepts, and the same assertion then passed. This is a local verification caveat, not a confirmed application defect. - 53 tests passed covering public-read access, cross-repository access, search resolution, CSV export access, events, and the refreshed GraphQL diagnostic.
- Targeted pylint on the shared filtering/serialization code and affected GraphQL, concept, ValueSet, and event paths: 10.00/10.
I did not rerun the full project suite. No application files were changed.
|
Approved but requires merge conflicts to be handled |
snyaggarwal
left a comment
There was a problem hiding this comment.
Second review after conflict resolution, at 2f184ea857b4339dc4c9cf42632d05817d5d859a (merging master b016e253 into the previously reviewed fb7f8549).
No actionable defects found in the merge resolutions.
I inspected Git's reconstructed conflict-resolution diff and compared the result against both parents, focusing on the interaction between master's batched list serialization and this PR's collection-version visibility checks:
SourceChildPageSerializerforwards the requester from both normal request context and cascade'susercontext.- Batched collection-version loading applies the visibility criteria through the correct
expansion__relation before populating each row's cached values. - Concept and mapping serializers retain both the collection-version visibility mixin and master's list/source-version serialization behavior.
- The existing collection-version access regression covers anonymous users, outsiders, members, staff, version lists/details, and cascade; I checked that those paths now reach the merged implementation.
Verification: targeted pylint on all four resolution-related files passed at 10.00/10 in a temporary OCL image container; all 50 Python files changed against the current PR base parsed successfully; git diff --check passed and the resolved files contain no conflict markers.
Limitation: I did not rerun Django/integration tests for this commit. The OCL API, PostgreSQL, Redis, and Elasticsearch containers are stopped. The prior review's runtime results apply to the earlier commit, not this merged one. No application files were changed during this review.
…r a code system the caller isn't shown exactly as one that doesn't exist - An OCL path naming one version, with another version named (the version parameter, url|version, HEAD, a coding's version, valueSetVersion, systemVersion), names no version OCL holds and is not found. Before, the path's own version row was the starting point, so a private version in it was answered differently from a missing one. - The shared resolver takes an optional head_shown check. FHIR's resolve() passes the caller's view of HEAD, so a repository the caller isn't shown resolves to nothing before any of its versions is read, and costs what one that doesn't exist does for an anonymous caller at an owner base. The version then reuses HEAD's check of the caller's rights, so it isn't asked twice. Every other caller, $resolveReference included, passes nothing and is unchanged. - An owner instance route reads the newest public release in place of a private newest one only for a public repository, so a private repository costs what a missing one does there too. - ValueSet membership reads a code's concepts only in code systems the caller is shown, so what a hidden code system holds doesn't change the work done or the answer. - The #943 test compares each answer to what it would be, for the same requester and base, once the repository is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wdpwbn9tU8NmVL8oW2A75d
Refs OpenConceptLab/ocl_online#224 (partial: the items still open are listed on that ticket).
To finish (reviewer): the steps after merging live in the checklist on OpenConceptLab/ocl_online#224:
Items this PR leaves open are tracked in OpenConceptLab/ocl_online#482 to OpenConceptLab/ocl_online#488.
Follow-up to #906 and #910: more places where a response depended on content in repos the requester can't view. Each commit makes one fix and adds a regression test that fails on
masterand passes here.Fixes
Concepts and search
concepts/lookup/) checks view access, and its page cache holds only public sources. This restores e1f3138, which was reverted in 7bfb6c9.$matchfilters on exact-case owner and source names (owner_text.keyword,source_text.keyword). Atarget_repois now searched by the stored names of the repos that were checked, so aversionkey or a lowercaseowner_typeworks too./parents/and/children/list only concepts the requester can view, the same ruleincludeParentConcepts/includeChildConceptsalready use. They aren't searchable now: a search there listed the repo's concepts instead./user/orgs/...) leaves out the private content of an owner whose name differs only in case. Concepts are filtered on the exact-case owner. Mapping, source, collection and repo indexes have no exact-case owner field, so when such an owner exists those searches show only public content.includeParentConcepts/includeChildConceptsaren't cached, since their content depends on the requester.collection_versions(concept and mapping version detail,versions/?includeCollectionVersions=true) and the/collection-versions/lists show only collection versions the requester can view.Mappings, cascade and imports
to_source.$cascadeand$clonecheck view access on the repo they run over, as well as on the root concept's repo.omitIfExistsIn($cascade, and a collection reference'somit_if_exists_in) is ignored when the requester, or the reference's creator, can't view the repo.includeMappingsin collection scope) match the concept and collection URLs by exact prefix, not by case-insensitive substring.FHIR
$lookup/$validate-code: a code system the requester can't view is handled exactly like one that doesn't exist, with or without acode. A public code system that shares a canonical URL with a private one is no longer hidden by it.$validate-codereads only the value sets the request names: theurlvalue set and the route's collection. Other query params can't pick another owner or collection. Adisplaywith a code that isn't in the value set now returnsfalseinstead of a 500.GraphQL
conceptIdsor text), their mappings and the EStotalCountuse the REST visibility rules. Staff are unaffected.Users, pins, follows and events
lastLoginSince/lastLoginBeforeapply only for staff.extras.*filters match only the named extras field.subscribed_orgslists only orgs the requester can view, andpinsshows only for the user themselves and staff./following/andincludeFollowing) show only follows of objects the requester can view. That includes the user themselves once they lose access.scopes=orgs.null, for example for a repo made private after the event, and the description is just the event type. Event lists can't be exported as CSV.$cascadeentries list the collection versions their requester can view.CSV export
?csv=truebuilds each export from the view's own queryset, so its access checks apply, and stores it under the requester. The response shape ({"url": ...}) is unchanged.Behavior changes
lastLoginSince/lastLoginBeforeparams are ignored.extras.x=values containing:or\are now matched literally.$validate-codeignoresorg,user,collectionandsourcequery params. Aversionparam only narrows the route's own collection./parents/and/children/ignoreq.FAILEDfrom bulk import.?csv=trueon event lists is ignored.Migration
events/0011_private_follow_events(data only, reverse is a no-op) marks existing Followed/Unfollowed events of non-public orgs, repos and repo versions as private. On production it changes 2 rows and runs in seconds.After deploy
/parents/and/children/endpoints.Verification
manage.py test --parallel=1 --keepdb,PYTHONHASHSEED=2, fresh ES indexes): 2,586 tests, OK.pylint -j0 core/: 10.00/10.EXPLAIN(no ANALYZE) of the new mapping-link query: index scans onto_concept_code,to_source_urland the membership table.owner_text.keywordandsource_text.keywordsubfields exist in the production concepts index mapping.🤖 Generated with Claude Code