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

JDBC external auth shared token #7309

Merged
merged 2 commits into from
Apr 9, 2021

Conversation

s2lomon
Copy link
Member

@s2lomon s2lomon commented Mar 16, 2021

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.

@cla-bot cla-bot bot added the cla-signed label Mar 16, 2021
@s2lomon s2lomon requested a review from kokosing March 16, 2021 22:50
@s2lomon s2lomon added the WIP label Mar 16, 2021
{
requireNonNull(tokenSource, "tokenSource is null");

knownToken = null;
Copy link
Member

Choose a reason for hiding this comment

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

remove

/*
* 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.
Copy link
Member

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);
Copy link
Member

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(
Copy link
Member

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

Copy link
Member Author

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)
Copy link
Member

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.

@findepi findepi changed the title Jdbc external auth shared token JDBC external auth shared token Mar 17, 2021
@s2lomon s2lomon force-pushed the jdbc-external-auth-shared-token branch from 22eb640 to b4eb4e6 Compare March 23, 2021 00:52
class CashedToken
implements KnownToken
{
private static final ReadWriteLock lock = new ReentrantReadWriteLock();
Copy link
Member

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
Copy link
Member

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()
Copy link
Member

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()
Copy link
Member

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) {
Copy link
Member

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.

@Override
KnownToken create()
{
return KnownToken.local();
Copy link
Member

Choose a reason for hiding this comment

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

inline this local() method.

Copy link
Member Author

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 {
Copy link
Member

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();
Copy link
Member

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.

Copy link
Member Author

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.

@s2lomon s2lomon force-pushed the jdbc-external-auth-shared-token branch from b4eb4e6 to 850779d Compare March 24, 2021 14:12
@s2lomon s2lomon removed the WIP label Mar 24, 2021
@s2lomon s2lomon force-pushed the jdbc-external-auth-shared-token branch from 850779d to 80ce2f0 Compare March 24, 2021 15:46
Copy link
Member

@kokosing kokosing left a comment

Choose a reason for hiding this comment

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

Looks good.


public final class MockTokenPoller
implements TokenPoller
{
private final Map<URI, Queue<TokenPollResult>> results = new HashMap<>();
private final Map<URI, BlockingDeque<TokenPollResult>> results = new HashMap<>();
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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,
Copy link
Member

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)

Copy link
Member Author

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);
Copy link
Member

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"));

Copy link
Member Author

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,
Copy link
Member

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)
Copy link
Member

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);
Copy link
Member

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?

Copy link
Member Author

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();
Copy link
Member

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)?

Copy link
Member Author

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,
Copy link
Member

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

Copy link
Member Author

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
Copy link
Member

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);
Copy link
Member

Choose a reason for hiding this comment

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

containsOnly?

@s2lomon s2lomon force-pushed the jdbc-external-auth-shared-token branch from 80ce2f0 to 55db7e4 Compare March 31, 2021 15:27
@s2lomon s2lomon requested a review from kokosing March 31, 2021 15:27

public final class MockTokenPoller
implements TokenPoller
{
private final Map<URI, Queue<TokenPollResult>> results = new HashMap<>();
private final Map<URI, BlockingDeque<TokenPollResult>> results = new HashMap<>();
Copy link
Member

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();
Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

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

just throws Exception

@kokosing
Copy link
Member

kokosing commented Apr 1, 2021

@dain @electrum would you like to take a look?

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.
@s2lomon s2lomon force-pushed the jdbc-external-auth-shared-token branch from 55db7e4 to 7162450 Compare April 8, 2021 17:03
@kokosing
Copy link
Member

kokosing commented Apr 9, 2021

CI hit: #7534

@kokosing kokosing merged commit ceb2d1b into trinodb:master Apr 9, 2021
@kokosing kokosing mentioned this pull request Apr 9, 2021
9 tasks
@martint martint added this to the 356 milestone Apr 26, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

4 participants