From dd36d4e5c85887add9c8678d8d5e6f9d2cd0f571 Mon Sep 17 00:00:00 2001 From: Angelo Sarto Date: Wed, 31 May 2017 08:46:48 -0500 Subject: [PATCH 1/6] feat(postgraphql) Added an option to allow the pgRole to be extracted from any location in the JWT, defaults to 'role' on the root of the JWT object --- docs/cli.md | 1 + docs/library.md | 1 + .../__tests__/withPostGraphQLContext-test.js | 45 +++++++++++++++++++ src/postgraphql/cli.ts | 3 ++ .../createPostGraphQLHttpRequestHandler.js | 1 + src/postgraphql/postgraphql.ts | 1 + src/postgraphql/withPostGraphQLContext.ts | 16 ++++++- 7 files changed, 67 insertions(+), 1 deletion(-) diff --git a/docs/cli.md b/docs/cli.md index 90e5bd6027..e3c267b9af 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -41,6 +41,7 @@ The usage of the `postgraphql` binary is as follows. To pull up this documentati --secret DEPRECATED: Use jwt-secret instead -e, --jwt-secret the secret to be used when creating and verifying JWTs. if none is provided auth will be disabled -A, --jwt-audience a comma separated list of audiences your jwt token can contain. If no audience is given the audience defaults to `postgraphql` + --jwt-role a comma seperated list of strings that create a path in the jwt from which to extract the postgres role. if none is provided it will use the key `role` on the root of the jwt. --export-schema-json [path] enables exporting the detected schema, in JSON format, to the given location. The directories must exist already, if the file exists it will be overwritten. --export-schema-graphql [path] enables exporting the detected schema, in GraphQL schema format, to the given location. The directories must exist already, if the file exists it will be overwritten. --show-error-stack [setting] show JavaScript error stacks in the GraphQL result errors diff --git a/docs/library.md b/docs/library.md index b859e1ad44..4e18d799ef 100644 --- a/docs/library.md +++ b/docs/library.md @@ -59,6 +59,7 @@ Arguments include: - `pgDefaultRole`: The default Postgres role to use. If no role was provided in a provided JWT token, this role will be used. - `jwtSecret`: The secret for your JSON web tokens. This will be used to verify tokens in the `Authorization` header, and signing JWT tokens you return in procedures. - `jwtAudiences`: The audiences to use when verifing the JWT token. If not set the audience will be `['postgraphql']`. + - `jwtRole`: a comma seperated list of strings that create a path in the jwt from which to extract the postgres role. if none is provided it will use the key `role` on the root of the jwt. - `jwtPgTypeIdentifier`: The Postgres type identifier for the compound type which will be signed as a JWT token if ever found as the return type of a procedure. Can be of the form: `my_schema.my_type`. You may use quotes as needed: `"my-special-schema".my_type`. - `watchPg`: When true, PostGraphQL will watch your database schemas and re-create the GraphQL API whenever your schema changes, notifying you as it does. This feature requires an event trigger to be added to the database by a superuser. When enabled PostGraphQL will try to add this trigger, if you did not connect as a superuser you will get a warning and the trigger won’t be added. - `disableQueryLog`: Turns off GraphQL query logging. By default PostGraphQL will log every GraphQL query it processes along with some other information. Set this to `true` to disable that feature. diff --git a/src/postgraphql/__tests__/withPostGraphQLContext-test.js b/src/postgraphql/__tests__/withPostGraphQLContext-test.js index d2c3c4bfba..916ba69b10 100644 --- a/src/postgraphql/__tests__/withPostGraphQLContext-test.js +++ b/src/postgraphql/__tests__/withPostGraphQLContext-test.js @@ -239,3 +239,48 @@ test('will set a role provided in the JWT superceding the default role', async ( ], }], ['commit']]) }) + +test('will set a role provided in the JWT', async () => { + const pgClient = { query: jest.fn(), release: jest.fn() } + const pgPool = { connect: jest.fn(() => pgClient) } + await withPostGraphQLContext({ + pgPool, + jwtToken: jwt.sign({ aud: 'postgraphql', a: 1, b: 2, c: 3, some: {other: {path: 'test_deep_role' }}}, 'secret', { noTimestamp: true }), + jwtSecret: 'secret', + jwtRole: ['some', 'other', 'path'], + }, () => {}) + expect(pgClient.query.mock.calls).toEqual([['begin'], [{ + text: 'select set_config($1, $2, true), set_config($3, $4, true), set_config($5, $6, true), set_config($7, $8, true), set_config($9, $10, true), set_config($11, $12, true)', + values: [ + 'role', 'test_deep_role', + 'jwt.claims.aud', 'postgraphql', + 'jwt.claims.a', 1, + 'jwt.claims.b', 2, + 'jwt.claims.c', 3, + 'jwt.claims.some', {'other': {'path': 'test_deep_role'}}, + ], + }], ['commit']]) +}) + +test('will set a role provided in the JWT superceding the default role', async () => { + const pgClient = { query: jest.fn(), release: jest.fn() } + const pgPool = { connect: jest.fn(() => pgClient) } + await withPostGraphQLContext({ + pgPool, + jwtToken: jwt.sign({ aud: 'postgraphql', a: 1, b: 2, c: 3, some: {other: {path: 'test_deep_role' }}}, 'secret', { noTimestamp: true }), + jwtSecret: 'secret', + jwtRole: ['some', 'other', 'path'], + pgDefaultRole: 'test_default_role', + }, () => {}) + expect(pgClient.query.mock.calls).toEqual([['begin'], [{ + text: 'select set_config($1, $2, true), set_config($3, $4, true), set_config($5, $6, true), set_config($7, $8, true), set_config($9, $10, true), set_config($11, $12, true)', + values: [ + 'role', 'test_deep_role', + 'jwt.claims.aud', 'postgraphql', + 'jwt.claims.a', 1, + 'jwt.claims.b', 2, + 'jwt.claims.c', 3, + 'jwt.claims.some', {'other': {'path': 'test_deep_role'}}, + ], + }], ['commit']]) +}) diff --git a/src/postgraphql/cli.ts b/src/postgraphql/cli.ts index 70d7ccbccf..9fe5156799 100755 --- a/src/postgraphql/cli.ts +++ b/src/postgraphql/cli.ts @@ -39,6 +39,7 @@ program .option('--secret ', 'DEPRECATED: Use jwt-secret instead') .option('-e, --jwt-secret ', 'the secret to be used when creating and verifying JWTs. if none is provided auth will be disabled') .option('-A, --jwt-audiences ', 'a comma separated list of audiences your jwt token can contain. If no audience is given the audience defaults to `postgraphql`', (option: string) => option.split(',')) + .option('--jwt-role ', 'a comma seperated list of strings that create a path in the jwt from which to extract the postgres role. if none is provided it will use the key `role` on the root of the jwt.', (option: string) => option.split(',')) .option('--export-schema-json [path]', 'enables exporting the detected schema, in JSON format, to the given location. The directories must exist already, if the file exists it will be overwritten.') .option('--export-schema-graphql [path]', 'enables exporting the detected schema, in GraphQL schema format, to the given location. The directories must exist already, if the file exists it will be overwritten.') .option('--show-error-stack [setting]', 'show JavaScript error stacks in the GraphQL result errors') @@ -71,6 +72,7 @@ const { secret: deprecatedJwtSecret, jwtSecret, jwtAudiences = ['postgraphql'], + jwtRole = ['role'], token: jwtPgTypeIdentifier, cors: enableCors = false, classicIds = false, @@ -116,6 +118,7 @@ const server = createServer(postgraphql(pgConfig, schemas, { jwtPgTypeIdentifier, jwtSecret: jwtSecret || deprecatedJwtSecret, jwtAudiences, + jwtRole, pgDefaultRole, watchPg, showErrorStack, diff --git a/src/postgraphql/http/createPostGraphQLHttpRequestHandler.js b/src/postgraphql/http/createPostGraphQLHttpRequestHandler.js index 97b0e8aabd..7fcff24c6b 100644 --- a/src/postgraphql/http/createPostGraphQLHttpRequestHandler.js +++ b/src/postgraphql/http/createPostGraphQLHttpRequestHandler.js @@ -395,6 +395,7 @@ export default function createPostGraphQLHttpRequestHandler (options) { jwtToken, jwtSecret: options.jwtSecret, jwtAudiences: options.jwtAudiences, + jwtRole: options.jwtRole, pgDefaultRole: options.pgDefaultRole, pgSettings, }, context => { diff --git a/src/postgraphql/postgraphql.ts b/src/postgraphql/postgraphql.ts index a026dcf8ee..dce7ad3c7b 100644 --- a/src/postgraphql/postgraphql.ts +++ b/src/postgraphql/postgraphql.ts @@ -17,6 +17,7 @@ type PostGraphQLOptions = { pgDefaultRole?: string, jwtSecret?: string, jwtAudiences?: Array, + jwtRole?: Array, jwtPgTypeIdentifier?: string, watchPg?: boolean, showErrorStack?: boolean, diff --git a/src/postgraphql/withPostGraphQLContext.ts b/src/postgraphql/withPostGraphQLContext.ts index be0f70ac1d..0c0046bd4a 100644 --- a/src/postgraphql/withPostGraphQLContext.ts +++ b/src/postgraphql/withPostGraphQLContext.ts @@ -36,6 +36,7 @@ export default async function withPostGraphQLContext( jwtToken, jwtSecret, jwtAudiences = ['postgraphql'], + jwtRole = ['role'], pgDefaultRole, pgSettings, }: { @@ -43,6 +44,7 @@ export default async function withPostGraphQLContext( jwtToken?: string, jwtSecret?: string, jwtAudiences?: Array, + jwtRole?: Array, pgDefaultRole?: string, pgSettings?: { [key: string]: mixed }, }, @@ -64,6 +66,7 @@ export default async function withPostGraphQLContext( jwtToken, jwtSecret, jwtAudiences, + jwtRole, pgDefaultRole, pgSettings, }) @@ -95,6 +98,7 @@ async function setupPgClientTransaction ({ jwtToken, jwtSecret, jwtAudiences, + jwtRole, pgDefaultRole, pgSettings, }: { @@ -102,6 +106,7 @@ async function setupPgClientTransaction ({ jwtToken?: string, jwtSecret?: string, jwtAudiences?: Array, + jwtRole?: Array, pgDefaultRole?: string, pgSettings?: { [key: string]: mixed }, }): Promise { @@ -124,7 +129,16 @@ async function setupPgClientTransaction ({ audience: jwtAudiences, }) - const roleClaim = jwtClaims['role'] + let roleClaim = jwtClaims ? jwtClaims : '' + const rolePath = jwtRole ? jwtRole : [] + for (let i = 0; i < rolePath.length; i++) { + try { + roleClaim = roleClaim[rolePath[i]] + } catch (e) { + i = rolePath.length + roleClaim = '' + } + } // If there is a `role` property in the claims, use that instead of our // default role. From 78d3f8c45da6048f956355ae37f9f4430edbafc4 Mon Sep 17 00:00:00 2001 From: Angelo Sarto Date: Thu, 8 Jun 2017 10:24:25 -0500 Subject: [PATCH 2/6] feat(postgraphql) Added an option to allow the pgRole to be extracted from any location in the JWT, defaults to 'role' on the root of the JWT object --- src/postgraphql/withPostGraphQLContext.ts | 29 +++++++++++++++-------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/postgraphql/withPostGraphQLContext.ts b/src/postgraphql/withPostGraphQLContext.ts index 0c0046bd4a..aa14801e42 100644 --- a/src/postgraphql/withPostGraphQLContext.ts +++ b/src/postgraphql/withPostGraphQLContext.ts @@ -129,16 +129,7 @@ async function setupPgClientTransaction ({ audience: jwtAudiences, }) - let roleClaim = jwtClaims ? jwtClaims : '' - const rolePath = jwtRole ? jwtRole : [] - for (let i = 0; i < rolePath.length; i++) { - try { - roleClaim = roleClaim[rolePath[i]] - } catch (e) { - i = rolePath.length - roleClaim = '' - } - } + const roleClaim = getPath(jwtClaims, jwtRole); // If there is a `role` property in the claims, use that instead of our // default role. @@ -241,4 +232,22 @@ function debugPgClient (pgClient: Client): Client { return pgClient } + +/** + * Safely extract a nested object or undefined where inObject is any object and path is + * an array of indexes into an object + * + * @private + */ +function getPath(inObject: any, path: any): any { + let object = inObject + // From https://github.com/lodash/lodash/blob/master/.internal/baseGet.js + let index = 0 + const length = path.length + + while (object && index < length) { + object = object[path[index++]] + } + return (index && index === length) ? object : undefined +} // tslint:enable no-any From fb7a845ba95f46551968ec2562bca03c6675924c Mon Sep 17 00:00:00 2001 From: Angelo Sarto Date: Thu, 8 Jun 2017 13:42:16 -0500 Subject: [PATCH 3/6] feat(postgraphql) Added an option to allow the pgRole to be extracted from any location in the JWT, defaults to 'role' on the root of the JWT object --- src/postgraphql/withPostGraphQLContext.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/postgraphql/withPostGraphQLContext.ts b/src/postgraphql/withPostGraphQLContext.ts index aa14801e42..c8508c9786 100644 --- a/src/postgraphql/withPostGraphQLContext.ts +++ b/src/postgraphql/withPostGraphQLContext.ts @@ -129,7 +129,7 @@ async function setupPgClientTransaction ({ audience: jwtAudiences, }) - const roleClaim = getPath(jwtClaims, jwtRole); + const roleClaim = getPath(jwtClaims, jwtRole) // If there is a `role` property in the claims, use that instead of our // default role. From 2dab36179b5ca708b9598ec6b7de115d18574530 Mon Sep 17 00:00:00 2001 From: Angelo Sarto Date: Thu, 8 Jun 2017 14:12:06 -0500 Subject: [PATCH 4/6] feat(postgraphql) Added an option to allow the pgRole to be extracted from any location in the JWT, defaults to 'role' on the root of the JWT object --- src/postgraphql/withPostGraphQLContext.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/postgraphql/withPostGraphQLContext.ts b/src/postgraphql/withPostGraphQLContext.ts index c8508c9786..3ea60210f3 100644 --- a/src/postgraphql/withPostGraphQLContext.ts +++ b/src/postgraphql/withPostGraphQLContext.ts @@ -44,7 +44,7 @@ export default async function withPostGraphQLContext( jwtToken?: string, jwtSecret?: string, jwtAudiences?: Array, - jwtRole?: Array, + jwtRole: Array, pgDefaultRole?: string, pgSettings?: { [key: string]: mixed }, }, @@ -106,7 +106,7 @@ async function setupPgClientTransaction ({ jwtToken?: string, jwtSecret?: string, jwtAudiences?: Array, - jwtRole?: Array, + jwtRole: Array, pgDefaultRole?: string, pgSettings?: { [key: string]: mixed }, }): Promise { @@ -234,12 +234,11 @@ function debugPgClient (pgClient: Client): Client { } /** - * Safely extract a nested object or undefined where inObject is any object and path is - * an array of indexes into an object + * Safely gets the value at `path` (array of keys) of `inObject`. * * @private */ -function getPath(inObject: any, path: any): any { +function getPath(inObject: mixed, path: Array): any { let object = inObject // From https://github.com/lodash/lodash/blob/master/.internal/baseGet.js let index = 0 From d6993c34a6815b6533e7aad5ca7a143237570c39 Mon Sep 17 00:00:00 2001 From: Angelo Sarto Date: Thu, 8 Jun 2017 14:13:16 -0500 Subject: [PATCH 5/6] feat(postgraphql) Added an option to allow the pgRole to be extracted from any location in the JWT, defaults to 'role' on the root of the JWT object --- docs/library.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/library.md b/docs/library.md index 4e18d799ef..dad5e25ac1 100644 --- a/docs/library.md +++ b/docs/library.md @@ -59,7 +59,7 @@ Arguments include: - `pgDefaultRole`: The default Postgres role to use. If no role was provided in a provided JWT token, this role will be used. - `jwtSecret`: The secret for your JSON web tokens. This will be used to verify tokens in the `Authorization` header, and signing JWT tokens you return in procedures. - `jwtAudiences`: The audiences to use when verifing the JWT token. If not set the audience will be `['postgraphql']`. - - `jwtRole`: a comma seperated list of strings that create a path in the jwt from which to extract the postgres role. if none is provided it will use the key `role` on the root of the jwt. + - `jwtRole`: a comma seperated list of strings that create a path in the jwt from which to extract the postgres role. If none is provided it will use the key `role` on the root of the jwt. - `jwtPgTypeIdentifier`: The Postgres type identifier for the compound type which will be signed as a JWT token if ever found as the return type of a procedure. Can be of the form: `my_schema.my_type`. You may use quotes as needed: `"my-special-schema".my_type`. - `watchPg`: When true, PostGraphQL will watch your database schemas and re-create the GraphQL API whenever your schema changes, notifying you as it does. This feature requires an event trigger to be added to the database by a superuser. When enabled PostGraphQL will try to add this trigger, if you did not connect as a superuser you will get a warning and the trigger won’t be added. - `disableQueryLog`: Turns off GraphQL query logging. By default PostGraphQL will log every GraphQL query it processes along with some other information. Set this to `true` to disable that feature. From 7f446d12d81fddc2badb988d53ca9500a1b32d04 Mon Sep 17 00:00:00 2001 From: Angelo Sarto Date: Thu, 8 Jun 2017 14:26:30 -0500 Subject: [PATCH 6/6] feat(postgraphql) Added an option to allow the pgRole to be extracted from any location in the JWT, defaults to 'role' on the root of the JWT object --- docs/library.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/library.md b/docs/library.md index dad5e25ac1..9ac0e493d8 100644 --- a/docs/library.md +++ b/docs/library.md @@ -59,7 +59,7 @@ Arguments include: - `pgDefaultRole`: The default Postgres role to use. If no role was provided in a provided JWT token, this role will be used. - `jwtSecret`: The secret for your JSON web tokens. This will be used to verify tokens in the `Authorization` header, and signing JWT tokens you return in procedures. - `jwtAudiences`: The audiences to use when verifing the JWT token. If not set the audience will be `['postgraphql']`. - - `jwtRole`: a comma seperated list of strings that create a path in the jwt from which to extract the postgres role. If none is provided it will use the key `role` on the root of the jwt. + - `jwtRole`: A comma separated list of strings that give a path in the jwt from which to extract the postgres role. If none is provided it will use the key `role` on the root of the jwt. - `jwtPgTypeIdentifier`: The Postgres type identifier for the compound type which will be signed as a JWT token if ever found as the return type of a procedure. Can be of the form: `my_schema.my_type`. You may use quotes as needed: `"my-special-schema".my_type`. - `watchPg`: When true, PostGraphQL will watch your database schemas and re-create the GraphQL API whenever your schema changes, notifying you as it does. This feature requires an event trigger to be added to the database by a superuser. When enabled PostGraphQL will try to add this trigger, if you did not connect as a superuser you will get a warning and the trigger won’t be added. - `disableQueryLog`: Turns off GraphQL query logging. By default PostGraphQL will log every GraphQL query it processes along with some other information. Set this to `true` to disable that feature.