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: Add dynamic retrieval for client password #1926

Merged
merged 9 commits into from Jul 25, 2019

Conversation

sehrope
Copy link
Contributor

@sehrope sehrope commented Jul 17, 2019

PR to add support for dynamic password retrieval. There was already something quite similar to this to support reading passwords from a PGPASS file. This PR expands that section a bit to support invoking an arbitrary function.

Rather than supporting a callback parameter, this PR allows for either a synchronous function that returns a string or one that returns a Promise that resolves to a string. As there's Promise support on every supported runtime I figure it's a better choice for anything new being added.

Here's some examples of how to use it:

// Existing hard coded password:
const pool = new pg.Pool({
    password: 'test',
});

// Sync return a static string
const pool = new pg.Pool({
    password: () => {
        return 'test';
    }
});

// Async API
const pool = new pg.Pool({
    password: async () => {
        await new Promise(resolve => setTimeout(resolve, 1000));
        const value = await someAsyncFunc();
        return value;
    }
});

This PR does not have any new tests yet, I just tested it with some local examples. If the concept itself seems workable then should be easy to add a few that use a function and check for its side effects. Let me know.

Adds option to specify a function for a client password. When the client
is connected, if the value of password is a function then it is invoked
to get the password to use for that connection.

The function must return either a string or a Promise that resolves to
a string. If the function throws or rejects with an error then it will
be bubbled up to the client.
Add testAsync() helper function to Suite to simplify running tests that
return a Promise. The test action is executed and if a syncronous error
is thrown then it is immediately considered failed. If the Promise resolves
successfully then the test is considered successful. If the Promise
rejects with an Error then the test is considered failed.
@sehrope
Copy link
Contributor Author

sehrope commented Jul 18, 2019

I force pushed an update to this that changes:

  • Explicitly sets the password field to null if the password function does not return a usable value
  • Changes the error for a non-string return value to be a TypeError

Also added some tests for retrieving a sync, async, and errant passwords via functions. For the tests I added a new helper suite.testAsync(...) to simplify writing them using promises.

Copy link
Owner

@brianc brianc left a comment

Choose a reason for hiding this comment

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

This is fantastic. 20 line code change, 150 lines of tests. 😍

lib/client.js Outdated
if (self.password !== null) {
if (typeof(self.password) === 'function') {
try {
self._Promise.resolve(self.password())
Copy link
Owner

Choose a reason for hiding this comment

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

ohhh nice use on supporting BYOP - not sure how popular BYOP is now that promises have shipped natively in node for years, but still nice for backwards compatibility 👌

lib/client.js Outdated
})
} catch (err) {
con.emit('error', err)
}
Copy link
Owner

Choose a reason for hiding this comment

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

wow - impressive error handling

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Apparently not as impressive as @charmander ;-)

@@ -72,6 +72,19 @@ class Suite {
const test = new Test(name, cb)
this._queue.push(test)
}

