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
Introduce a plugin manager interface to allow different mechanisms to acquire a password. #2321
Conversation
… acquire a password.
+1 to the overall idea. We should do this in a way that does not require even a dummy password to be supplied. Skimming through this patch, it looks like it'd fail prior to invoking the handler if a value is not provided: if (password == null) {
throw new PSQLException(
GT.tr(
"The server requested password-based authentication, but no password was provided."),
PSQLState.CONNECTION_REJECTED);
}
byte[] encodedPassword = authenticationPlugin.getEncodedPassword(user, password); Do we also want to be able to dynamically generate the username or other properties? How about providing additional connection properties to the supplier method so that it has more context for the request? One requested use case for this feature is RDS's IAM authentication. If the same username is being used in two different databases (e.g. "app_user"), there'd be no way for a generic plugin class to know which access key to use to generate the auth token. We should either pass the entire properties object or an additional "arg" parameter the way the SSL factories work. |
Good catch. Thanks! Yes, we need to do this in such a way that they do not need to provide a dummy password. |
package org.postgresql.core; | ||
|
||
public interface AuthenticationPlugin { | ||
byte[] getEncodedPassword(String userName, String password); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably need some javadoc here.
for example, this asks for encoded password, but what encoding?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fair enough. This is really a work in progress.
@sehrope I've reworked this, please have a look. |
|
||
byte[] getEncodedPassword(Properties info) throws PSQLException, PSQLException; | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer somewhat more generic interface that would include different auth-methods that PG supports today and some future looking like oauth2 access token that are currently commonly implemented through overloaded password field (though they are nothing like a passwords), and in future would hopefully get it's own auth-method (but keep the existing plugin ;) )
public interface AuthenticationPlugin {
AuthenticationInfo getAuthenticationInfo(Properties info) throws PSQLException, PSQLException;
}
public class AuthenticationInfo {
final String authenticationType;
final byte[] credential;
AuthenticationInfo (String authenticationType, byte[] credential) {
this.authenticationType= authenticationType;
this.credential= credential;
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would you need to know the auth method? I would think at the end of the day all we need is the password?
How would I use the AuthenticationInfo ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you scope AuthenticationPlugin only to Password auth-method then you would not need that info (and you may call plugin PasswordAuthenticationPlugin).
My proposal is to make plugin applicable to the other auth-methods as well. Depending on authenticationType the driver would put credential (password) in use differently.
That said, below may be a better interface.
public interface AuthenticationPlugin {
String getAuthenticationType(Properties info);
byte[] getCredential(Properties info) throws PSQLException, PSQLException;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What would getAuthenticationType(...)
return? Is that supposed to be the server auth method? If so, that would not work as the server decides how it wants the client to authenticate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The intended use is that if you want a different authentication mechanism you load it via Class.forName()
and acquire your password any way you wish. Ultimately the server only needs the password bytes. As @sehrope mentioned there is no real way to determine many of the auth methods ahead of time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In case server determines the method should we have something like this?
public interface AuthenticationPlugin {
boolean isSupported(String authenticationType, Properties info);
byte[] getCredential(String authenticationType, Properties info) throws PSQLException, PSQLException;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is currently one place where the server determines that we would use GSS encryption, but not authentication. I don't see anywhere else in the protocol where this happens.
pgjdbc/src/main/java/org/postgresql/core/AuthenticationPluginManager.java
Outdated
Show resolved
Hide resolved
pgjdbc/src/main/java/org/postgresql/core/AuthenticationPluginManager.java
Show resolved
Hide resolved
return new PasswordAuthentication(); | ||
} else { | ||
try { | ||
return (AuthenticationPlugin) Class.forName(authenticationClassName).getDeclaredConstructor().newInstance(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does there need to be consideration about what classloader to use?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should match however we handle other class parameters like socket factories.
We should also either add an "authManagerPluginArg" parameter to allow the caller to customize the instantiated class, e.g. new FooAuthPlugin(String arg)
.
Otherwise we'd be relying on them passing extra properties into the Properties object that we would ignore in the Driver itself. But if there's no standard for naming those, there's no guarantee we don't eventually add them as "real" properties. Maybe a prefixed name like "ext." to signify end user properties that we'll allow to exist in the Properties map but otherwise ignore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding args to the plugin is interesting. We would have to pass the arg into getAuthenticationPlugin
So one option is to provide a second getAuthenticationPlugin(Properties props, Object arg) ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No the Properties alone is fine as the user can retrieve the value if they want it. They just need a factory name, like sslfactoryarg
but specifically for this.
We could add a separate constructor that takes a String (again similar to the SSL factories) but it's not strictly necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we just follow the same pattern as the SSL factories (and use the ObjectFactory logic)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that's a good idea. All this class instantiation should happen in one place so the ClassLoader et al are handled consistently.
|
||
public interface AuthenticationPlugin { | ||
|
||
byte[] getEncodedPassword(Properties info) throws PSQLException, PSQLException; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there risk in providing (i.e. leaking) reference to the mutable Properties
instance?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe Function<PGProperty, String>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be nice if it's read-only but I don't think it's an issue. We already do it like this for anything created by ObjectFactory:
pgjdbc/pgjdbc/src/main/java/org/postgresql/util/ObjectFactory.java
Lines 39 to 62 in f61fbfe
public static Object instantiate(String classname, Properties info, boolean tryString, | |
@Nullable String stringarg) | |
throws ClassNotFoundException, SecurityException, NoSuchMethodException, | |
IllegalArgumentException, InstantiationException, IllegalAccessException, | |
InvocationTargetException { | |
@Nullable Object[] args = {info}; | |
Constructor<?> ctor = null; | |
Class<?> cls = Class.forName(classname); | |
try { | |
ctor = cls.getConstructor(Properties.class); | |
} catch (NoSuchMethodException ignored) { | |
} | |
if (tryString && ctor == null) { | |
try { | |
ctor = cls.getConstructor(String.class); | |
args = new String[]{stringarg}; | |
} catch (NoSuchMethodException ignored) { | |
} | |
} | |
if (ctor == null) { | |
ctor = cls.getConstructor(); | |
args = new Object[0]; | |
} | |
return ctor.newInstance(args); |
Plus we're blindly running whatever user code they supply to us via the class so in theory they could do whatever they want anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was not tracking on that. Do we have it documented anywhere that mutating the Properties instance could lead to indeterminate behavior (presumably not just for this connection but other connections, including future)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I'm not sure it's listed anywhere but it should be.
Modifying one should not impact any other connections though. Each one has their own copy even if it starts from the same common Properties conf:
Properties props = new Properties(defaults); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO the method name getEncodedPassword()
should be encodedPassword()
in that this really isn't a "getter" per say.
Should we add password expiration time as well? IAM and AAD tokens have limited validity period. Expiration time would be relevant for connection poolers ... |
As this would undoubtedly come from some property, I would think it would be enough to pass the properties into the plugin as proposed above. |
@davecramer I'd like to review this a bit more and am planning on getting to it either today or tomorrow. |
I haven't checked this and the use case, but having a Test setting an additional implementation of |
I started going through this PR. Definitely not ready to merge yet as it needs tests and I'm pretty sure there's at least one issue with supplying passwords to GSS auth. I'll follow up in a bit when I push some changes to a branch I'm testing out. |
@sehrope I left that out on purpose as anyone that is implementing their own authentication mechanism is not going to use GSS authentication. I guess I could be mistaken there... |
Yes I think it'd make sense for it to be consistent regardless of auth mechanism. That way it could be used for something like end user .pgpass handling regardless of auth mechanism. I pushed a branch here that started off as this PR rebased atop master: master...sehrope:pr-2321-add-plugin-manager-v3 First commit splits out some unrelated exception handling clean up. Second commit pulls out the user / password args from the internal methods and has any usage of them pull the values directly from Properties. This makes the "feature" commit much smaller and easier to review. The meat of the PR is the third commit which adds the new plugin feature. Couple changes / additions:
It passes the existing tests (https://github.com/sehrope/pgjdbc/runs/4131435657?check_suite_focus=true) but still needs some new ones for the new functionality. If this approach and API changes make sense, I can add those this week. |
seems like a good idea, lets continue down this path. Thanks! |
Closing this in favor of #2369 |
No description provided.