Skip to content

feat: enforce scopeBy in Go and Python services - #169

Merged
cultron merged 1 commit into
mainfrom
feat/scope-go-python
Oct 29, 2026
Merged

cultron merged 1 commit into
mainfrom
feat/scope-go-python

Conversation

@cultron

@cultron cultron commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Why

Since 0.45, Go and Python services check who's calling but don't filter rows by tenant. In a multi-tenant schema, any signed-in user could read and change every tenant's rows.

What

With auth configured, Go and Python now match TypeScript:

  • lists and reads only return the caller's tenant's rows;
  • a create stores the caller's tenant, whatever the body says;
  • an update can't move a row to another tenant;
  • another tenant's row returns 404;
  • a caller with no tenant gets 403;
  • bypassRoles and enforceOn are honored.

How each language does it:

  • Go: the middleware also puts the caller on the request context. Scoped services add a tenant WHERE before the user's filters, and those filters (including the grouped or) are ANDed with it. The tenant is compared as text, because claims are strings and the column may be an integer or a uuid. Shared helpers live in services/scope.go and handlers/scope.go.
  • Python: QueryBuilder takes scope conditions. Routers pass request.state.auth to the service, which scopes every query and sets the tenant on create.
  • TypeScript fix found by the new checks: a PATCH with another tenant's organizationId moved the row to that tenant. The scope guard now forces the caller's tenant on update.

Nested scopeBy paths (task.workspaceId) are still TypeScript-only. Go and Python print a warning for them.

Tests

  • The auth smoke now runs the tenant checks in all three languages. Another tenant can't read, list, update or delete the row; the owner's update can't move it; a caller with no tenant gets 403; and a field named orgId scopes like organizationId. It runs for all six provider cases.
  • Unit tests: 518 pass.

Locally, every auth check passes in TypeScript, Go and Python.

With auth configured, a Go or Python service now keeps each caller to its own
tenant's rows, matching TypeScript: lists and reads are filtered, a create
stores the caller's tenant whatever the body says, an update cannot move a row
to another tenant, another tenant's row is a 404, and a caller with no tenant
gets 403. Bypass roles and enforceOn are honored.

- Go: the auth middleware also stores the caller on the request context;
  scoped services add a tenant WHERE (compared as text) before the user's
  filters, which are ANDed. Shared helpers in services/scope.go and
  handlers/scope.go.
- Python: QueryBuilder takes scope conditions; routers pass the caller to the
  service, which scopes every query and sets the tenant on create.
- TypeScript: an update can no longer move a scoped row to another tenant
  (the scope guard now forces the caller's tenant on update too).
- Nested scopeBy paths are still TypeScript only; Go and Python warn.

The auth smoke now checks tenant isolation in all three languages, including
cross-tenant update and delete and an update that tries to move a row.
@cultron
cultron merged commit 2e37d80 into main Oct 29, 2026
8 checks passed
@cultron
cultron deleted the feat/scope-go-python branch October 29, 2026 13:44
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.

1 participant