refactor(tenancy): answer which organization owns a scope, for other domains - #1450
Conversation
…domains Organizations should answer which organization owns a workspace or a membership, given only its ID, so a domain that stores bare scope IDs never queries the tenancy tables itself. These tests fail until OrganizationService offers the seven read-only lookups.
…aces it holds Both answers come from repository methods that already exist, so this step adds no query.
The lookup reads the caller's session on every call. The memoized organization_for_workspace_id in workspace_scope does not fit an ownership check: it keeps answering a deleted workspace's old organization, so a check built on it would accept a scope that no longer exists.
The member list keeps every status, so a scope on a suspended membership still resolves to its organization.
The organization-wide membership list joins workspace in one query rather than reading the workspace IDs first. It keeps every status, as the organization member list does. The WorkspaceMemberRepository docstring loses its claim that every access is keyed by the (workspace, user) pair. The lookup by membership ID makes it false.
…ds for every method The module docstring said every method but one resolves the organization from the caller alone. The seven ID lookups take no caller, the invitation token methods have none, and accepting or declining a pending membership reaches an organization that is not the active one. The narrowed rule covers what those methods share: a method acting for a caller never trusts an organization the request names, and another tenant's ID answers not-found. The docstring also cited private-repository issues and recounted how the service arrived. Both are cut.
|
Understand this PR’s impact Explore downstream dependencies and potential security impact with Blast Radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: mozilla-ai/otari/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (4)
Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review. WalkthroughChangesOrganization scope lookups
Priority: ⬇️ Low Estimated code review effort: 3 (Moderate) | ~20 minutes Change: Refactor Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
✨ Simplify code
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 |
Description
Nothing changes for someone using Otari. No route, response or behavior moves, and the OpenAPI spec, the Postman collection and the dashboard client are untouched.
A budget ceiling names what it caps by a bare ID: an organization, a workspace, or a membership in either. To check that a ceiling belongs to the caller's organization, the budgets code queries the organization tables itself today. This PR lets organizations answer those questions, so budgets can ask instead of reading another domain's tables.
OrganizationServicegains seven read-only lookups:get_organization_id_for_workspace,get_organization_id_for_organization_memberandget_workspace_id_for_workspace_member, eachNonefor an unknown IDhas_organizationget_workspace_ids_in_organization,get_organization_member_idsandget_workspace_member_ids_in_organizationThe new methods have no caller in
srcyet. Plan Task 5.5 of #1202 moves the organization budget surface onto them and deletes the budgets code's own copies of these queries. Until then both copies exist. This PR leaves the budgets files alone, because another change is editing them.Details for review:
organization_for_workspace_idinservices/workspace_scope.pyis not reused. Its memo is process-wide and keeps answering after a workspace is deleted. A probe that looked a workspace up, deleted it and looked it up again got the old organization ID back. An ownership check built on it would accept a ceiling on a workspace that no longer exists, and that ceiling would never bind. The new lookup reads the caller's session on every call, andtest_a_deleted_workspace_resolves_to_no_organizationpins that.has_organizationuses the repository's genericget, andget_workspace_ids_in_organizationuses the existingWorkspaceRepository.get_ids_by_organization. Four queries are new, in the two tenancy repositories.workspace, where the budgets code runs two queries.tests/unit/test_service_module_imports.pypins. No SQLAlchemy import is added, and no baseline or rule inscripts/check_architecture.pychanges.Two docstrings change:
WorkspaceMemberRepositorysaid every access is keyed by the (workspace, user) pair. The new lookup by membership ID makes that false, so the sentence is gone, in the commit that adds the lookup.organization_service.pysaid every method but one resolves the organization from the caller. The new lookups take no caller, the invitation-token methods have none, and accepting a pending membership reaches an organization that is not the active one. It now states the rule those methods share: a method acting for a caller never trusts an organization the request names, and another tenant's ID answers not-found. The rewrite also drops issue references and the account of how the service arrived. It is its owndocs(tenancy)commit, so it can be dropped alone.How to test it locally
The eight tests build two organizations, each with a workspace and members. They check each lookup's answer, that an unknown ID gives
None,Falseor[], and that one organization's rows never appear in the other's lists. One test deletes a workspace after a lookup and checks that the next lookup finds nothing.The full unit and integration suites, the OSS smoke gate,
make openapi-checkandmake postman-checkran on an earlier point of this branch, with no diff in either generated artifact. Their only failures were tests that read a local.envfile or need network access, whichAGENTS.mdlists as environment noise. The commits since then change only docstrings, and the last rebase brought in only agent-gates commits.PR Type
Relevant issues
Part of #1202 (plan Task 5.3). The first caller comes with plan Task 5.5.
Checklist
tests/unit,tests/integration).make lint,make typecheck,make test).uv run python scripts/generate_openapi.py).ARCHITECTURE.mdorscripts/check_architecture.py, the description names the rule and says why.AI Usage
AI Model/Tool used:
Claude (Opus 5), via Claude Code.
Any additional AI details you'd like to share:
This was shared work. The task comes from the plan I wrote for #1202: which seven questions organizations should answer, which repositories hold the queries, and that the memoized workspace lookup is reused only if its semantics fit. I set the limits it ran under: no edits to the budgets files another change is editing, no import from organizations into budgets, a failing test first, and one small commit per step. I chose the method names from options Claude proposed, and I asked for the module docstring rewrite after reviewing the first five commits. Claude wrote the code, the tests and the docstrings, ran the checks, found by experiment that the memoized lookup keeps a deleted workspace's organization, and drafted this description. I reviewed all six commits, and every added or touched docstring, before anything was pushed.
NOTE:
When responding to reviewer questions, please respond yourself rather than copy/pasting reviewer comments into an AI and pasting back its answer. We want to discuss with you, not your AI :)
Summary
Added seven read-only
OrganizationServicelookups for organization, workspace, and membership ownership.These lookups:
None,False, or empty lists for unknown IDs.Added repository queries and integration tests for normal, unknown, deleted-workspace, and cross-organization cases. Callers must perform authorization before using these lookups.