Skip to content

refactor(tenancy): answer which organization owns a scope, for other domains - #1450

Merged
peteski22 merged 6 commits into
mainfrom
feat/organization-scope-lookups
Sep 21, 2026
Merged

peteski22 merged 6 commits into
mainfrom
feat/organization-scope-lookups

Conversation

@peteski22

@peteski22 peteski22 commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

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. OrganizationService gains seven read-only lookups:

  • get_organization_id_for_workspace, get_organization_id_for_organization_member and get_workspace_id_for_workspace_member, each None for an unknown ID
  • has_organization
  • get_workspace_ids_in_organization, get_organization_member_ids and get_workspace_member_ids_in_organization

The new methods have no caller in src yet. 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_id in services/workspace_scope.py is 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, and test_a_deleted_workspace_resolves_to_no_organization pins that.
  • Two lookups need no new query. has_organization uses the repository's generic get, and get_workspace_ids_in_organization uses the existing WorkspaceRepository.get_ids_by_organization. Four queries are new, in the two tenancy repositories.
  • The two membership lists keep every status, suspended included, as the budgets queries they replace do.
  • The workspace membership list is one join on workspace, where the budgets code runs two queries.
  • The lookups take no caller and check no access, so a caller authorizes first.
  • Budgets still depends on organizations and never the reverse. No organizations module imports anything from budgets, which tests/unit/test_service_module_imports.py pins. No SQLAlchemy import is added, and no baseline or rule in scripts/check_architecture.py changes.

Two docstrings change:

  • WorkspaceMemberRepository said 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.
  • The module docstring of organization_service.py said 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 own docs(tenancy) commit, so it can be dropped alone.

How to test it locally

uv run pytest tests/integration/test_organization_scope_lookups.py -v
make lint && make typecheck

The eight tests build two organizations, each with a workspace and members. They check each lookup's answer, that an unknown ID gives None, False or [], 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-check and make postman-check ran on an earlier point of this branch, with no diff in either generated artifact. Their only failures were tests that read a local .env file or need network access, which AGENTS.md lists as environment noise. The commits since then change only docstrings, and the last rebase brought in only agent-gates commits.

PR Type

  • New Feature
  • Bug Fix
  • Refactor
  • Documentation
  • Infrastructure / CI

Relevant issues

Part of #1202 (plan Task 5.3). The first caller comes with plan Task 5.5.

Checklist

  • I understand the code I am submitting.
  • I have added or updated tests that cover my change (tests/unit, tests/integration).
  • I ran the Definition of Done checks locally (make lint, make typecheck, make test).
  • Documentation was updated where necessary.
  • If the API contract changed, I regenerated the OpenAPI spec (uv run python scripts/generate_openapi.py).
  • If this changes a rule in ARCHITECTURE.md or scripts/check_architecture.py, the description names the rule and says why.

AI Usage

  • No AI was used.
  • AI was used for drafting/refactoring.
  • This is fully AI-generated.

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 :)

  • I am an AI Agent filling out this form (check box if true)

Summary

Added seven read-only OrganizationService lookups for organization, workspace, and membership ownership.

These lookups:

  • Resolve related organization and workspace IDs.
  • Check whether an organization exists.
  • List memberships and workspaces for an organization.
  • Return None, False, or empty lists for unknown IDs.
  • Include memberships with all statuses.

Added repository queries and integration tests for normal, unknown, deleted-workspace, and cross-organization cases. Callers must perform authorization before using these lookups.

…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.
@peteski22
peteski22 deployed to integration-tests September 21, 2026 17:58 — with GitHub Actions Active
@peteski22
peteski22 deployed to integration-tests September 21, 2026 17:58 — with GitHub Actions Active
@peteski22
peteski22 deployed to integration-tests September 21, 2026 17:58 — with GitHub Actions Active
@coderabbitai

coderabbitai Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

Understand this PR’s impact

Explore downstream dependencies and potential security impact with Blast Radius.

View blast radius →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: mozilla-ai/otari/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 0a9be88b-68d2-4487-842d-35795538eff3

📥 Commits

Reviewing files that changed from the base of the PR and between 1948278 and 5deedf9.

📒 Files selected for processing (4)
  • src/gateway/repositories/tenancy/organization_member_repository.py
  • src/gateway/repositories/tenancy/workspace_repository.py
  • src/gateway/services/tenancy/organization_service.py
  • tests/integration/test_organization_scope_lookups.py

Included review availability: Your plan provides up to 2 included reviews per hour; 1 remains after this review.


Walkthrough

Changes

Organization scope lookups

Layer / File(s) Summary
Repository lookup methods
src/gateway/repositories/tenancy/organization_member_repository.py, src/gateway/repositories/tenancy/workspace_repository.py
Repositories now resolve owning organization or workspace IDs and list membership IDs for an organization without filtering status.
Service scope API
src/gateway/services/tenancy/organization_service.py
OrganizationService now provides seven read-only methods that delegate to the repository lookups and return IDs, lists, or None.
Integration validation
tests/integration/test_organization_scope_lookups.py
Integration tests cover existing, unknown, deleted, and suspended organization-scope records.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Refactor

Suggested reviewers: njbrake

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title accurately describes the refactor and uses an imperative verb. However, it is 76 characters, exceeding the requested approximate 70-character limit. Shorten the title to about 70 characters or fewer, for example: "refactor(tenancy): expose organization ownership lookups".
Docstring Coverage ⚠️ Warning Docstring coverage is 65.63% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 32 functions across 4 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description includes all required sections, explains the change and its scope, documents local testing and known environment limitations, identifies the PR type and issue, completes the checklist,…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
✨ Simplify code
  • Commit to this branch
  • Create a new PR

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@peteski22
peteski22 deployed to integration-tests September 21, 2026 17:59 — with GitHub Actions Active
@peteski22
peteski22 marked this pull request as ready for review September 21, 2026 18:50
@peteski22
peteski22 requested a review from tbille September 21, 2026 18:50
@peteski22
peteski22 merged commit b967dae into main Sep 21, 2026
20 checks passed
@peteski22
peteski22 deleted the feat/organization-scope-lookups branch September 21, 2026 19:00

This branch was successfully deployed

1 active deployment
integration-tests — 5deedf96 Deployed Sep 21, 2026 by peteski22 via test-integration (4/4) #2380
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