diff --git a/docs/pages/product/apis-integrations/rest-api.mdx b/docs/pages/product/apis-integrations/rest-api.mdx index 3ccf3d7267b12..8f042b44792bc 100644 --- a/docs/pages/product/apis-integrations/rest-api.mdx +++ b/docs/pages/product/apis-integrations/rest-api.mdx @@ -124,13 +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 | -| `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 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). 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(); }); 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 () => {