-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat: Warn when multitenancy omits contextToOrchestratorId #11411
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,6 +55,11 @@ It's a [multitenancy][ref-multitenancy] option. | |
| caching key for various in-memory structures like data model compilation | ||
| results, etc. | ||
|
|
||
| It does not key database connections, execution queues, or pre-aggregation table | ||
| caches — those use [`context_to_orchestrator_id`][self-orchestrator-id], which | ||
| you must also define if [`driver_factory`][self-driver-factory] selects a | ||
| connection based on the security context. | ||
|
|
||
| Called on each request. | ||
|
|
||
| <CodeGroup> | ||
|
|
@@ -321,6 +326,15 @@ execution queues, pre-aggregation table caches. By default, the same instance is | |
| used for **all** tenants; override this property in situations where each tenant | ||
| requires their own Query Orchestrator. | ||
|
|
||
| Overriding it is **required** whenever [`driver_factory`][self-driver-factory] | ||
| selects a connection based on the security context, and the id must be derived | ||
| from the same tenant identifier. Setting | ||
| [`context_to_app_id`][self-opts-ctx-to-appid] alone is not enough: the app id keys | ||
| the data model compilation cache, while the driver is resolved once per data | ||
| source per *orchestrator* id. With the default single global orchestrator id, | ||
| every tenant reuses the connection resolved for whichever tenant queried first, | ||
| so one tenant receives another tenant's data. | ||
|
Comment on lines
+329
to
+336
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Low — "required" is stated a bit more absolutely than the mechanism warrants. Two edges where the blanket "required … whenever
|
||
|
|
||
| <Warning> | ||
|
|
||
| Please remember to override | ||
|
|
@@ -1511,6 +1525,7 @@ module.exports = { | |
| [ref-rest-scopes]: /reference/core-data-apis/rest-api#api-scopes | ||
| [ref-config-options]: /admin/connect-to-data#configuration-options | ||
| [self-orchestrator-id]: #context_to_orchestrator_id | ||
| [self-driver-factory]: #driver_factory | ||
| [ref-multiple-data-sources]: /admin/connect-to-data/multiple-data-sources | ||
| [ref-websockets]: /recipes/core-data-api/real-time-data-fetch | ||
| [ref-matching-preaggs]: /docs/pre-aggregations/matching-pre-aggregations | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -455,6 +455,20 @@ export class OptsHandler { | |
| }); | ||
| } | ||
|
|
||
| if ( | ||
| opts.contextToAppId && | ||
| !opts.contextToOrchestratorId && | ||
| this.isCustomDriverFactory() | ||
| ) { | ||
| this.core.logger('Multitenancy Without ContextToOrchestratorId', { | ||
| warning: ( | ||
| 'You are using multitenancy with a custom driverFactory but without ' + | ||
| 'configuring contextToOrchestratorId: ' + | ||
| 'https://cube.dev/docs/reference/configuration/config#context_to_orchestrator_id' | ||
| ), | ||
| }); | ||
| } | ||
|
Comment on lines
+458
to
+470
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. High — this will fire (and give harmful advice) for a large class of safe configurations. The condition is
The actual unsafe condition is "
|
||
|
|
||
| if (options.devServer && !options.apiSecret) { | ||
| options.apiSecret = crypto.randomBytes(16).toString('hex'); | ||
| displayCLIWarning( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1202,4 +1202,62 @@ describe('OptsHandler class', () => { | |
| expect(Array.isArray(permissions)).toBeTruthy(); | ||
| expect(permissions).toEqual(['graphql', 'meta', 'data', 'jobs']); | ||
| }); | ||
|
|
||
| describe('contextToOrchestratorId warning', () => { | ||
| const WARNING_MSG = 'Multitenancy Without ContextToOrchestratorId'; | ||
|
|
||
| const tenantId = ({ securityContext }: any) => ( | ||
| `CUBE_APP_${securityContext.tenant}` | ||
| ); | ||
|
|
||
| const buildCore = (opts: CreateOptions) => { | ||
| const logger = jest.fn(); | ||
| const core = new CubejsServerCoreExposed({ | ||
| ...conf, | ||
| logger, | ||
| scheduledRefreshTimer: false, | ||
| ...opts, | ||
| }); | ||
|
|
||
| expect(core.options).toBeDefined(); | ||
|
|
||
| return logger.mock.calls.filter((call) => call[0] === WARNING_MSG); | ||
| }; | ||
|
|
||
| test('must warn on tenant-specific driverFactory without contextToOrchestratorId', () => { | ||
| const warnings = buildCore({ | ||
| contextToAppId: tenantId, | ||
| driverFactory: ({ securityContext }: any) => ({ | ||
| type: <DatabaseType>'postgres', | ||
| database: `tenant_${securityContext.tenant}`, | ||
| }), | ||
| }); | ||
|
|
||
| expect(warnings).toHaveLength(1); | ||
| expect(warnings[0][1].warning).toContain('contextToOrchestratorId'); | ||
| }); | ||
|
|
||
| test('must not warn when contextToOrchestratorId is configured', () => { | ||
| const warnings = buildCore({ | ||
| contextToAppId: tenantId, | ||
| contextToOrchestratorId: tenantId, | ||
| driverFactory: ({ securityContext }: any) => ({ | ||
| type: <DatabaseType>'postgres', | ||
| database: `tenant_${securityContext.tenant}`, | ||
| }), | ||
| }); | ||
|
|
||
| expect(warnings).toHaveLength(0); | ||
| }); | ||
|
|
||
| test('must not warn without a custom driverFactory', () => { | ||
| process.env.CUBEJS_DB_TYPE = 'postgres'; | ||
|
|
||
| const warnings = buildCore({ | ||
| contextToAppId: tenantId, | ||
| }); | ||
|
|
||
| expect(warnings).toHaveLength(0); | ||
| }); | ||
| }); | ||
|
Comment on lines
+1253
to
+1262
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Low — test gaps and env leakage.
|
||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Medium — this correctly contradicts another docs page, which should be fixed in the same PR.
The new sentence ("It does not key database connections…") matches the implementation: drivers are memoized inside the closure created per
orchestratorIdingetOrchestratorApi(packages/cubejs-server-core/src/core/server.ts:572-620), never per app id.But
docs-mintlify/embedding/multitenancy.mdx:282-284still says the opposite:That's exactly the misconception this PR is trying to kill, on the primary multitenancy page. Worth dropping "connection pool" from that sentence here so the two pages agree.
Fix this →