Skip to content

Conversation

@felixevers
Copy link
Member

@felixevers felixevers commented Dec 21, 2025

Note

Implements location-scoped authorization and wiring end-to-end, plus Location CRUD.

  • Auth/Context: Parse organization claim; on login compute/update user_root_locations (creating org root LocationNodes as needed); cache accessible location IDs; thread-safe LockedAsyncSession.
  • Service: New AuthorizationService to compute/ cache accessible locations, check patient/task access, and filter queries.
  • Resolvers: Enforce access checks in patient, patients, recent_patients, task, tasks, recent_tasks; add Location queries/create|update|delete mutations and location_node subscriptions.
  • Schema/Types: Add inputs CreateLocationNodeInput/UpdateLocationNodeInput, LocationNodeType.organization_ids; extend queries with rootLocationIds filters; update generated client.
  • DB/Models/Migrations: Add user_root_locations and location_organizations tables; change LocationNode.kind to enum; expose new association tables via models; merge migration head.
  • Frontend: Global context stores user rootLocations and selected rootLocationIds (persisted); new LocationSelectionDialog (multi-select root); filter Patients/Tasks/GlobalData by rootLocationIds; UI tweaks (header picker, chips coloring, settings show organizations).
  • DevOps/Tests: Exclude migrations from ruff in CI; update fixtures/tests to grant location access; scaffold supports organization_ids; Keycloak export emits organization attribute and scope.

Written by Cursor Bugbot for commit 0596f3a. This will update automatically on new commits. Configure here.

@felixevers felixevers self-assigned this Dec 21, 2025
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is being reviewed by Cursor Bugbot

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

location_service = PatientMutation._get_location_service(db)
accessible_location_ids = await auth_service.get_user_accessible_location_ids(
info.context.user
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing context parameter in authorization call breaks caching

The call to get_user_accessible_location_ids is missing the second context parameter. All other calls in the codebase pass info.context.user, info.context, but this one only passes info.context.user. This bypasses the caching mechanism in AuthorizationService, causing redundant database queries during update_patient operations and potential inconsistency within a single request.

Fix in Cursor Fix in Web

result = await info.context.db.execute(
select(models.Task)
.where(models.Task.id == id)
.options(selectinload(models.Task.patient).selectinload(models.Patient.assigned_locations))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task queries missing teams relationship for authorization check

The can_access_patient method checks both patient.assigned_locations and patient.teams for authorization. However, all task queries only eager-load assigned_locations with selectinload(models.Task.patient).selectinload(models.Patient.assigned_locations) but not teams. In async SQLAlchemy, accessing an unloaded relationship raises a MissingGreenlet error. This will cause authorization checks to fail for any user whose access is granted through teams. Similarly, delete_patient uses repo.get_by_id() which loads no relationships before calling can_access_patient.

Additional Locations (2)

Fix in Cursor Fix in Web

@felixevers felixevers merged commit 1601010 into main Dec 24, 2025
19 checks passed
@felixevers felixevers deleted the feat/scope-organization branch December 24, 2025 01:15
f"User {self.id} has {len(user_root_location_ids)} root location IDs but query returned empty. "
f"Checking if locations exist: {[loc.id for loc in existing_locations]} "
f"with parent_ids: {[loc.parent_id for loc in existing_locations]}"
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debugging queries and logging left in production code

The root_locations field resolver contains debugging code that appears unintended for production. It performs a redundant query to user_root_locations before the actual query, logs user IDs and location IDs at INFO level, and conditionally performs another diagnostic query when results are empty. This causes unnecessary database load and verbose logging. The actual work only requires lines 114-123; the rest appears to be diagnostic code that was left in.

Fix in Cursor Fix in Web

@strawberry.field
def organizations(self, info) -> str | None:
"""Get organizations from the context"""
return info.context.organizations
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Organizations field returns requester's data for any user

The organizations field on UserType returns info.context.organizations, which is always the requesting user's organizations regardless of which user is being queried. When using user(id: "other-user") or users queries, the organizations field will incorrectly return the requester's organizations instead of the target user's. Since the organizations column was removed from the User model, this field now has misleading semantics - it appears to be user-specific data but actually reflects the auth context.

Fix in Cursor Fix in Web

raise GraphQLError(
"Insufficient permission. Please contact an administrator if you believe this is an error.",
extensions={"code": "FORBIDDEN"},
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users can create inaccessible root locations

The authorization check in create_location_node only validates parent_id when it's truthy. When parent_id is None (creating a root location), the check is bypassed. This allows any authenticated user with at least one accessible location to create orphaned root locations that they cannot subsequently access, since the new location is not added to their user_root_locations. The mutation succeeds but returns a location the user can never query again.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants