/******************************************************************************* * Copyright (c) 2015 Red Hat, Inc. * Distributed under license by Red Hat, Inc. All rights reserved. * This program is made available under the terms of the * Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Red Hat, Inc. - initial API and implementation ******************************************************************************/ package org.jboss.tools.openshift.core.connection; import java.io.IOException; import java.net.MalformedURLException; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import org.apache.commons.lang.ObjectUtils; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.equinox.security.storage.StorageException; import org.eclipse.osgi.util.NLS; import org.jboss.tools.common.databinding.ObservablePojo; import org.jboss.tools.openshift.common.core.ICredentialsPrompter; import org.jboss.tools.openshift.common.core.IRefreshable; import org.jboss.tools.openshift.common.core.connection.ConnectionType; import org.jboss.tools.openshift.common.core.connection.ConnectionURL; import org.jboss.tools.openshift.common.core.connection.IConnection; import org.jboss.tools.openshift.common.core.utils.StringUtils; import org.jboss.tools.openshift.common.core.utils.UrlUtils; import org.jboss.tools.openshift.core.ICommonAttributes; import org.jboss.tools.openshift.core.preferences.OpenShiftCorePreferences; import org.jboss.tools.openshift.internal.common.core.UsageStats; import org.jboss.tools.openshift.internal.common.core.security.OpenShiftSecureStorageKey; import org.jboss.tools.openshift.internal.common.core.security.SecureStore; import org.jboss.tools.openshift.internal.common.core.security.SecureStoreException; import org.jboss.tools.openshift.internal.core.OpenShiftCoreActivator; import org.jboss.tools.openshift.internal.core.util.ResourceUtils; import com.openshift.restclient.ClientBuilder; import com.openshift.restclient.IClient; import com.openshift.restclient.IResourceFactory; import com.openshift.restclient.ISSLCertificateCallback; import com.openshift.restclient.NotFoundException; import com.openshift.restclient.OpenShiftException; import com.openshift.restclient.authorization.IAuthorizationContext; import com.openshift.restclient.authorization.UnauthorizedException; import com.openshift.restclient.capability.ICapability; import com.openshift.restclient.model.IResource; import com.openshift.restclient.model.IResourceBuilder; public class Connection extends ObservablePojo implements IRefreshable, IOpenShiftConnection { public static final String SECURE_STORAGE_BASEKEY = "org.jboss.tools.openshift.core"; public static final String SECURE_STORAGE_PASSWORD_KEY = "password"; public static final String SECURE_STORAGE_TOKEN_KEY = "token"; private IClient client; private boolean passwordLoaded = false; private boolean tokenLoaded = false; private boolean rememberPassword; private boolean rememberToken; private boolean promptCredentialsEnabled = true; private ICredentialsPrompter credentialsPrompter; private String authScheme; private Map<String, Object> extendedProperties = new HashMap<>(); //TODO modify default client to take url and throw lib specific exception public Connection(String url, ICredentialsPrompter credentialsPrompter, ISSLCertificateCallback sslCertCallback) throws MalformedURLException { this(new ClientBuilder(url).sslCertificateCallback(sslCertCallback).build(), credentialsPrompter); } public Connection(IClient client, ICredentialsPrompter credentialsPrompter) { this.client = client; this.credentialsPrompter = credentialsPrompter; } @Override public Map<String, Object> getExtendedProperties() { return new HashMap<>(extendedProperties); } @Override public void setExtendedProperty(String name, Object value) { Map<String, Object> oldExt = new HashMap<>(extendedProperties); extendedProperties.put(name, value); firePropertyChange(PROPERTY_EXTENDED_PROPERTIES, oldExt, this.extendedProperties); } @Override public void setExtendedProperties(Map<String, Object> ext) { firePropertyChange(PROPERTY_EXTENDED_PROPERTIES, this.extendedProperties, this.extendedProperties = ext); } @Override public String getClusterNamespace() { return (String) getExtendedProperties().getOrDefault(ICommonAttributes.CLUSTER_NAMESPACE_KEY, ICommonAttributes.COMMON_NAMESPACE); } /** * Retrieve the resource factory associated with this connection * for stubbing versioned resources supported by th server * @return an {@link IResourceFactory} */ public IResourceFactory getResourceFactory() { return client.getResourceFactory(); } @SuppressWarnings({ "rawtypes", "unchecked" }) public <B extends IResourceBuilder> B getResourceBuilder(Class<? extends ICapability> klass){ if(client.supports(klass)) { ICapability cap = client.getCapability(klass); if(cap instanceof IResourceBuilder) { return (B) cap; } } return null; } @Override public String getUsername(){ return client.getAuthorizationContext().getUserName(); } @Override public void setUsername(String userName) { IAuthorizationContext authContext = client.getAuthorizationContext(); String old = authContext.getUserName(); authContext.setUserName(userName); firePropertyChange(PROPERTY_USERNAME, old, userName); } @Override public String getPassword() { return loadAuthorizationContext().getPassword(); } @Override public void setPassword(String password) { IAuthorizationContext authContext = client.getAuthorizationContext(); String old = authContext.getPassword(); authContext.setPassword(password); firePropertyChange(PROPERTY_PASSWORD, old, password); this.passwordLoaded = true; } @Override public void setRememberPassword(boolean rememberPassword) { firePropertyChange(PROPERTY_REMEMBER_PASSWORD, this.rememberPassword, this.rememberPassword = rememberPassword); } @Override public boolean isRememberPassword() { return rememberPassword; } public boolean isRememberToken() { return rememberToken; } public void setRememberToken(boolean rememberToken) { firePropertyChange(PROPERTY_REMEMBER_TOKEN, this.rememberToken, this.rememberToken = rememberToken); } @Override public void enablePromptCredentials(boolean enable) { this.promptCredentialsEnabled = enable; } @Override public boolean isEnablePromptCredentials() { return promptCredentialsEnabled; } public String getAuthScheme() { return org.apache.commons.lang.StringUtils.defaultIfBlank(this.authScheme, IAuthorizationContext.AUTHSCHEME_OAUTH); } protected String load(String id) { String value = null; SecureStore store = getSecureStore(getHost(), getUsername()); if (store != null) { try { value = store.get(id); } catch (SecureStoreException e) { OpenShiftCoreActivator.pluginLog().logError(e.getMessage(), e); } } return value; } private boolean saveOrClear(String id, String value, boolean saveOrClear) { SecureStore store = getSecureStore(getHost(), getUsername()); if (store != null) { try { if (saveOrClear && !StringUtils.isEmpty(value)) { store.put(id, value); } else { store.remove(id); } } catch (SecureStoreException e) { firePropertyChange(SecureStoreException.ID, null, e); OpenShiftCoreActivator.logError(NLS.bind("Exception saving {0} for connection to {1}",id, getHost()), e); if(e.getCause() instanceof StorageException) { return false; } } } return true; } /** * Returns a secure store for the current host and username */ protected SecureStore getSecureStore(String host, String username) { return new SecureStore(new OpenShiftSecureStorageKey(SECURE_STORAGE_BASEKEY, host, username)); } @Override public boolean connect() throws OpenShiftException { if(authorize()) { savePasswordOrToken(); saveAuthSchemePreference(); return true; } return false; } protected boolean authorize() { try { IAuthorizationContext context = loadAuthorizationContext(); if (!context.isAuthorized() && credentialsPrompter != null && promptCredentialsEnabled){ credentialsPrompter.promptAndAuthenticate(this, null); } else { updateCredentials(context); } } catch (UnauthorizedException e) { if (isEnablePromptCredentials() && credentialsPrompter != null) { credentialsPrompter.promptAndAuthenticate(this, e.getAuthorizationDetails()); } else { throw e; } } return getToken() != null; } private IAuthorizationContext loadAuthorizationContext() { IAuthorizationContext context = client.getAuthorizationContext(); synchronized (context) { if(!passwordLoaded) { setPassword(load(SECURE_STORAGE_PASSWORD_KEY)); setRememberPassword(context.getPassword() != null); } if(!tokenLoaded) { setToken(load(SECURE_STORAGE_TOKEN_KEY)); setRememberToken(context.getToken() != null); //potential conflict with load password? } } return context; } private void savePasswordOrToken() { // not using getters here because for save there should be no reason // to trigger a load from storage. if (IAuthorizationContext.AUTHSCHEME_BASIC.equals(getAuthScheme())) { boolean success = saveOrClear(SECURE_STORAGE_PASSWORD_KEY, client.getAuthorizationContext().getPassword(), isRememberPassword()); if (success) { //Avoid second secure storage prompt. // Password is stored, token should be cleared. clearToken(); } } else if (IAuthorizationContext.AUTHSCHEME_OAUTH.equals(getAuthScheme())){ boolean success = saveOrClear(SECURE_STORAGE_TOKEN_KEY, client.getAuthorizationContext().getToken(), isRememberToken()); if(success) { //Avoid second secure storage prompt. //Token is stored, password should be cleared. clearPassword(); } } } private void clearPassword() { client.getAuthorizationContext().setPassword(null); setRememberPassword(false); saveOrClear(SECURE_STORAGE_PASSWORD_KEY, null, false); } private void clearToken() { // dont clear the token instance var: JBIDE-22594 setRememberToken(false); saveOrClear(SECURE_STORAGE_TOKEN_KEY, null, false); } public void removeSecureStoreData() { SecureStore store = getSecureStore(getHost(), getUsername()); if (store != null) { try { store.removeNode(); } catch (SecureStoreException e) { firePropertyChange(SecureStoreException.ID, null, e); OpenShiftCoreActivator.logWarning(e.getMessage(), e); } } } protected void saveAuthSchemePreference() { ConnectionURL url = ConnectionURL.safeForConnection(this); if(!StringUtils.isEmpty(url)) { OpenShiftCorePreferences.INSTANCE.saveAuthScheme(url.toString(), getAuthScheme()); } } private void updateCredentials(IAuthorizationContext context) { setToken(context.getToken()); if (IAuthorizationContext.AUTHSCHEME_OAUTH.equalsIgnoreCase(getAuthScheme())) { setUsername(context.getUser().getName()); } } /** * Computes authorization state of connection. May be a long running operation. * @return */ public boolean isAuthorized(IProgressMonitor monitor) { try { IAuthorizationContext context = client.getAuthorizationContext(); boolean result = context.isAuthorized(); if(result) { //Call connect() to set the correct strategy instance to the client //in the case when no strategy has been set yet, and as we do it, //we can discard the current result, which nevertheless //being true, promises that connect will go smoothly. return connect(); } return result; } catch (UnauthorizedException e) { return false; } } public void setAuthScheme(String scheme) { firePropertyChange(PROPERTY_AUTHSCHEME, this.authScheme, this.authScheme = scheme); } @Override public String getHost() { return client.getBaseURL().toString(); } @Override public boolean isDefaultHost() { // TODO: implement return false; } @Override public String getScheme() { return client.getBaseURL().getProtocol() + UrlUtils.SCHEME_SEPARATOR; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((client == null) ? 0 : client.getBaseURL().toString().hashCode()); if(client != null) { result = prime * result + ((client.getAuthorizationContext().getUserName() == null) ? 0 : client.getAuthorizationContext().getUserName().hashCode()); } return result; } @Override public IConnection clone() { IClient clone = client.clone(); Connection connection = new Connection(clone, credentialsPrompter); connection.passwordLoaded = this.passwordLoaded; connection.tokenLoaded = this.tokenLoaded; connection.rememberPassword = this.rememberPassword; connection.rememberToken = this.rememberToken; connection.promptCredentialsEnabled = promptCredentialsEnabled; return connection; } @Override public void update(IConnection connection) { Assert.isLegal(connection instanceof Connection); Connection otherConnection = (Connection) connection; this.client = otherConnection.client; this.credentialsPrompter = otherConnection.credentialsPrompter; this.rememberToken = otherConnection.rememberToken; this.rememberPassword = otherConnection.rememberPassword; this.tokenLoaded = otherConnection.tokenLoaded; this.rememberPassword = otherConnection.rememberPassword; IAuthorizationContext otherContext = otherConnection.client.getAuthorizationContext(); IAuthorizationContext context = this.client.getAuthorizationContext(); context.setUserName(otherContext.getUserName()); context.setPassword(otherContext.getPassword()); context.setToken(otherContext.getToken()); } @Override public void refresh() { connect(); } @Override public ConnectionType getType() { return ConnectionType.Kubernetes; } @Override public String toString() { return client.getBaseURL().toString(); } /** * * @param resource * @return * @throws UnauthorizedException */ public <T extends IResource> T createResource(T resource) { try { return client.create(resource); } catch (UnauthorizedException e) { return retryCreate(e, resource); } } /** * * @param resource * @return * @throws UnauthorizedException */ public <T extends IResource> T updateResource(T resource) { try { return client.update(resource); } catch (UnauthorizedException e) { return retryUpdate(e, resource); } } /** * Get a list of resource types in the default namespace * * @return List<IResource> * @throws OpenShiftException */ @Override public <T extends IResource> List<T> getResources(String kind) { return getResources(kind,""); } @Override public <T extends IResource> List<T> getResources(String kind, String namespace) { try { return client.list(kind, namespace); } catch (UnauthorizedException e) { return retryList(e, kind, namespace); } } @Override public <T extends IResource> T getResource(String kind, String namespace, String name) { try { return client.get(kind, name, namespace); } catch (UnauthorizedException e) { return retryGet(e, kind, name, namespace); } } /** * Get or refresh a resource * * @return a <IResource> * @throws OpenShiftException */ @Override public <T extends IResource> T refresh(IResource resource) { try { return client.get(resource.getKind(), resource.getName(), resource.getNamespace()); } catch (UnauthorizedException e) { return retryGet(e, resource.getKind(), resource.getName(), resource.getNamespace()); } } private <T extends IResource> T retryGet(OpenShiftException e, String kind, String name, String namespace){ setToken(null);// token must be invalid, make sure not to try with // cache if (connect()) { return client.get(kind, name, namespace); } throw e; } private <T extends IResource> T retryCreate(OpenShiftException e, T resource){ setToken(null);// token must be invalid, make sure not to try with // cache if (connect()) { return client.create(resource); } throw e; } private <T extends IResource> T retryUpdate(OpenShiftException e, T resource){ setToken(null);// token must be invalid, make sure not to try with // cache if (connect()) { return client.update(resource); } throw e; } private <T extends IResource> List<T> retryList(OpenShiftException e, String kind, String namespace){ setToken(null);// token must be invalid, make sure not to try with // cache if (connect()) { return client.list(kind, namespace); } throw e; } /** * Delete the resource from the namespace it is associated with. The delete operation * return silently regardless if successful or not * * @param resource * @throws OpenShiftException */ public void deleteResource(IResource resource) { client.delete(resource); } @Override public boolean canConnect() throws IOException { try { client.getOpenShiftAPIVersion(); return true; } catch (NotFoundException e) { return false; } } public String getToken() { return loadAuthorizationContext().getToken(); } public void setToken(String token) { IAuthorizationContext context = client.getAuthorizationContext(); String old = context.getToken(); context.setToken(token); firePropertyChange(SECURE_STORAGE_TOKEN_KEY, old, token); this.tokenLoaded = true; } @Override public void notifyUsage() { UsageStats.getInstance().newV3Connection(getHost()); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Connection other = (Connection) obj; if (client == null) { if (other.client != null) return false; } else if (other.client == null || !client.getBaseURL().toString().equals(other.client.getBaseURL().toString())) return false; if (client.getAuthorizationContext().getUserName() == null) { if (other.client.getAuthorizationContext().getUserName() != null) return false; } else if (!client.getAuthorizationContext().getUserName().equals(other.client.getAuthorizationContext().getUserName())) return false; return true; } @Override public boolean credentialsEqual(IConnection connection) { if(!equals(connection)) { return false; } //It is safe to cast now. Connection other = (Connection)connection; //User name is already compared if(!Objects.equals(client.getAuthorizationContext().getPassword(), other.client.getAuthorizationContext().getPassword())) { return false; } if(!Objects.equals(client.getAuthorizationContext().getToken(), other.client.getAuthorizationContext().getToken())) { return false; } return true; } public boolean ownsResource(IResource resource) { if (resource == null) { return false; } return ObjectUtils.equals(this.client, ResourceUtils.getClient(resource)); } }