Skip to content

Feat/project soft tenant scope - #22

Merged
KilianBoute merged 2 commits into
mainfrom
feat/project-soft-tenant-scope
Aug 18, 2026
Merged

KilianBoute merged 2 commits into
mainfrom
feat/project-soft-tenant-scope

Conversation

@KilianBoute

@KilianBoute KilianBoute commented Aug 18, 2026 •

Copy link
Copy Markdown
Member

Description

Introduces a shared tenancy resolution layer that consistently determines the organisation and project ownership of devices across services.

The main motivation is to eliminate inconsistent tenant scoping between independently deployed pipelines. Previously, ownership resolution logic could diverge across services, causing resources to become invisible to the correct tenant or incorrectly visible to another tenant without producing explicit failures. By centralizing this logic into a single implementation, tenant scoping becomes deterministic and reliable across the platform.

The new resolver enforces strict organisation ownership validation against persisted records while introducing a soft project-scoping strategy that safely supports the current single-project rollout. Project assignment is derived deterministically instead of relying on potentially incomplete or inconsistent project records, ensuring all services resolve ownership identically during migration phases.

This change also improves backward compatibility by handling legacy ownership formats and pre-bootstrap deployments without breaking existing data flows. Ambiguous or invalid ownership states now fail closed instead of silently producing incorrectly scoped resources, reducing the risk of cross-tenant leakage and difficult-to-debug visibility issues.

Comprehensive tests were added to validate canonical ownership resolution, legacy migration behavior, bootstrap edge cases, and failure scenarios. Dependency updates pull in the required shared models and project scope logic needed to support the new tenancy behavior.

KilianBoute and others added 2 commits August 18, 2026 14:03
Every pipeline stamps the organisation and project that own a source device
onto the resources it derives from that device's media, and Hub API later
reads them back with a single selector. Monitor had the only implementation;
the remaining pipelines were about to copy it.

Copying is the wrong shape for this. Two services that resolve the same device
differently do not fail loudly: they write resources that are silently
invisible to the tenant, or visible to the wrong one. Independently deployed
services can only agree if there is exactly one implementation, so it moves
here before the duplication spreads.

The two halves of the resolution are deliberately asymmetric. The organisation
is verified against persisted records, because it is the tenant boundary, a
wrong value crosses tenants, and a populated collection exists to check
against; ambiguous, orphaned, and conflicting ownership all fail closed rather
than guess. The project is assigned from models/pkg/projectscope, because
during the hidden rollout no authoritative project record exists and any query
would let two services disagree.

An empty organisation collection means organisations-bootstrap has not run on
that instance rather than that the device is unowned, so a primary
organisation is derived from the master user's id — the value the migration
will later persist. Erroring there would drop every message on an un-migrated
deployment.

This lands beside pkg/database rather than inside it: resolution needs
models.Device, and pkg/database stays a dependency-light leaf with no models
import. Collection names are configuration because services do not all spell
them the same way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Before bootstrap has run the organisation collection is empty, so ownership is
derived rather than read: a primary organisation reuses the master user's id.
That derivation is only meaningful because bootstrap mints one organisation per
master user.

When ownership followed a sub-user's user_id, the existence check proved only
that the sub-user was real. A dangling master link therefore produced an
organisation id for an account that no longer exists, and bootstrap will never
create it. The media would be stamped with a tenant no reader selects and
disappear — the silent failure this resolver exists to prevent, arrived at by a
different route.

Confirm the owner has a user record before deriving from it. The lookup runs
only when a master link was followed and only on the pre-bootstrap path, so the
common case pays nothing: when the owner is the queried user itself its
existence is already proven, and when an organisation record exists it is
returned before this point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@KilianBoute
KilianBoute merged commit ec2979e into main Aug 18, 2026
5 checks passed
@KilianBoute
KilianBoute deleted the feat/project-soft-tenant-scope branch August 18, 2026 14:19
@codecov

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.86577% with 30 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
pkg/tenancy/device.go 79.86% 14 Missing and 16 partials ⚠️

📢 Thoughts on this report? Let us know!

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