-
Notifications
You must be signed in to change notification settings - Fork 3k
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
JDBC external auth shared token #7309
JDBC external auth shared token #7309
Conversation
client/trino-client/src/test/java/io/trino/client/auth/external/MockRedirectHandler.java
Show resolved
Hide resolved
{ | ||
requireNonNull(tokenSource, "tokenSource is null"); | ||
|
||
knownToken = null; |
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.
remove
client/trino-client/src/test/java/io/trino/client/auth/external/MockRedirectHandler.java
Show resolved
Hide resolved
client/trino-client/src/main/java/io/trino/client/auth/external/KnownToken.java
Show resolved
Hide resolved
/* | ||
* Operation for obtaining token is almost always bound to take a lot of time, as it will | ||
* require user actions in a web-browser. The idea here is to trigger it only once, despite the number of | ||
* concurrent invocations to setup a new token. |
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 if authentication flow failed somehow (server does not know it failed) and user would like to start a new one?
{ | ||
public ExternalAuthenticationSharedToken() | ||
{ | ||
super("externalAuthenticationSharedToken", Optional.of("false"), NOT_REQUIRED, ALLOWED, BOOLEAN_CONVERTER); |
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 looks to me more about implementation than UX behaviour.
How about externalAuthenticationCache=NONE|MEMORY
? Where NONE
is default.
.sleepOnRedirect(Duration.ofMillis(100)); | ||
|
||
ExternalAuthenticator authenticator = new ExternalAuthenticator(redirectHandler, tokenPoller, KnownToken.shared("user-name"), Duration.ofSeconds(1)); | ||
List<Future<Request>> requests = executor.invokeAll(ImmutableList.<Callable<Request>>of( |
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.
Use IntStream
and collect to list
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.
Sorry, but since we need to generate Callable to change it to Future, doing this through stream requires construction like () -> () -> authenticator.authenticate(null, getUnauthorizedResponse("Bearer x_token_server=\"http://token.uri\", x_redirect_server=\"http://redirect.uri\"")
, which I find much less readable than simply repeating 4 times the same call.
} | ||
|
||
@Override | ||
public void setupToken(Supplier<Token> tokenSource) |
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 make this method simply synchronized and same for getToken
. Then you can use plain map.
22eb640
to
b4eb4e6
Compare
class CashedToken | ||
implements KnownToken | ||
{ | ||
private static final ReadWriteLock lock = new ReentrantReadWriteLock(); |
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.
Can we have it as regular singleton, where you have single static instance
and other fields are not static.
* many Connections will be actively using it. It's very important as obtaining the new token | ||
* will take minutes, as it mostly requires user thinking time. | ||
*/ | ||
class CashedToken |
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.
MemoryCachedKnownToken
? It is not about the money....
{ | ||
//Try to lock and generate new token. If some other thread (Connection) has | ||
//already obtained writeLock and is generating new token, then skipp this | ||
//to block on getToken() |
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.
add space after //
here and below
|
||
static KnownToken local() | ||
{ | ||
return new KnownToken() |
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.
Please extract this as named class.
throw new RuntimeException(e); | ||
} | ||
catch (ExecutionException executionException) { | ||
if (executionException.getCause() != null) { |
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.
checkState, there should always be a cause.
client/trino-client/src/test/java/io/trino/client/auth/external/TestExternalAuthenticator.java
Show resolved
Hide resolved
@Override | ||
KnownToken create() | ||
{ | ||
return KnownToken.local(); |
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.
inline this local()
method.
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 don't want to, as this would require declaring KnownToken classes instances with public modifiers etc. I want them to be hidden in the client-api and here used only through designated factory methods, without revealing their implementation etc.
return KnownToken.local(); | ||
} | ||
}, | ||
CASHED { |
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.
MEMORY
@Override | ||
KnownToken create() | ||
{ | ||
return KnownToken.cashed(); |
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.
inline this cached()
(cachedInMemory()
) method.
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.
Just like above, I would have to make this implementation public and I don't think that's a good idea.
b4eb4e6
to
850779d
Compare
850779d
to
80ce2f0
Compare
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.
Looks good.
client/trino-client/src/test/java/io/trino/client/auth/external/MockRedirectHandler.java
Show resolved
Hide resolved
|
||
public final class MockTokenPoller | ||
implements TokenPoller | ||
{ | ||
private final Map<URI, Queue<TokenPollResult>> results = new HashMap<>(); | ||
private final Map<URI, BlockingDeque<TokenPollResult>> results = new HashMap<>(); |
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 this changed? Is this accessed by multiple threads?
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, in our new tests it is.
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.
so results
should also be thread safe. Isn't it?
MockRedirectHandler redirectHandler = new MockRedirectHandler(); | ||
|
||
ExternalAuthenticator authenticator = new ExternalAuthenticator(redirectHandler, tokenPoller, KnownToken.local(), Duration.ofSeconds(1)); | ||
List<Future<Request>> requests = times(4, |
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.
nit: each arg in separate line (please put 4
to the next line)
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.
👍
public void testAuthenticationFromMultipleThreadsWithLocallyStoredToken() | ||
throws Exception | ||
{ | ||
ExecutorService executor = newFixedThreadPool(5); |
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.
Let's have a shared executor service per entire class. So we would avoid opening and closing it in each tests.
Use newCachedThreadPool(daemonThreadsNamed(getClass().getName() + "-%s"));
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 will make the tests less separated and will add additional handling for the assertions. Right now since I'm blocking until all the tasks are finished, I can simply assert the end results. I can change the thread pool construction though.
.sleepOnRedirect(Duration.ofMillis(10)); | ||
|
||
ExternalAuthenticator authenticator = new ExternalAuthenticator(redirectHandler, tokenPoller, KnownToken.memoryCached(), Duration.ofSeconds(1)); | ||
List<Future<Request>> requests = times(4, |
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.
nit: args in separate lines
assertThat(redirectHandler.getRedirectionCount()).isEqualTo(1); | ||
} | ||
|
||
static Stream<Callable<Request>> times(int times, Callable<Request> request) |
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.
private, please put it to the very bottom
throws Exception | ||
{ | ||
ExecutorService executor = newFixedThreadPool(5); | ||
ExecutorService interruptableThreadPool = newFixedThreadPool(1); |
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 do you need two pools?
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.
To generate interrupted exception instead of cancellation exception (which happens why I simply cancel a thread)
.collect(toImmutableList()); | ||
|
||
Thread.sleep(100); | ||
interruptableThreadPool.shutdownNow(); |
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.
did you mean interruptedAuthentication.cancel(true)
?
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, as even with true for the interrupt, the end exception is Cancelation rather than Interrupted Exception and it seems to me that it's better for the case that we want to check here.
ExternalAuthenticator authenticator = new ExternalAuthenticator(redirectHandler, (uri, duration) -> TokenPollResult.pending(uri), KnownToken.memoryCached(), Duration.ofMillis(1)); | ||
Future<Request> interruptedAuthentication = interruptableThreadPool.submit( | ||
() -> authenticator.authenticate(null, getUnauthorizedResponse("Bearer x_token_server=\"http://token.uri\", x_redirect_server=\"http://redirect.uri\""))); | ||
List<Future<Request>> requests = times(2, |
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 think you need to sleep before you schedule anything here, to make sure interruptedAuthentication
went sleep
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 can add a very short sleep, just in case.
* and it's designed to use it in fully serialized manner. | ||
*/ | ||
@NotThreadSafe | ||
class LocalKnowToken |
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.
Known
.extracting(Future::get) | ||
.extracting(Request::headers) | ||
.extracting(headers -> headers.get(AUTHORIZATION)) | ||
.allMatch("Bearer valid-token"::equals); |
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.
containsOnly
?
80ce2f0
to
55db7e4
Compare
|
||
public final class MockTokenPoller | ||
implements TokenPoller | ||
{ | ||
private final Map<URI, Queue<TokenPollResult>> results = new HashMap<>(); | ||
private final Map<URI, BlockingDeque<TokenPollResult>> results = new HashMap<>(); |
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.
so results
should also be thread safe. Isn't it?
public void shutDownThreadPool() | ||
throws InterruptedException | ||
{ | ||
executor.shutdown(); |
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.
shutdownNow()
is enough
|
||
@AfterClass(alwaysRun = true) | ||
public void shutDownThreadPool() | ||
throws InterruptedException |
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.
just throws Exception
This change allows sharing external authentication tokens between different Connections. Each time when a new token is required, first Connection that needs it, will handle obtaining a new token when all the other Connections wait for this operation to finish. Token is kept in memmory, guarded by ReadWriteLock. To enable token cache use externalAuthenticationTokenCache=MEMORY Default value for externalAuthenticationTokenCache is NONE.
55db7e4
to
7162450
Compare
CI hit: #7534 |
This change allows sharing external authentication tokens
between different Connections, as long as they've been requested for
the same user.
Tokens are kept in ConcurrentHashMap, where jdbc user name is the key.
Different Connections - even for the same user, might be requested and used concurrently, I've decided to synchronize on resolving a token, to prevent spamming an end client with many web-browser pages opening. The idea is that whenever a user decide to share a token, only first request for a token will be served and all other will try to reuse token established by the previous request.
This option has been proposed only for jdbc, as it doesn't seem to have any meaning in cli.