/** * Copyright 2015 Anaplan 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.md file for the specific language governing permissions and * limitations under the License. */ /** * Basic Anaplan Connection class that helps establish an API connection using * the provided credentials from the connector. * * Author: Spondon Saha */ package com.anaplan.connector.connection; import com.anaplan.client.AnaplanAPIException; import com.anaplan.client.Credentials; import com.anaplan.client.Service; import com.anaplan.client.Workspace; import com.anaplan.connector.AnaplanConnectorProperties; import com.anaplan.connector.exceptions.AnaplanConnectionException; import com.anaplan.connector.exceptions.ConnectorPropertiesException; import com.anaplan.connector.utils.UserMessages; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.BufferedInputStream; import java.io.FileInputStream; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.List; /** * Validation and communication with the Anaplan model. */ public class AnaplanConnection { private static final Logger logger = LogManager.getLogger( AnaplanConnection.class.getName()); private static final String USERNAME_FIELD = "username"; private static final String PASSWORD_FIELD = "password"; private static final String URL_FIELD = "url"; private static final String CERT_PATH = "certPath"; private static final String URL_PROXY = "proxyHost"; private static final String URL_PROXY_USER = "proxyUser"; private static final String URL_PROXY_PASS = "proxyPass"; private final AnaplanConnectorProperties connectionConfig; private final boolean isCertificate; // cached Anaplan objects when a valid open connection exists, else null private Service openConnection = null; /** * Constructor. * * @param isCertificate If set to true then just use certificate for * authentication. If false, then we are using basic authentication and * so just need username and password. * @param credentials String[] of credential values, which includes the * username, password, API url, certificate path and proxy URL and * their credentials if using this connector behind a firewall. */ public AnaplanConnection(boolean isCertificate, String... credentials) { logger.debug("NOTICE: {} @ {}", credentials[0], credentials[2]); this.isCertificate = isCertificate; connectionConfig = new AnaplanConnectorProperties(); try { if (isCertificate) connectionConfig.setProperties(credentials, CERT_PATH, URL_FIELD, URL_PROXY, URL_PROXY_USER, URL_PROXY_PASS); else connectionConfig.setProperties(credentials, USERNAME_FIELD, PASSWORD_FIELD, URL_FIELD, URL_PROXY, URL_PROXY_USER, URL_PROXY_PASS); } catch (ConnectorPropertiesException e) { logger.error("Could not set connector properties!", e); } logger.info("Stored connection properties!"); } /** * Getter for the connection ID, which is the string representation of this * object. * * @return Connection ID string. */ public String getConnectionId() { return this.toString(); } /** * Opens the DER encoded certificate from the provided path into an input * stream and then generates a X.509 certificate from the file contents and * returns it. To view the certificate contents on the command line, do: * $ openssl x509 -in /path/to/certificate/file.cer -inform der -text -noout * * @param certificateLocation Location on file system of login certificate. * @return X509Certificate object containing user certificate details. * @throws AnaplanConnectionException Thrown if any sort of Certificate * parsing exception occurs. */ public X509Certificate readCertificate(String certificateLocation) throws AnaplanConnectionException { BufferedInputStream buffStream = null; X509Certificate x509 = null; try { buffStream = new BufferedInputStream( new FileInputStream(certificateLocation)); Certificate cert = CertificateFactory.getInstance("X.509") .generateCertificate(buffStream); if (cert instanceof X509Certificate) { x509 = (X509Certificate) cert; logger.info("Certificate VALID!"); logger.debug(x509.toString()); } } catch (CertificateException e) { throw new AnaplanConnectionException("Bad certificate", e); } catch (IOException e) { throw new AnaplanConnectionException("Could not open certificate", e); } catch (Throwable e) { throw new AnaplanConnectionException("Unknown exception occurred", e); } finally { if (buffStream != null) { try { buffStream.close(); } catch (IOException e) { throw new AnaplanConnectionException(e.getMessage()); } } } return x509; } /** * Opens a service for accessing anaplan workspaces with this connection's * API endpoint and credentials. * @return The service object that contains workspace/model details for the * authenticated user. * @throws AnaplanConnectionException If there was an error with the service * or any or the required properties. */ private Service cacheService() throws AnaplanConnectionException { logger.debug("Trying Anaplan service connection..."); final String apiUrl = connectionConfig.getStringProperty(URL_FIELD); Service service; logger.debug("API Url: {}", apiUrl); try { service = new Service(new URI(apiUrl)); } catch (URISyntaxException e) { closeConnection(); throw new AnaplanConnectionException( UserMessages.getMessage("invalidApiUri", apiUrl), e); } // fetch all stored credentials and properties final String username = connectionConfig.getStringProperty(USERNAME_FIELD); final String password = connectionConfig.getStringProperty(PASSWORD_FIELD); final String certLocation = connectionConfig.getStringProperty(CERT_PATH); final String proxyHost = connectionConfig.getStringProperty(URL_PROXY); final String proxyUser = connectionConfig.getStringProperty(URL_PROXY_USER); final String proxyPass = connectionConfig.getStringProperty(URL_PROXY_PASS); Credentials creds; try { // read in the certificate if provided, or fallback to basic // authentication. if (isCertificate) creds = new Credentials(readCertificate(certLocation)); else creds = new Credentials(username, password, null, null); // Set the credentials on the service. service.setServiceCredentials(creds); // Set proxy credentials if provided. if (proxyHost != null && !proxyHost.isEmpty()) { service.setProxyLocation(new URI(proxyHost)); if (proxyUser != null && !proxyUser.isEmpty()) { service.setProxyCredentials(new Credentials(proxyUser, proxyPass, null, null)); logger.debug("Proxy server configured"); } } } catch (AnaplanAPIException | URISyntaxException e) { closeConnection(); final String msg = UserMessages.getMessage("apiConnectFail", e.getMessage()); logger.error(msg, e); throw new AnaplanConnectionException(msg, e); } logger.debug("Anaplan service connection information cached."); // validate username/password credentials by pulling workspaces for user List<Workspace> availableWorkspaces = null; try { availableWorkspaces = service.getWorkspaces(); } catch (AnaplanAPIException e) { closeConnection(); logger.error(e.getMessage(), e); if (e.getMessage() == null || !e.getMessage().toLowerCase().contains("credentials")) { String exceptionDetails = e.getMessage(); if (e.getCause() != null && e.getCause().getMessage() != null) { exceptionDetails = exceptionDetails + " (" + e.getCause().getMessage() + ")"; } throw new AnaplanConnectionException(exceptionDetails, e); } // else handle credentials issues below } if (availableWorkspaces == null || availableWorkspaces.isEmpty()) { final String msg = UserMessages.getMessage("accessFail"); logger.error("{} (availableWorkspaces={})", msg, availableWorkspaces); throw new AnaplanConnectionException(msg); } logger.debug("Anaplan service connection validated successfully"); openConnection = service; return service; } /** * Note that model's associated service object is left open for model * access. Service should be closed by caller when access complete using * {@link #closeConnection()}. * * If any open connection already exists it will be closed. * * @return null if workspace or model is not valid, else anaplan model * object * @throws AnaplanConnectionException * With user-friendly message if the model cannot be opened. */ public Service openConnection() throws AnaplanConnectionException { logger.info("Establishing connection...."); if (openConnection == null) { logger.info("No new connection found, establishing new connection!"); return cacheService(); } else { logger.info("Connection exists, returning cached connection!"); return this.openConnection; } } /** * Getter for retrieving the open-connection, which is the service object * to use for querying workspace and model details. * @return */ public Service getConnection() { return this.openConnection; } /** * Closes the open-connection if one exists. */ public void closeConnection() { if (openConnection != null) { openConnection.close(); } openConnection = null; logger.info("Connection closed."); } }