diff --git a/client/trino-cli/src/main/java/io/trino/cli/ClientOptions.java b/client/trino-cli/src/main/java/io/trino/cli/ClientOptions.java index 52fbc8e6cfd4..5e79612f1ab5 100644 --- a/client/trino-cli/src/main/java/io/trino/cli/ClientOptions.java +++ b/client/trino-cli/src/main/java/io/trino/cli/ClientOptions.java @@ -21,6 +21,7 @@ import com.google.common.net.HostAndPort; import io.airlift.units.Duration; import io.trino.client.ClientSession; +import io.trino.client.auth.external.ExternalRedirectStrategy; import okhttp3.logging.HttpLoggingInterceptor; import java.net.URI; @@ -105,6 +106,9 @@ public class ClientOptions @Option(names = "--external-authentication", paramLabel = "", description = "Enable external authentication") public boolean externalAuthentication; + @Option(names = "--external-authentication-redirect-handler", paramLabel = "", description = "External authentication redirect handlers: ${COMPLETION-CANDIDATES} " + DEFAULT_VALUE, defaultValue = "ALL") + public List externalAuthenticationRedirectHandler = new ArrayList<>(); + @Option(names = "--source", paramLabel = "", defaultValue = "trino-cli", description = "Name of source making query " + DEFAULT_VALUE) public String source; diff --git a/client/trino-cli/src/main/java/io/trino/cli/Console.java b/client/trino-cli/src/main/java/io/trino/cli/Console.java index 468fd5fd4d8f..0eb43e248dc4 100644 --- a/client/trino-cli/src/main/java/io/trino/cli/Console.java +++ b/client/trino-cli/src/main/java/io/trino/cli/Console.java @@ -178,7 +178,8 @@ public boolean run() clientOptions.krb5CredentialCachePath, !clientOptions.krb5DisableRemoteServiceHostnameCanonicalization, false, - clientOptions.externalAuthentication)) { + clientOptions.externalAuthentication, + clientOptions.externalAuthenticationRedirectHandler)) { if (hasQuery) { return executeCommand( queryRunner, diff --git a/client/trino-cli/src/main/java/io/trino/cli/QueryRunner.java b/client/trino-cli/src/main/java/io/trino/cli/QueryRunner.java index 1b658cfa7d66..79602def5668 100644 --- a/client/trino-cli/src/main/java/io/trino/cli/QueryRunner.java +++ b/client/trino-cli/src/main/java/io/trino/cli/QueryRunner.java @@ -17,7 +17,9 @@ import io.trino.client.ClientSession; import io.trino.client.OkHttpUtil; import io.trino.client.StatementClient; +import io.trino.client.auth.external.CompositeRedirectHandler; import io.trino.client.auth.external.ExternalAuthenticator; +import io.trino.client.auth.external.ExternalRedirectStrategy; import io.trino.client.auth.external.HttpTokenPoller; import io.trino.client.auth.external.KnownToken; import io.trino.client.auth.external.RedirectHandler; @@ -28,6 +30,7 @@ import java.io.Closeable; import java.io.File; import java.time.Duration; +import java.util.List; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; @@ -43,7 +46,6 @@ import static io.trino.client.OkHttpUtil.setupTimeouts; import static io.trino.client.OkHttpUtil.tokenAuth; import static io.trino.client.StatementClientFactory.newStatementClient; -import static java.lang.System.out; import static java.util.Objects.requireNonNull; import static java.util.concurrent.TimeUnit.SECONDS; @@ -79,7 +81,8 @@ public QueryRunner( Optional kerberosCredentialCachePath, boolean kerberosUseCanonicalHostname, boolean delegatedKerberos, - boolean externalAuthentication) + boolean externalAuthentication, + List externalRedirectHandlers) { this.session = new AtomicReference<>(requireNonNull(session, "session is null")); this.debug = debug; @@ -99,7 +102,7 @@ public QueryRunner( setupHttpProxy(builder, httpProxy); setupBasicAuth(builder, session, user, password); setupTokenAuth(builder, session, accessToken); - setupExternalAuth(builder, session, externalAuthentication, sslSetup); + setupExternalAuth(builder, session, externalAuthentication, externalRedirectHandlers, sslSetup); builder.addNetworkInterceptor(new HttpLoggingInterceptor(System.err::println).setLevel(networkLogging)); @@ -179,6 +182,7 @@ private static void setupExternalAuth( OkHttpClient.Builder builder, ClientSession session, boolean enabled, + List externalRedirectHandlers, Consumer sslSetup) { if (!enabled) { @@ -187,10 +191,8 @@ private static void setupExternalAuth( checkArgument(session.getServer().getScheme().equalsIgnoreCase("https"), "Authentication using externalAuthentication requires HTTPS to be enabled"); - RedirectHandler redirectHandler = uri -> { - out.println("External authentication required. Please go to:"); - out.println(uri.toString()); - }; + RedirectHandler redirectHandler = new CompositeRedirectHandler(externalRedirectHandlers); + TokenPoller poller = new HttpTokenPoller(builder.build(), sslSetup); ExternalAuthenticator authenticator = new ExternalAuthenticator( diff --git a/client/trino-cli/src/test/java/io/trino/cli/TestQueryRunner.java b/client/trino-cli/src/test/java/io/trino/cli/TestQueryRunner.java index 676cd18dfeaf..63e74490c796 100644 --- a/client/trino-cli/src/test/java/io/trino/cli/TestQueryRunner.java +++ b/client/trino-cli/src/test/java/io/trino/cli/TestQueryRunner.java @@ -44,6 +44,7 @@ import static io.trino.cli.ClientOptions.OutputFormat.CSV; import static io.trino.cli.TerminalUtils.getTerminal; import static io.trino.client.ClientStandardTypes.BIGINT; +import static io.trino.client.auth.external.ExternalRedirectStrategy.PRINT; import static java.util.concurrent.TimeUnit.MINUTES; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNull; @@ -168,7 +169,8 @@ static QueryRunner createQueryRunner(ClientSession clientSession, boolean insecu Optional.empty(), false, false, - false); + false, + ImmutableList.of(PRINT)); } static PrintStream nullPrintStream() diff --git a/client/trino-client/src/main/java/io/trino/client/auth/external/CompositeRedirectHandler.java b/client/trino-client/src/main/java/io/trino/client/auth/external/CompositeRedirectHandler.java new file mode 100644 index 000000000000..d86cd02ab504 --- /dev/null +++ b/client/trino-client/src/main/java/io/trino/client/auth/external/CompositeRedirectHandler.java @@ -0,0 +1,54 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.client.auth.external; + +import java.net.URI; +import java.util.List; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static java.lang.String.format; +import static java.util.Objects.requireNonNull; + +public class CompositeRedirectHandler + implements RedirectHandler +{ + private final List handlers; + + public CompositeRedirectHandler(List strategies) + { + this.handlers = requireNonNull(strategies, "strategies is null") + .stream() + .map(ExternalRedirectStrategy::getHandler) + .collect(toImmutableList()); + checkState(!handlers.isEmpty(), "Expected at least one external redirect handler"); + } + + @Override + public void redirectTo(URI uri) throws RedirectException + { + RedirectException redirectException = new RedirectException(format("Could not redirect to " + uri)); + for (RedirectHandler handler : handlers) { + try { + handler.redirectTo(uri); + return; + } + catch (RedirectException e) { + redirectException.addSuppressed(e); + } + } + + throw redirectException; + } +} diff --git a/client/trino-client/src/main/java/io/trino/client/auth/external/ExternalRedirectStrategy.java b/client/trino-client/src/main/java/io/trino/client/auth/external/ExternalRedirectStrategy.java new file mode 100644 index 000000000000..fcd881d6019b --- /dev/null +++ b/client/trino-client/src/main/java/io/trino/client/auth/external/ExternalRedirectStrategy.java @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.client.auth.external; + +import com.google.common.collect.ImmutableList; + +import static java.util.Objects.requireNonNull; + +public enum ExternalRedirectStrategy +{ + DESKTOP_OPEN(new DesktopBrowserRedirectHandler()), + SYSTEM_OPEN(new SystemOpenRedirectHandler()), + PRINT(new SystemOutPrintRedirectHandler()), + OPEN(new CompositeRedirectHandler(ImmutableList.of(SYSTEM_OPEN, DESKTOP_OPEN))), + ALL(new CompositeRedirectHandler(ImmutableList.of(OPEN, PRINT))) + /**/; + + private final RedirectHandler handler; + + ExternalRedirectStrategy(RedirectHandler handler) + { + this.handler = requireNonNull(handler, "handler is null"); + } + + public RedirectHandler getHandler() + { + return handler; + } +} diff --git a/client/trino-client/src/main/java/io/trino/client/auth/external/SystemOpenRedirectHandler.java b/client/trino-client/src/main/java/io/trino/client/auth/external/SystemOpenRedirectHandler.java new file mode 100644 index 000000000000..b601d4336303 --- /dev/null +++ b/client/trino-client/src/main/java/io/trino/client/auth/external/SystemOpenRedirectHandler.java @@ -0,0 +1,102 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.client.auth.external; + +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.URI; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; + +import static java.lang.String.format; +import static java.util.Locale.ENGLISH; + +public class SystemOpenRedirectHandler + implements RedirectHandler +{ + private static final List LINUX_BROWSERS = ImmutableList.of( + "xdg-open", + "gnome-open", + "kde-open", + "chromium", + "google", + "google-chrome", + "firefox", + "mozilla", + "opera", + "epiphany", + "konqueror"); + + private static final String MACOS_OPEN_COMMAND = "open"; + private static final String WINDOWS_OPEN_COMMAND = "rundll32 url.dll,FileProtocolHandler"; + + private static final Splitter SPLITTER = Splitter.on(":") + .omitEmptyStrings() + .trimResults(); + + @Override + public void redirectTo(URI uri) + throws RedirectException + { + String operatingSystem = System.getProperty("os.name").toLowerCase(ENGLISH); + + try { + if (operatingSystem.contains("mac")) { + exec(uri, MACOS_OPEN_COMMAND); + } + else if (operatingSystem.contains("windows")) { + exec(uri, WINDOWS_OPEN_COMMAND); + } + else { + String executablePath = findLinuxBrowser() + .orElseThrow(() -> new FileNotFoundException("Could not find any known linux browser in $PATH")); + exec(uri, executablePath); + } + } + catch (IOException e) { + throw new RedirectException(format("Could not open uri %s", uri), e); + } + } + + private static Optional findLinuxBrowser() + { + List paths = SPLITTER.splitToList(System.getenv("PATH")); + for (String path : paths) { + File[] found = Paths.get(path) + .toFile() + .listFiles((dir, name) -> LINUX_BROWSERS.contains(name)); + + if (found == null) { + continue; + } + + if (found.length > 0) { + return Optional.of(found[0].getPath()); + } + } + + return Optional.empty(); + } + + private static void exec(URI uri, String openCommand) + throws IOException + { + Runtime.getRuntime().exec(openCommand + " " + uri.toString()); + } +} diff --git a/client/trino-client/src/main/java/io/trino/client/auth/external/SystemOutPrintRedirectHandler.java b/client/trino-client/src/main/java/io/trino/client/auth/external/SystemOutPrintRedirectHandler.java new file mode 100644 index 000000000000..5452264a3ccd --- /dev/null +++ b/client/trino-client/src/main/java/io/trino/client/auth/external/SystemOutPrintRedirectHandler.java @@ -0,0 +1,27 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.trino.client.auth.external; + +import java.net.URI; + +public class SystemOutPrintRedirectHandler + implements RedirectHandler +{ + @Override + public void redirectTo(URI uri) throws RedirectException + { + System.out.println("External authentication required. Please go to:"); + System.out.println(uri.toString()); + } +} diff --git a/client/trino-jdbc/src/main/java/io/trino/jdbc/ConnectionProperties.java b/client/trino-jdbc/src/main/java/io/trino/jdbc/ConnectionProperties.java index 16dfacc8aea3..49416683452e 100644 --- a/client/trino-jdbc/src/main/java/io/trino/jdbc/ConnectionProperties.java +++ b/client/trino-jdbc/src/main/java/io/trino/jdbc/ConnectionProperties.java @@ -20,6 +20,7 @@ import com.google.common.net.HostAndPort; import io.airlift.units.Duration; import io.trino.client.ClientSelectedRole; +import io.trino.client.auth.external.ExternalRedirectStrategy; import java.io.File; import java.util.List; @@ -28,8 +29,10 @@ import java.util.Properties; import java.util.Set; import java.util.function.Predicate; +import java.util.stream.StreamSupport; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; import static com.google.common.collect.Maps.immutableEntry; import static io.trino.client.ClientSelectedRole.Type.ALL; @@ -75,6 +78,7 @@ enum SslVerificationMode public static final ConnectionProperty ACCESS_TOKEN = new AccessToken(); public static final ConnectionProperty EXTERNAL_AUTHENTICATION = new ExternalAuthentication(); public static final ConnectionProperty EXTERNAL_AUTHENTICATION_TIMEOUT = new ExternalAuthenticationTimeout(); + public static final ConnectionProperty> EXTERNAL_AUTHENTICATION_REDIRECT_HANDLERS = new ExternalAuthenticationRedirectHandlers(); public static final ConnectionProperty EXTERNAL_AUTHENTICATION_TOKEN_CACHE = new ExternalAuthenticationTokenCache(); public static final ConnectionProperty> EXTRA_CREDENTIALS = new ExtraCredentials(); public static final ConnectionProperty CLIENT_INFO = new ClientInfo(); @@ -119,6 +123,7 @@ enum SslVerificationMode .add(EXTERNAL_AUTHENTICATION) .add(EXTERNAL_AUTHENTICATION_TIMEOUT) .add(EXTERNAL_AUTHENTICATION_TOKEN_CACHE) + .add(EXTERNAL_AUTHENTICATION_REDIRECT_HANDLERS) .build(); private static final Map> KEY_LOOKUP = unmodifiableMap(ALL_PROPERTIES.stream() @@ -478,6 +483,24 @@ public ExternalAuthentication() } } + private static class ExternalAuthenticationRedirectHandlers + extends AbstractConnectionProperty> + { + private static final Splitter ENUM_SPLITTER = Splitter.on(',').trimResults().omitEmptyStrings(); + + public ExternalAuthenticationRedirectHandlers() + { + super("externalAuthenticationRedirectHandlers", Optional.of("OPEN"), NOT_REQUIRED, ALLOWED, ExternalAuthenticationRedirectHandlers::parse); + } + + public static List parse(String value) + { + return StreamSupport.stream(ENUM_SPLITTER.split(value).spliterator(), false) + .map(ExternalRedirectStrategy::valueOf) + .collect(toImmutableList()); + } + } + private static class ExternalAuthenticationTimeout extends AbstractConnectionProperty { diff --git a/client/trino-jdbc/src/main/java/io/trino/jdbc/TrinoDriverUri.java b/client/trino-jdbc/src/main/java/io/trino/jdbc/TrinoDriverUri.java index 6ee59c052a98..24756473dcce 100644 --- a/client/trino-jdbc/src/main/java/io/trino/jdbc/TrinoDriverUri.java +++ b/client/trino-jdbc/src/main/java/io/trino/jdbc/TrinoDriverUri.java @@ -20,7 +20,7 @@ import com.google.common.net.HostAndPort; import io.trino.client.ClientException; import io.trino.client.ClientSelectedRole; -import io.trino.client.auth.external.DesktopBrowserRedirectHandler; +import io.trino.client.auth.external.CompositeRedirectHandler; import io.trino.client.auth.external.ExternalAuthenticator; import io.trino.client.auth.external.HttpTokenPoller; import io.trino.client.auth.external.RedirectHandler; @@ -58,6 +58,7 @@ import static io.trino.jdbc.ConnectionProperties.CLIENT_TAGS; import static io.trino.jdbc.ConnectionProperties.DISABLE_COMPRESSION; import static io.trino.jdbc.ConnectionProperties.EXTERNAL_AUTHENTICATION; +import static io.trino.jdbc.ConnectionProperties.EXTERNAL_AUTHENTICATION_REDIRECT_HANDLERS; import static io.trino.jdbc.ConnectionProperties.EXTERNAL_AUTHENTICATION_TIMEOUT; import static io.trino.jdbc.ConnectionProperties.EXTERNAL_AUTHENTICATION_TOKEN_CACHE; import static io.trino.jdbc.ConnectionProperties.EXTRA_CREDENTIALS; @@ -103,8 +104,7 @@ public final class TrinoDriverUri private static final Splitter QUERY_SPLITTER = Splitter.on('&').omitEmptyStrings(); private static final Splitter ARG_SPLITTER = Splitter.on('=').limit(2); - private static final AtomicReference REDIRECT_HANDLER = new AtomicReference<>(new DesktopBrowserRedirectHandler()); - + private static final AtomicReference REDIRECT_HANDLER = new AtomicReference<>(null); private final HostAndPort address; private final URI uri; @@ -321,7 +321,14 @@ public void setupClient(OkHttpClient.Builder builder) KnownTokenCache knownTokenCache = EXTERNAL_AUTHENTICATION_TOKEN_CACHE.getValue(properties).get(); - ExternalAuthenticator authenticator = new ExternalAuthenticator(REDIRECT_HANDLER.get(), poller, knownTokenCache.create(), timeout); + Optional configuredHandler = EXTERNAL_AUTHENTICATION_REDIRECT_HANDLERS.getValue(properties) + .map(CompositeRedirectHandler::new) + .map(RedirectHandler.class::cast); + + RedirectHandler redirectHandler = Optional.ofNullable(REDIRECT_HANDLER.get()) + .orElseGet(() -> configuredHandler.orElseThrow(() -> new RuntimeException("External authentication redirect handler is not configured"))); + + ExternalAuthenticator authenticator = new ExternalAuthenticator(redirectHandler, poller, knownTokenCache.create(), timeout); builder.authenticator(authenticator); builder.addInterceptor(authenticator);