diff --git a/docs-mintlify/reference/core-data-apis/rest-api/index.mdx b/docs-mintlify/reference/core-data-apis/rest-api/index.mdx index 70557d0c97f3e..af121c1331a41 100644 --- a/docs-mintlify/reference/core-data-apis/rest-api/index.mdx +++ b/docs-mintlify/reference/core-data-apis/rest-api/index.mdx @@ -185,7 +185,7 @@ accessible for everyone. | API scope | REST (JSON) API endpoints | Accessible by default? | | --- | --- | --- | -| `meta` | [`/v1/meta`][ref-ref-meta], [Metadata API][ref-ref-metadata] | ✅ Yes | +| `meta` | [`/v1/meta`][ref-ref-meta], [Metadata API][ref-ref-metadata], `/v1/graphql-to-json` | ✅ Yes | | `data` | [`/v1/load`][ref-ref-load], [`/v1/cubesql`][ref-ref-cubesql] | ✅ Yes | | `graphql` | `/graphql` | ✅ Yes | | `sql` | [`/v1/sql`][ref-ref-sql] | ✅ Yes | diff --git a/packages/cubejs-api-gateway/src/gateway.ts b/packages/cubejs-api-gateway/src/gateway.ts index cdac72992b27c..7753373b5900e 100644 --- a/packages/cubejs-api-gateway/src/gateway.ts +++ b/packages/cubejs-api-gateway/src/gateway.ts @@ -336,32 +336,6 @@ class ApiGateway { * graphql scope * *************************************************************** */ - app.post(`${this.basePath}/v1/graphql-to-json`, userMiddlewares, async (req: any, res) => { - const { query, variables } = req.body; - const compilerApi = await this.getCompilerApi(req.context); - - const metaConfig = await compilerApi.metaConfig(req.context, { - requestId: req.context.requestId, - }); - - let schema = compilerApi.getGraphQLSchema(); - if (!schema) { - schema = makeSchema(metaConfig); - compilerApi.setGraphQLSchema(schema); - } - - try { - const jsonQuery = getJsonQueryFromGraphQLQuery(query, metaConfig, variables); - res.json({ jsonQuery }); - } catch (e: any) { - const stack = getEnv('devMode') ? e.stack : undefined; - this.logger('GraphQL to JSON error', { - error: (stack || e).toString(), - }); - res.json({ jsonQuery: null }); - } - }); - app.use( `${this.basePath}/graphql`, userMiddlewares, @@ -541,6 +515,33 @@ class ApiGateway { }) ); + // Named for GraphQL but guarded by `meta`: it only reads the data model + // metadata to translate a query string, and executes nothing. + app.post(`${this.basePath}/v1/graphql-to-json`, jsonParser, userMiddlewares, userAsyncHandler(async (req: any, res) => { + await this.assertApiScope( + 'meta', + req?.context?.securityContext + ); + + const { query, variables } = req.body; + const compilerApi = await this.getCompilerApi(req.context); + + const metaConfig = await compilerApi.metaConfig(req.context, { + requestId: req.context.requestId, + }); + + try { + const jsonQuery = getJsonQueryFromGraphQLQuery(query, metaConfig, variables); + res.json({ jsonQuery }); + } catch (e: any) { + const stack = getEnv('devMode') ? e.stack : undefined; + this.logger('GraphQL to JSON error', { + error: (stack || e).toString(), + }); + res.json({ jsonQuery: null }); + } + })); + app.post( `${this.basePath}/v1/cubesql`, userMiddlewares, diff --git a/packages/cubejs-api-gateway/test/permissions.test.ts b/packages/cubejs-api-gateway/test/permissions.test.ts index b1a850f15437c..07e978068ce77 100644 --- a/packages/cubejs-api-gateway/test/permissions.test.ts +++ b/packages/cubejs-api-gateway/test/permissions.test.ts @@ -48,6 +48,15 @@ describe('Gateway Api Scopes', () => { expect(res.body && res.body.error) .toStrictEqual('API scope is missing: graphql'); + res = await request(app) + .post('/cubejs-api/v1/graphql-to-json') + .set('Content-type', 'application/json') + .set('Authorization', AUTH_TOKEN) + .send({ query: 'query { cube { Foo { bar } } }' }) + .expect(403); + expect(res.body && res.body.error) + .toStrictEqual('API scope is missing: meta'); + res = await request(app) .get('/cubejs-api/v1/meta') .set('Authorization', AUTH_TOKEN) @@ -114,6 +123,44 @@ describe('Gateway Api Scopes', () => { apiGateway.release(); }); + // `/v1/graphql-to-json` only reads the data model metadata, so it is guarded + // by the `meta` scope - not `graphql`, which gates the GraphQL API itself. + test('GraphQL to JSON declined without meta scope', async () => { + const { app, apiGateway } = createApiGateway({ + contextToApiScopes: async () => ['graphql', 'data', 'jobs'], + }); + + const res = await request(app) + .post('/cubejs-api/v1/graphql-to-json') + .set('Content-type', 'application/json') + .set('Authorization', AUTH_TOKEN) + .send({ query: 'query { cube { Foo { bar } } }' }) + .expect(403); + + expect(res.body && res.body.error) + .toStrictEqual('API scope is missing: meta'); + + apiGateway.release(); + }); + + test('GraphQL to JSON allowed with meta scope but no graphql scope', async () => { + const { app, apiGateway } = createApiGateway({ + contextToApiScopes: async () => ['meta', 'data', 'jobs'], + }); + + const res = await request(app) + .post('/cubejs-api/v1/graphql-to-json') + .set('Content-type', 'application/json') + .set('Authorization', AUTH_TOKEN) + .send({ query: 'query { cube { Foo { bar } } }' }) + .expect(200); + + expect(res.body && res.body.jsonQuery) + .toStrictEqual({ measures: ['Foo.bar'] }); + + apiGateway.release(); + }); + test('Meta declined', async () => { const { app, apiGateway } = createApiGateway({ contextToApiScopes: async () => ['graphql', 'data', 'jobs'], @@ -135,6 +182,16 @@ describe('Gateway Api Scopes', () => { expect(res2.body && res2.body.error) .toStrictEqual('API scope is missing: meta'); + const res3 = await request(app) + .post('/cubejs-api/v1/graphql-to-json') + .set('Content-type', 'application/json') + .set('Authorization', AUTH_TOKEN) + .send({ query: 'query { cube { Foo { bar } } }' }) + .expect(403); + + expect(res3.body && res3.body.error) + .toStrictEqual('API scope is missing: meta'); + apiGateway.release(); });