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..9ac0e493d8 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 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. 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 31463cc09b..1d21892742 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..3ea60210f3 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,7 @@ async function setupPgClientTransaction ({ audience: jwtAudiences, }) - const roleClaim = jwtClaims['role'] + const roleClaim = getPath(jwtClaims, jwtRole) // If there is a `role` property in the claims, use that instead of our // default role. @@ -227,4 +232,21 @@ function debugPgClient (pgClient: Client): Client { return pgClient } + +/** + * Safely gets the value at `path` (array of keys) of `inObject`. + * + * @private + */ +function getPath(inObject: mixed, path: Array): 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