From 863ae6d910833567cd89cc3991d30a89a0543a85 Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Fri, 28 Mar 2025 19:54:02 +0200 Subject: [PATCH 1/4] feat(api-gateway)!: Move '/v1/sql' to new `sql` API Scope --- packages/cubejs-api-gateway/src/gateway.ts | 8 ++-- .../cubejs-api-gateway/src/types/strings.ts | 1 + .../test/permissions.test.ts | 43 +++++++++++++------ 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index 1dedb76efd759..0f89cba369c53 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -155,7 +155,7 @@ class ApiGateway { public readonly contextToApiScopesFn: ContextToApiScopesFn; public readonly contextToApiScopesDefFn: ContextToApiScopesFn = - async () => ['graphql', 'meta', 'data']; + async () => ['graphql', 'meta', 'data', 'sql']; protected readonly requestLoggerMiddleware: RequestLoggerMiddlewareFn; @@ -1311,7 +1311,7 @@ class ApiGateway { res, }: {query: string, disablePostProcessing: boolean} & BaseRequest) { try { - await this.assertApiScope('data', context.securityContext); + await this.assertApiScope('sql', context.securityContext); const result = await this.sqlServer.sql4sql(query, disablePostProcessing, context.securityContext); res({ sql: result }); @@ -1339,7 +1339,7 @@ class ApiGateway { const requestStarted = new Date(); try { - await this.assertApiScope('data', context.securityContext); + await this.assertApiScope('sql', context.securityContext); const [queryType, normalizedQueries] = await this.getNormalizedQueries(query, context, disableLimitEnforcing, memberExpressions); @@ -2448,7 +2448,7 @@ class ApiGateway { ); } else { scopes.forEach((p) => { - if (['graphql', 'meta', 'data', 'jobs'].indexOf(p) === -1) { + if (['graphql', 'meta', 'data', 'sql', 'jobs'].indexOf(p) === -1) { throw new Error( `A user-defined contextToApiScopes function returns a wrong scope: ${p}` ); diff --git a/packages/cubejs-api-gateway/src/types/strings.ts b/packages/cubejs-api-gateway/src/types/strings.ts index 129cb3c217758..b8c24670d8dfb 100644 --- a/packages/cubejs-api-gateway/src/types/strings.ts +++ b/packages/cubejs-api-gateway/src/types/strings.ts @@ -107,6 +107,7 @@ type ApiScopes = 'graphql' | 'meta' | 'data' | + 'sql' | 'jobs'; export { diff --git a/packages/cubejs-api-gateway/test/permissions.test.ts b/packages/cubejs-api-gateway/test/permissions.test.ts index c4aa2ad079a56..b1a850f15437c 100644 --- a/packages/cubejs-api-gateway/test/permissions.test.ts +++ b/packages/cubejs-api-gateway/test/permissions.test.ts @@ -62,6 +62,13 @@ describe('Gateway Api Scopes', () => { expect(res.body && res.body.error) .toStrictEqual('API scope is missing: data'); + res = await request(app) + .get('/cubejs-api/v1/sql') + .set('Authorization', AUTH_TOKEN) + .expect(403); + expect(res.body && res.body.error) + .toStrictEqual('API scope is missing: sql'); + res = await request(app) .post('/cubejs-api/v1/pre-aggregations/jobs') .set('Authorization', AUTH_TOKEN) @@ -175,39 +182,47 @@ describe('Gateway Api Scopes', () => { expect(res3.body && res3.body.error) .toStrictEqual('API scope is missing: data'); - const res4 = await request(app) - .get('/cubejs-api/v1/sql') + const res6 = await request(app) + .get('/cubejs-api/v1/dry-run') .set('Authorization', AUTH_TOKEN) .expect(403); - expect(res4.body && res4.body.error) + expect(res6.body && res6.body.error) .toStrictEqual('API scope is missing: data'); - const res5 = await request(app) - .post('/cubejs-api/v1/sql') + const res7 = await request(app) + .post('/cubejs-api/v1/dry-run') .set('Content-type', 'application/json') .set('Authorization', AUTH_TOKEN) .expect(403); - expect(res5.body && res5.body.error) + expect(res7.body && res7.body.error) .toStrictEqual('API scope is missing: data'); - const res6 = await request(app) - .get('/cubejs-api/v1/dry-run') + apiGateway.release(); + }); + + test('Sql declined', async () => { + const { app, apiGateway } = createApiGateway({ + contextToApiScopes: async () => ['graphql', 'meta', 'jobs', 'data'], + }); + + const res1 = await request(app) + .get('/cubejs-api/v1/sql') .set('Authorization', AUTH_TOKEN) .expect(403); - expect(res6.body && res6.body.error) - .toStrictEqual('API scope is missing: data'); + expect(res1.body && res1.body.error) + .toStrictEqual('API scope is missing: sql'); - const res7 = await request(app) - .post('/cubejs-api/v1/dry-run') + const res2 = await request(app) + .post('/cubejs-api/v1/sql') .set('Content-type', 'application/json') .set('Authorization', AUTH_TOKEN) .expect(403); - expect(res7.body && res7.body.error) - .toStrictEqual('API scope is missing: data'); + expect(res2.body && res2.body.error) + .toStrictEqual('API scope is missing: sql'); apiGateway.release(); }); From 3efb6f69796d28c7a852fd8375dea85cb839c8ee Mon Sep 17 00:00:00 2001 From: Konstantin Burkalev Date: Fri, 28 Mar 2025 20:17:03 +0200 Subject: [PATCH 2/4] fix tests --- packages/cubejs-server-core/test/unit/OptsHandler.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cubejs-server-core/test/unit/OptsHandler.test.ts b/packages/cubejs-server-core/test/unit/OptsHandler.test.ts index 306b810192ec0..4db38e5979321 100644 --- a/packages/cubejs-server-core/test/unit/OptsHandler.test.ts +++ b/packages/cubejs-server-core/test/unit/OptsHandler.test.ts @@ -1140,7 +1140,7 @@ describe('OptsHandler class', () => { const permissions = await gateway.contextToApiScopesFn(); expect(permissions).toBeDefined(); expect(Array.isArray(permissions)).toBeTruthy(); - expect(permissions).toEqual(['graphql', 'meta', 'data']); + expect(permissions).toEqual(['graphql', 'meta', 'data', 'sql']); }); test('must set env api scopes if fn not specified', async () => { From dfd20445d3892b11736757cc84dab1dbb63313c8 Mon Sep 17 00:00:00 2001 From: Igor Lukanin Date: Sun, 30 Mar 2025 17:47:19 +0200 Subject: [PATCH 3/4] Update docs --- docs/pages/product/apis-integrations/rest-api.mdx | 1 + docs/pages/reference/configuration/config.mdx | 4 ++-- docs/pages/reference/configuration/environment-variables.mdx | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/pages/product/apis-integrations/rest-api.mdx b/docs/pages/product/apis-integrations/rest-api.mdx index 3ccf3d7267b12..29498b24966d5 100644 --- a/docs/pages/product/apis-integrations/rest-api.mdx +++ b/docs/pages/product/apis-integrations/rest-api.mdx @@ -129,6 +129,7 @@ accessible for everyone. | `meta` | [`/v1/meta`][ref-ref-meta] | ✅ Yes | | `data` | [`/v1/load`][ref-ref-load], [`/v1/sql`][ref-ref-sql] | ✅ Yes | | `graphql` | `/graphql` | ✅ Yes | +| `sql` | [`/v1/sql`][ref-ref-sql] | ✅ Yes | | `jobs` | [`/v1/pre-aggregations/jobs`][ref-ref-paj] | ❌ No | | No scope | `/livez`, `/readyz` | ✅ Yes, always | diff --git a/docs/pages/reference/configuration/config.mdx b/docs/pages/reference/configuration/config.mdx index 883aaf4013d61..9feaea5fa23af 100644 --- a/docs/pages/reference/configuration/config.mdx +++ b/docs/pages/reference/configuration/config.mdx @@ -1008,13 +1008,13 @@ from cube import config @config('context_to_api_scopes') def context_to_api_scopes(context: dict, default_scopes: list[str]) -> list[str]: - return ['meta', 'data', 'graphql'] + return ['meta', 'data', 'graphql', 'sql'] ``` ```javascript module.exports = { contextToApiScopes: (securityContext, defaultScopes) => { - return ['meta', 'data', 'graphql']; + return ['meta', 'data', 'graphql', 'sql']; }, }; ``` diff --git a/docs/pages/reference/configuration/environment-variables.mdx b/docs/pages/reference/configuration/environment-variables.mdx index 4e7d712972f1d..55e2d5327cc57 100644 --- a/docs/pages/reference/configuration/environment-variables.mdx +++ b/docs/pages/reference/configuration/environment-variables.mdx @@ -860,7 +860,7 @@ endpoints. | Possible Values | Default in Development | Default in Production | | ------------------------------------------------------------------------------ | ---------------------- | --------------------- | -| A comma-delimited string with any combination of [API scopes][ref-rest-scopes] | `meta,data,graphql` | `meta,data,graphql` | +| A comma-delimited string with any combination of [API scopes][ref-rest-scopes] | `meta,data,graphql,sql`| `meta,data,graphql,sql`| See also the [`context_to_api_scopes` configuration option](/reference/configuration/config#context_to_api_scopes). From afd787c416df143cb11efc035aef2365afa109b1 Mon Sep 17 00:00:00 2001 From: Igor Lukanin Date: Thu, 10 Apr 2025 22:17:53 +0200 Subject: [PATCH 4/4] Fix docs --- docs/pages/product/apis-integrations/rest-api.mdx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/pages/product/apis-integrations/rest-api.mdx b/docs/pages/product/apis-integrations/rest-api.mdx index 29498b24966d5..8f042b44792bc 100644 --- a/docs/pages/product/apis-integrations/rest-api.mdx +++ b/docs/pages/product/apis-integrations/rest-api.mdx @@ -124,14 +124,13 @@ by making them accessible to specific users only or disallowing access for everyone. By default, API endpoints in all scopes, except for `jobs`, are accessible for everyone. -| API scope | REST API endpoints | Accessible by default? | -| --------- | ----------------------------------------------------------------------------------------- | ---------------------- | -| `meta` | [`/v1/meta`][ref-ref-meta] | ✅ Yes | -| `data` | [`/v1/load`][ref-ref-load], [`/v1/sql`][ref-ref-sql] | ✅ Yes | -| `graphql` | `/graphql` | ✅ Yes | -| `sql` | [`/v1/sql`][ref-ref-sql] | ✅ Yes | -| `jobs` | [`/v1/pre-aggregations/jobs`][ref-ref-paj] | ❌ No | -| No scope | `/livez`, `/readyz` | ✅ Yes, always | +| API scope | REST API endpoints | Accessible by default? | +| --- | --- | --- | +| `meta` | [`/v1/meta`][ref-ref-meta] | ✅ Yes | +| `data` | [`/v1/load`][ref-ref-load] | ✅ Yes | +| `graphql` | `/graphql` | ✅ Yes | +| `jobs` | [`/v1/pre-aggregations/jobs`][ref-ref-paj] | ❌ No | +| No scope | `/livez`, `/readyz` | ✅ Yes | You can set accessible API scopes _for all requests_ using the `CUBEJS_DEFAULT_API_SCOPES` environment variable. For example, to disallow