testAsync (name, action) {
Copy link
Owner

Choose a reason for hiding this comment

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

i apologize the tests here aren't written w/ a modern node testing framework. There's this cost-benefit thing I keep running into where it's like "Should I re-write all these tests into jest/mocha/etc? They already work and I know they work...but maybe rewriting will introduce some breakage." The original tests in the repo were written before mocha or jest existed so it's kinda like...well....if it ain't broke...otoh I feel like it's probably more confusing to contribute to. I also at the time, coming from ruby, thought i'd be 'clever' and attach a bunch of stuff to the global namespace like assert and other things. That was a mistake. :(

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No worries. I've thought about cleaning up some of them more than once and it's always the same conclusion. Maybe having a separate harness for things going forward as it's definitely easier to write async tests with an async harness. Doesn't even have to be be full on mocha/jest/whatever ... just having something to call out to with async actions should be enough.

I like how you can easily run any one of the tests in this project via "node path/to/test.js".

Copy link
Collaborator

@charmander charmander left a comment

Choose a reason for hiding this comment

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

Some formatting consistency options.

lib/client.js Outdated Show resolved Hide resolved
lib/client.js Outdated Show resolved Hide resolved
lib/client.js Outdated Show resolved Hide resolved
test/integration/connection/dynamic-password.js Outdated Show resolved Hide resolved
const helper = require('./../test-helper')
const suite = new helper.Suite()
const pg = require('../../../lib/index');
const Client = pg.Client;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
const Client = pg.Client;
const Client = pg.Client

Copy link
Collaborator

Choose a reason for hiding this comment

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

(This one still exists)

Copy link
Owner

Choose a reason for hiding this comment

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

yeah i need to turn on linting on the test folder

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Would also help if we turned back on the semi rule. This PR passed "make lint" as the semi rule got removed when the eslint config was last cleaned up (by me...).

test/integration/connection/dynamic-password.js Outdated Show resolved Hide resolved
test/integration/connection/dynamic-password.js Outdated Show resolved Hide resolved
test/suite.js Outdated Show resolved Hide resolved
@sehrope
Copy link
Contributor Author

sehrope commented Jul 19, 2019

I pushed some updates to address the comments (thanks @charmander!). As part of this I also cleaned up the error handling in client.js to use the same style of Promise.resove() chaining as it removes the need for separate sync error handling.

I left the error handling for that piece on L131 of client.js as a separate .catch(...) to handle anything being thrown by the supplied cb rather than a single .then(onSuccess, onError). Anything that errors out would then bubble up as an error event on the client.

FYI, taking a peek at the callers of that checkPgPass(...) function, I think I may have found a potential future issue with the SCRAM auth, I'm going to open a separate issue to track it.

lib/client.js Outdated Show resolved Hide resolved
const helper = require('./../test-helper')
const suite = new helper.Suite()
const pg = require('../../../lib/index');
const Client = pg.Client;
Copy link
Collaborator

Choose a reason for hiding this comment

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

(This one still exists)

@sehrope
Copy link
Contributor Author

sehrope commented Jul 19, 2019

No clue why it failed on node v12 in the PR build but passed for me (https://travis-ci.org/sehrope/node-postgres/builds/561008527). There's no specific error in the travis logs either. I think there was some transient error building Postgres.

I'm going to push a dummy commit to see if it happens again.

Copy link
Collaborator

@charmander charmander left a comment

Choose a reason for hiding this comment

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

Nice :)

@alekstr
Copy link

alekstr commented Jul 23, 2019

Btw, checked and tested this change in Prod with RDS Postgres 11.4, works fine! When you plan merge this into master?

@brianc
Copy link
Owner

brianc commented Jul 25, 2019

Right now! & I'll document it and release a new minor version. 😄

@brianc brianc merged commit 0894a3c into brianc:master Jul 25, 2019
@brianc
Copy link
Owner

brianc commented Jul 25, 2019

Published pg@7.12.0 - thanks!

@benkeil
Copy link

benkeil commented Oct 30, 2020

You missed the types...

@sehrope
Copy link
Contributor Author

sehrope commented Oct 30, 2020

@benkeil Yes the typings are maintained externally in DefinitelyTyped (i.e. @types/pg) so it can't be updated in tandem. I submitted a PR for that so hopefully that'll get synced soon: DefinitelyTyped/DefinitelyTyped#49257

@benkeil
Copy link

benkeil commented Oct 30, 2020

Naive question: why not migrate the project to typescript?

@sehrope
Copy link
Contributor Author

sehrope commented Oct 31, 2020

I think it will eventually and some of the internal components have already been migrated to Typescript (e.g. see the packet parser).

@brianc
Copy link
Owner

brianc commented Nov 2, 2020

Naive question: why not migrate the project to typescript?

yeah i'm very slowly working on that

@vitaly-t
Copy link
Contributor

vitaly-t commented Apr 8, 2021

This PR and the change are confusing. I thought at first this would actually allow dynamic passwords support, but all it does - makes only one call in the beginning. After that, if the password changes, the callback is never called again. So it is not very useful then.

I had to use a connection object, and then do pool.options.password = 'new-password', or if connection string used, then set new one for pool.options.connectionString, in order to start using a new password.

@vitaly-t vitaly-t mentioned this pull request Apr 8, 2021
charmander pushed a commit to brianc/node-postgres-docs that referenced this pull request Apr 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants