/* * Copyright © 2014-2015 Cask Data, Inc. * * 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 co.cask.cdap.cli; import co.cask.cdap.cli.command.system.VersionCommand; import co.cask.cdap.cli.util.FilePathResolver; import co.cask.cdap.cli.util.table.AltStyleTableRenderer; import co.cask.cdap.cli.util.table.TableRenderer; import co.cask.cdap.cli.util.table.TableRendererConfig; import co.cask.cdap.client.MetaClient; import co.cask.cdap.client.config.ClientConfig; import co.cask.cdap.client.config.ConnectionConfig; import co.cask.cdap.client.exception.DisconnectedException; import co.cask.cdap.common.UnauthenticatedException; import co.cask.cdap.proto.Id; import co.cask.cdap.security.authentication.client.AccessToken; import co.cask.cdap.security.authentication.client.AuthenticationClient; import co.cask.cdap.security.authentication.client.Credential; import co.cask.cdap.security.authentication.client.basic.BasicAuthenticationClient; import com.google.common.base.Charsets; import com.google.common.base.Supplier; import com.google.common.base.Throwables; import com.google.common.collect.Lists; import com.google.common.io.CharStreams; import com.google.common.io.Files; import com.google.common.io.InputSupplier; import com.google.gson.Gson; import com.google.gson.JsonSyntaxException; import jline.TerminalFactory; import jline.console.ConsoleReader; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; import java.util.List; import java.util.Properties; import javax.annotation.Nullable; /** * Configuration for the CDAP CLI. */ public class CLIConfig implements TableRendererConfig { public static final String ENV_ACCESSTOKEN = "ACCESS_TOKEN"; private static final int DEFAULT_LINE_WIDTH = 80; private static final int MIN_LINE_WIDTH = 40; private static final Gson GSON = new Gson(); private final ClientConfig clientConfig; private final FilePathResolver resolver; private final String version; private final PrintStream output; private CLIConnectionConfig connectionConfig; private TableRenderer tableRenderer; private List<ConnectionChangeListener> connectionChangeListeners; private Supplier<Integer> lineWidthSupplier = new Supplier<Integer>() { @Override public Integer get() { try { return TerminalFactory.get().getWidth(); } catch (Exception e) { return DEFAULT_LINE_WIDTH; } } }; /* * Wrapper class for reading/writing of username + accessToken */ private class UserAccessToken { private final AccessToken accessToken; private final String username; public UserAccessToken(AccessToken accessToken, String username) { this.accessToken = accessToken; this.username = username; } public AccessToken getAccessToken() { return accessToken; } public String getUsername() { return username; } } /** * @param clientConfig client configuration */ public CLIConfig(ClientConfig clientConfig, PrintStream output, TableRenderer tableRenderer) { this.clientConfig = clientConfig; this.output = output; this.tableRenderer = tableRenderer; this.resolver = new FilePathResolver(); this.version = tryGetVersion(); this.connectionChangeListeners = Lists.newArrayList(); } public CLIConfig() { this(ClientConfig.builder().build(), System.out, new AltStyleTableRenderer()); } public PrintStream getOutput() { return output; } public TableRenderer getTableRenderer() { return tableRenderer; } public Id.Namespace getCurrentNamespace() { if (connectionConfig == null || connectionConfig.getNamespace() == null) { throw new DisconnectedException(connectionConfig); } return connectionConfig.getNamespace(); } public void setTableRenderer(TableRenderer tableRenderer) { this.tableRenderer = tableRenderer; } public void setConnectionConfig(@Nullable CLIConnectionConfig connectionConfig) { this.connectionConfig = connectionConfig; clientConfig.setConnectionConfig(connectionConfig); notifyConnectionChanged(); } public void tryConnect(CLIConnectionConfig connectionConfig, boolean verifySSLCert, PrintStream output, boolean debug) throws Exception { try { clientConfig.setVerifySSLCert(verifySSLCert); UserAccessToken userToken = acquireAccessToken(clientConfig, connectionConfig, output, debug); AccessToken accessToken = null; if (userToken != null) { accessToken = userToken.getAccessToken(); connectionConfig = new CLIConnectionConfig(connectionConfig, connectionConfig.getNamespace(), userToken.getUsername()); } checkConnection(clientConfig, connectionConfig, accessToken); setConnectionConfig(connectionConfig); clientConfig.setAccessToken(accessToken); output.printf("Successfully connected to CDAP instance at %s", connectionConfig.getURI().toString()); output.println(); } catch (IOException e) { throw new IOException(String.format("CDAP instance at '%s' could not be reached: %s", connectionConfig.getURI().toString(), e.getMessage()), e); } } public void updateAccessToken(PrintStream output) throws IOException { UserAccessToken newAccessToken = getNewAccessToken(clientConfig.getConnectionConfig(), output, false); clientConfig.setAccessToken(newAccessToken.getAccessToken()); } private void checkConnection(ClientConfig baseClientConfig, ConnectionConfig connectionInfo, AccessToken accessToken) throws IOException, UnauthenticatedException { ClientConfig clientConfig = new ClientConfig.Builder(baseClientConfig) .setConnectionConfig(connectionInfo) .setAccessToken(accessToken) .build(); MetaClient metaClient = new MetaClient(clientConfig); try { metaClient.ping(); } catch (IOException e) { throw new IOException("Check hostname and/or port", e); } } private boolean isAuthenticationEnabled(ConnectionConfig connectionInfo) throws IOException { return getAuthenticationClient(connectionInfo).isAuthEnabled(); } @Nullable private UserAccessToken acquireAccessToken(ClientConfig clientConfig, ConnectionConfig connectionInfo, PrintStream output, boolean debug) throws IOException { if (!isAuthenticationEnabled(connectionInfo)) { return null; } try { UserAccessToken savedToken = getSavedAccessToken(connectionInfo.getHostname()); if (savedToken == null) { throw new UnauthenticatedException(); } checkConnection(clientConfig, connectionInfo, savedToken.getAccessToken()); return savedToken; } catch (UnauthenticatedException ignored) { // access token invalid - fall through to try acquiring token manually } return getNewAccessToken(connectionInfo, output, debug); } private UserAccessToken getNewAccessToken(ConnectionConfig connectionInfo, PrintStream output, boolean debug) throws IOException { AuthenticationClient authenticationClient = getAuthenticationClient(connectionInfo); Properties properties = new Properties(); properties.put(BasicAuthenticationClient.VERIFY_SSL_CERT_PROP_NAME, String.valueOf(clientConfig.isVerifySSLCert())); String username = ""; // obtain new access token via manual user input output.printf("Authentication is enabled in the CDAP instance: %s.\n", connectionInfo.getHostname()); ConsoleReader reader = new ConsoleReader(); for (Credential credential : authenticationClient.getRequiredCredentials()) { String prompt = "Please, specify " + credential.getDescription() + "> "; String credentialValue; if (credential.isSecret()) { credentialValue = reader.readLine(prompt, '*'); } else { credentialValue = reader.readLine(prompt); } properties.put(credential.getName(), credentialValue); if (credential.getName().contains("username")) { username = credentialValue; } } authenticationClient.configure(properties); AccessToken accessToken = authenticationClient.getAccessToken(); UserAccessToken userToken = new UserAccessToken(accessToken, username); if (accessToken != null) { if (saveAccessToken(userToken, connectionInfo.getHostname()) && debug) { output.printf("Saved access token to %s\n", getAccessTokenFile(connectionInfo.getHostname()).getAbsolutePath()); } } return userToken; } private AuthenticationClient getAuthenticationClient(ConnectionConfig connectionInfo) { AuthenticationClient authenticationClient = new BasicAuthenticationClient(); authenticationClient.setConnectionInfo(connectionInfo.getHostname(), connectionInfo.getPort(), connectionInfo.isSSLEnabled()); return authenticationClient; } @Nullable private UserAccessToken getSavedAccessToken(String hostname) { File file = getAccessTokenFile(hostname); try (BufferedReader reader = Files.newReader(file, Charsets.UTF_8)) { return GSON.fromJson(reader, UserAccessToken.class); } catch (IOException | JsonSyntaxException ignored) { // Fall through } return null; } private boolean saveAccessToken(UserAccessToken accessToken, String hostname) { File accessTokenFile = getAccessTokenFile(hostname); try { Files.write(GSON.toJson(accessToken), accessTokenFile, Charsets.UTF_8); return true; } catch (IOException ignored) { // NO-OP } return false; } private File getAccessTokenFile(String hostname) { String accessTokenEnv = System.getenv(CLIConfig.ENV_ACCESSTOKEN); if (accessTokenEnv != null) { return resolver.resolvePathToFile(accessTokenEnv); } return resolver.resolvePathToFile("~/.cdap.accesstoken." + hostname); } private String tryGetVersion() { try { InputSupplier<? extends InputStream> versionFileSupplier = new InputSupplier<InputStream>() { @Override public InputStream getInput() throws IOException { return VersionCommand.class.getClassLoader().getResourceAsStream("VERSION"); } }; return CharStreams.toString(CharStreams.newReaderSupplier(versionFileSupplier, Charsets.UTF_8)); } catch (IOException e) { throw Throwables.propagate(e); } } public void setNamespace(Id.Namespace namespace) { setConnectionConfig(new CLIConnectionConfig(connectionConfig, namespace)); } public ClientConfig getClientConfig() { return clientConfig; } public CLIConnectionConfig getConnectionConfig() { return connectionConfig; } public String getVersion() { return version; } public void addHostnameChangeListener(ConnectionChangeListener listener) { this.connectionChangeListeners.add(listener); } private void notifyConnectionChanged() { for (ConnectionChangeListener listener : connectionChangeListeners) { listener.onConnectionChanged(connectionConfig); } } public void setLineWidth(Supplier<Integer> lineWidthSupplier) { this.lineWidthSupplier = lineWidthSupplier; } public int getLineWidth() { return Math.max(MIN_LINE_WIDTH, lineWidthSupplier.get()); } /** * Listener for hostname changes. */ public interface ConnectionChangeListener { void onConnectionChanged(CLIConnectionConfig config); } }