feat(api): add multi-tenancy support - #1220
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1220 +/- ##
==========================================
+ Coverage 50.81% 53.56% +2.74%
==========================================
Files 149 153 +4
Lines 13872 14103 +231
==========================================
+ Hits 7049 7554 +505
+ Misses 6219 5919 -300
- Partials 604 630 +26 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
70cc870 to
0b158e1
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces request-scoped multi-tenancy across Console’s REST API, CIRA (APF) authentication path, and WebSocket redirection flow by validating/propagating an optional x-tenant-id and enforcing tenant-scoped device lookups before any WSMAN interaction.
Changes:
- Adds tenant context propagation + enforcement: REST middleware scopes requests, usecases consistently resolve devices via tenant context, and repos add a tenant-agnostic
GetByGUIDfor CIRA authentication. - Enforces tenant ownership on CIRA sockets and includes tenant information in redirection JWTs, validating and propagating that tenant into the WebSocket request context.
- Updates OpenAPI route options/tests and extends Postman collections/environment with tenancy scenarios.
Reviewed changes
Copilot reviewed 52 out of 53 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/usecase/sqldb/device.go | Adjusts single-row scan logic and adds GetByGUID (tenant-unscoped) lookup. |
| internal/usecase/sqldb/device_test.go | Adds unit coverage for GetByGUID behavior in SQL repo. |
| internal/usecase/nosqldb/mongo/device.go | Adds GetByGUID to Mongo repo for CIRA-style GUID-only resolution. |
| internal/usecase/nosqldb/mongo/device_test.go | Adds Mongo repo tests for GetByGUID (found/not found/malformed). |
| internal/usecase/devices/wsman/message.go | Adds tenant ownership enforcement for CIRA socket usage. |
| internal/usecase/devices/wsman/message_test.go | Adds test ensuring CIRA tenant mismatch is rejected. |
| internal/usecase/devices/wifistate.go | Switches management lookups to tenant-scoped device resolution. |
| internal/usecase/devices/wifiprofile.go | Switches management lookups to tenant-scoped device resolution. |
| internal/usecase/devices/tenant.go | Introduces centralized tenant-scoped device lookup helper for usecase methods. |
| internal/usecase/devices/repo.go | Adds GetByGUID usecase API for GUID-only callers (CIRA). |
| internal/usecase/devices/power.go | Switches management lookups to tenant-scoped device resolution. |
| internal/usecase/devices/network.go | Switches management lookups to tenant-scoped device resolution. |
| internal/usecase/devices/linkpreference.go | Switches management lookups to tenant-scoped device resolution. |
| internal/usecase/devices/kvm.go | Switches management lookups to tenant-scoped device resolution. |
| internal/usecase/devices/interfaces.go | Extends repo/feature interfaces with GetByGUID. |
| internal/usecase/devices/interceptor.go | Enforces tenant-scoped device lookup before redirect feature execution. |
| internal/usecase/devices/info.go | Switches management lookups to tenant-scoped device resolution. |
| internal/usecase/devices/features.go | Switches management lookups to tenant-scoped device resolution. |
| internal/usecase/devices/consent.go | Switches management lookups to tenant-scoped device resolution. |
| internal/usecase/devices/connections.go | Switches management lookups to tenant-scoped device resolution. |
| internal/usecase/devices/certificates.go | Switches management lookups to tenant-scoped device resolution. |
| internal/usecase/devices/boot.go | Switches management lookups to tenant-scoped device resolution. |
| internal/usecase/devices/alarms.go | Switches management lookups to tenant-scoped device resolution. |
| internal/tenant/tenant.go | Adds tenant format/validation and context propagation utilities. |
| internal/tenant/tenant_test.go | Adds unit tests for tenant validation and context round-trip. |
| internal/mocks/devicemanagement_mocks.go | Updates generated mocks to include GetByGUID. |
| internal/controller/ws/v1/redirect.go | Validates tenant claim in redirection JWT and propagates tenant into WS request context. |
| internal/controller/ws/v1/redirect_test.go | Adds WS tests for tenant claim propagation and invalid-tenant rejection. |
| internal/controller/tcp/cira/tunnel.go | Stores tenant on CIRA connection entry and improves structured logging. |
| internal/controller/tcp/cira/handler.go | Learns tenant from DB on CIRA auth by GUID-only lookup and stores it on handler. |
| internal/controller/tcp/cira/handler_test.go | Adds coverage for tenant learning during CIRA auth. |
| internal/controller/openapi/tenant_test.go | Ensures protected routes declare tenant header in OpenAPI spec. |
| internal/controller/openapi/route_options.go | Documents tenant header on protected routes via shared route option. |
| internal/controller/httpapi/v1/wificonfigs.go | Scopes CRUD operations by tenant header and enforces body/header consistency on writes. |
| internal/controller/httpapi/v1/tenant.go | Adds helpers to read tenant from context and apply it to request bodies. |
| internal/controller/httpapi/v1/tenant_test.go | Adds tests for tenant header/body reconciliation helpers. |
| internal/controller/httpapi/v1/profiles.go | Scopes CRUD operations by tenant header and enforces body/header consistency on writes. |
| internal/controller/httpapi/v1/profiles_test.go | Adds REST tests asserting tenant header propagation for profiles endpoints. |
| internal/controller/httpapi/v1/ieee8021xconfigs.go | Scopes CRUD operations by tenant header and enforces body/header consistency on writes. |
| internal/controller/httpapi/v1/explorer.go | Passes tenant header through to explorer execution. |
| internal/controller/httpapi/v1/explorer_test.go | Adds coverage ensuring explorer routes pass tenant scope. |
| internal/controller/httpapi/v1/error.go | Maps CIRA tenant mismatch to HTTP 401 (per legacy behavior). |
| internal/controller/httpapi/v1/domains.go | Scopes CRUD operations by tenant header and enforces body/header consistency on writes. |
| internal/controller/httpapi/v1/devices.go | Scopes list/get/delete and redirection login by tenant; adds tenant claim to redirection JWT. |
| internal/controller/httpapi/v1/devices_test.go | Extends tests to assert tenant claim in redirection JWT. |
| internal/controller/httpapi/v1/ciraconfigs.go | Scopes CRUD operations by tenant header and enforces body/header consistency on writes. |
| internal/controller/httpapi/router.go | Adds tenant middleware to protected router group. |
| internal/controller/httpapi/middleware/tenant.go | Adds tenant header validation + request context scoping middleware. |
| internal/controller/httpapi/middleware/tenant_test.go | Adds middleware tests for scoping and rejection of malformed headers. |
| integration-test/collections/console_rps_apis.postman_collection.json | Adds RPS tenancy scenarios and shared pre-request tenant header logic. |
| integration-test/collections/console_mps_apis.postman_collection.json | Adds MPS tenancy scenarios and shared pre-request tenant header logic. |
| integration-test/collections/console_environment.postman_environment.json | Adds tenant variables used by Postman tenancy scenarios. |
| .golangci.yml | Updates gofumpt formatter configuration. |
Files not reviewed (1)
- internal/mocks/devicemanagement_mocks.go: Generated file
Suppressed comments (1)
internal/usecase/sqldb/device.go:228
rows.Err()is checked before callingrows.Next(), which can miss iteration errors and incorrectly return (nil, nil) on driver errors. Therows.Err()check should happen afterNext()/scan completes.
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
4227369 to
861f20d
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 55 out of 56 changed files in this pull request and generated 2 comments.
Files not reviewed (1)
- internal/mocks/devicemanagement_mocks.go: Generated file
Suppressed comments (1)
internal/usecase/sqldb/device.go:227
GetByGUIDchecksrows.Err()before iterating and returnsnil, nilwhenrows.Next()is false, which can hide DB/driver errors (the recommended pattern isNext()/Scan()thenrows.Err()after iteration).
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 57 out of 58 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- internal/mocks/devicemanagement_mocks.go: Generated file
Suppressed comments (1)
internal/usecase/sqldb/device.go:238
- GetByGUID returns the first row for a GUID even if multiple rows exist (e.g., same GUID registered under multiple tenants). The Mongo implementation explicitly returns a NotUnique error in that case, so this can lead to backend-dependent behavior and ambiguous tenant resolution during CIRA auth.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 57 out of 58 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- internal/mocks/devicemanagement_mocks.go: Generated file
Suppressed comments (2)
internal/usecase/sqldb/device.go:183
- When
GetByIDdetects multiple rows for the same(guid, tenantid), it wraps the condition as aDatabaseErrorwith a plainerror. This diverges from the repo’s establishedNotUniqueErrorsemantics (used byInsert) and from Mongo’sGetByGUIDbehavior, and prevents consistent upstream handling (e.g., mapping to 409).
internal/usecase/sqldb/device.go:231 GetByGUIDreturns the first matching row without checking for additional matches (GUID collisions across tenants) and also returns(nil, nil)whenrows.Next()is false without checkingrows.Err(). For CIRA auth (GUID-only), a duplicate GUID across tenants should be treated as a NotUnique error rather than arbitrarily selecting a tenant.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 57 out of 58 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- internal/mocks/devicemanagement_mocks.go: Generated file
Suppressed comments (1)
internal/usecase/sqldb/device.go:244
- GetByGUID returns the first row for a GUID without checking whether additional rows exist. If the same GUID is present in multiple tenants (or data integrity is otherwise violated), this can mis-attribute CIRA authentication and break tenant isolation. Mongo’s GetByGUID defensively returns NotUnique in this case; sqldb should do the same.
bd95d93 to
d2cabcc
Compare
Summary
Adds tenant isolation across Console REST, CIRA, and WebSocket management paths.
Fixes: #780
Refer this doc https://github.com/device-management-toolkit/console/wiki/console-tenancy-design to understand the high-level design decisions and the implementation deatils.
Highlevel Implemenation
^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$x-tenant-idheader and a request body'stenantId.