Skip to content

feat(api): add multi-tenancy support - #1220

Open
sudhir-intc wants to merge 1 commit into
mainfrom
feat-multi-tenancy
Open

feat(api): add multi-tenancy support#1220
sudhir-intc wants to merge 1 commit into
mainfrom
feat-multi-tenancy

Conversation

@sudhir-intc

@sudhir-intc sudhir-intc commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

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

  • In the REST API's the tenant-id is advertised through the 'x-tenant-id' header
  • The tenant-id is of the format: ^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$
  • In the REST API handlers Reject malformed tenant IDs before protected handlers execute.
  • Rejects conflicts between the x-tenant-id header and a request body's tenantId.
  • Propagate the validated tenant through request context without changing existing feature interface signatures.
  • Apply tenant-scoped lookups to device-management operations before any WSMAN interaction.
  • Scope CRUD operations for devices, profiles, domains, CIRA configurations, wireless configurations, and IEEE 802.1x configurations.
  • Keeps CIRA authentication tenant-aware by resolving the device by GUID, learning the tenant from the authenticated database record, and storing it on the connection.
  • Add the device tenant to redirection JWTs using a shared claim name.
  • Validates the redirection JWT tenant claim and propagates it into the WebSocket request context before invoking the redirect feature.
  • Keep the existing WebSocket feature interface unchanged while enforcing tenant isolation at the device lookup boundary.
  • Documents the tenant header and validation behavior in the OpenAPI definition.
  • Extends the MPS and RPS Postman collections with tenant-scoped and cross-tenant scenarios.

@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 67.07692% with 107 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.56%. Comparing base (6c1cdb7) to head (d2cabcc).

Files with missing lines Patch % Lines
internal/mocks/devicemanagement_mocks.go 0.00% 18 Missing ⚠️
internal/controller/httpapi/v1/devices.go 51.51% 14 Missing and 2 partials ⚠️
internal/usecase/devices/repo.go 0.00% 13 Missing ⚠️
internal/usecase/sqldb/device.go 78.43% 6 Missing and 5 partials ⚠️
internal/controller/httpapi/v1/ciraconfigs.go 53.84% 4 Missing and 2 partials ⚠️
internal/controller/httpapi/v1/domains.go 53.84% 4 Missing and 2 partials ⚠️
internal/controller/httpapi/v1/ieee8021xconfigs.go 53.84% 4 Missing and 2 partials ⚠️
internal/controller/httpapi/v1/profiles.go 60.00% 4 Missing and 2 partials ⚠️
internal/controller/httpapi/v1/wificonfigs.go 53.84% 4 Missing and 2 partials ⚠️
internal/usecase/nosqldb/mongo/device.go 71.42% 4 Missing and 2 partials ⚠️
... and 5 more
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sudhir-intc
sudhir-intc force-pushed the feat-multi-tenancy branch 2 times, most recently from 70cc870 to 0b158e1 Compare August 25, 2026 15:34
@sudhir-intc
sudhir-intc requested a lite review from Copilot August 25, 2026 16:35

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 GetByGUID for 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 calling rows.Next(), which can miss iteration errors and incorrectly return (nil, nil) on driver errors. The rows.Err() check should happen after Next()/scan completes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/tenant/tenant.go Outdated
Comment thread internal/usecase/sqldb/device.go Outdated
Comment thread internal/controller/httpapi/middleware/tenant.go
Comment thread internal/controller/httpapi/v1/tenant.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  • GetByGUID checks rows.Err() before iterating and returns nil, nil when rows.Next() is false, which can hide DB/driver errors (the recommended pattern is Next()/Scan() then rows.Err() after iteration).

Comment thread internal/usecase/sqldb/device.go
Comment thread internal/usecase/sqldb/device.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread internal/usecase/nosqldb/mongo/device.go
Comment thread internal/usecase/sqldb/device.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 GetByID detects multiple rows for the same (guid, tenantid), it wraps the condition as a DatabaseError with a plain error. This diverges from the repo’s established NotUniqueError semantics (used by Insert) and from Mongo’s GetByGUID behavior, and prevents consistent upstream handling (e.g., mapping to 409).
    internal/usecase/sqldb/device.go:231
  • GetByGUID returns the first matching row without checking for additional matches (GUID collisions across tenants) and also returns (nil, nil) when rows.Next() is false without checking rows.Err(). For CIRA auth (GUID-only), a duplicate GUID across tenants should be treated as a NotUnique error rather than arbitrarily selecting a tenant.

Comment thread internal/usecase/sqldb/device.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@sudhir-intc
sudhir-intc marked this pull request as ready for review August 28, 2026 11:04
@sudhir-intc
sudhir-intc requested a review from a team as a code owner August 28, 2026 11:04
@sudhir-intc
sudhir-intc requested a review from nbmaiti August 28, 2026 11:42
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.

Spike: Explore middleware extensibility options for Console

2 participants