Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(postgraphql): extract pgRole from arbitrary JWT path #480

Merged
merged 9 commits into from
Jun 8, 2017
1 change: 1 addition & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ The usage of the `postgraphql` binary is as follows. To pull up this documentati
--secret <string> DEPRECATED: Use jwt-secret instead
-e, --jwt-secret <string> the secret to be used when creating and verifying JWTs. if none is provided auth will be disabled
-A, --jwt-audience <string> a comma separated list of audiences your jwt token can contain. If no audience is given the audience defaults to `postgraphql`
--jwt-role <string> 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
Expand Down
1 change: 1 addition & 0 deletions docs/library.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

@benjie benjie Jun 8, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if none is provided < missing capital following fullstop

- `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.
Expand Down
45 changes: 45 additions & 0 deletions src/postgraphql/__tests__/withPostGraphQLContext-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']])
})
3 changes: 3 additions & 0 deletions src/postgraphql/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ program
.option('--secret <string>', 'DEPRECATED: Use jwt-secret instead')
.option('-e, --jwt-secret <string>', 'the secret to be used when creating and verifying JWTs. if none is provided auth will be disabled')
.option('-A, --jwt-audiences <string>', '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 <string>', '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')
Expand Down Expand Up @@ -71,6 +72,7 @@ const {
secret: deprecatedJwtSecret,
jwtSecret,
jwtAudiences = ['postgraphql'],
jwtRole = ['role'],
token: jwtPgTypeIdentifier,
cors: enableCors = false,
classicIds = false,
Expand Down Expand Up @@ -116,6 +118,7 @@ const server = createServer(postgraphql(pgConfig, schemas, {
jwtPgTypeIdentifier,
jwtSecret: jwtSecret || deprecatedJwtSecret,
jwtAudiences,
jwtRole,
pgDefaultRole,
watchPg,
showErrorStack,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ export default function createPostGraphQLHttpRequestHandler (options) {
jwtToken,
jwtSecret: options.jwtSecret,
jwtAudiences: options.jwtAudiences,
jwtRole: options.jwtRole,
pgDefaultRole: options.pgDefaultRole,
pgSettings,
}, context => {
Expand Down
1 change: 1 addition & 0 deletions src/postgraphql/postgraphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type PostGraphQLOptions = {
pgDefaultRole?: string,
jwtSecret?: string,
jwtAudiences?: Array<string>,
jwtRole?: Array<string>,
jwtPgTypeIdentifier?: string,
watchPg?: boolean,
showErrorStack?: boolean,
Expand Down
25 changes: 24 additions & 1 deletion src/postgraphql/withPostGraphQLContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ export default async function withPostGraphQLContext(
jwtToken,
jwtSecret,
jwtAudiences = ['postgraphql'],
jwtRole = ['role'],
pgDefaultRole,
pgSettings,
}: {
pgPool: Pool,
jwtToken?: string,
jwtSecret?: string,
jwtAudiences?: Array<string>,
jwtRole?: Array<string>,
pgDefaultRole?: string,
pgSettings?: { [key: string]: mixed },
},
Expand All @@ -64,6 +66,7 @@ export default async function withPostGraphQLContext(
jwtToken,
jwtSecret,
jwtAudiences,
jwtRole,
pgDefaultRole,
pgSettings,
})
Expand Down Expand Up @@ -95,13 +98,15 @@ async function setupPgClientTransaction ({
jwtToken,
jwtSecret,
jwtAudiences,
jwtRole,
pgDefaultRole,
pgSettings,
}: {
pgClient: Client,
jwtToken?: string,
jwtSecret?: string,
jwtAudiences?: Array<string>,
jwtRole?: Array<string>,
pgDefaultRole?: string,
pgSettings?: { [key: string]: mixed },
}): Promise<string | undefined> {
Expand All @@ -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.
Expand Down Expand Up @@ -227,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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Safely gets the value at `path` (array of keys) of `inObject`.

*
* @private
*/
function getPath(inObject: any, path: any): any {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe something like (inObject: mixed, path: Array<string>)?

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