/*******************************************************************************
* Copyright (c) 2015, 2016 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are 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:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.dash.cloudfoundry;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.equinox.security.storage.StorageException;
import org.eclipse.jface.operation.IRunnableContext;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFCredentials;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFExceptions;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFSpace;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.ClientRequests;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CloudFoundryClientFactory;
import org.springframework.ide.eclipse.boot.dash.cloudfoundry.ops.Operation;
import org.springframework.ide.eclipse.boot.dash.dialogs.PasswordDialogModel;
import org.springframework.ide.eclipse.boot.dash.dialogs.StoreCredentialsMode;
import org.springframework.ide.eclipse.boot.dash.model.BootDashModelContext;
import org.springframework.ide.eclipse.boot.dash.model.RunTarget;
import org.springframework.ide.eclipse.boot.dash.model.WizardModelUserInteractions;
import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.CannotAccessPropertyException;
import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.RunTargetType;
import org.springframework.ide.eclipse.boot.dash.model.runtargettypes.TargetProperties;
import org.springframework.ide.eclipse.boot.util.Log;
import org.springsource.ide.eclipse.commons.livexp.core.CompositeValidator;
import org.springsource.ide.eclipse.commons.livexp.core.LiveExpression;
import org.springsource.ide.eclipse.commons.livexp.core.LiveVariable;
import org.springsource.ide.eclipse.commons.livexp.core.ValidationResult;
import org.springsource.ide.eclipse.commons.livexp.core.Validator;
import org.springsource.ide.eclipse.commons.livexp.core.ValueListener;
import org.springsource.ide.eclipse.commons.livexp.ui.Ilabelable;
import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil;
import com.google.common.collect.ImmutableSet;
import reactor.core.publisher.Flux;
/**
* Cloud Foundry Target properties that uses {@link LiveExpression} and
* {@link Validator}.
*/
public class CloudFoundryTargetWizardModel {
public enum LoginMethod implements Ilabelable {
PASSWORD,
TEMPORARY_CODE;
@Override
public String getLabel() {
String[] pieces = name().split("_");
StringBuilder label = new StringBuilder();
for (int i = 0; i < pieces.length; i++) {
if (i>0) {
label.append(" ");
}
label.append(StringUtils.capitalize(pieces[i].toLowerCase()));
}
return label.toString();
}
}
private RunTargetType runTargetType;
private BootDashModelContext context;
private LiveVariable<String> url = new LiveVariable<>();
private LiveVariable<CFSpace> space = new LiveVariable<>();
private LiveVariable<Boolean> selfsigned = new LiveVariable<>(false);
private LiveVariable<Boolean> skipSslValidation = new LiveVariable<>(false);
private LiveVariable<LoginMethod> method = new LiveVariable<>(LoginMethod.PASSWORD);
private LiveVariable<String> userName = new LiveVariable<>();
private LiveVariable<String> password = new LiveVariable<>();
private LiveVariable<StoreCredentialsMode> storeCredentials = new LiveVariable<>(StoreCredentialsMode.STORE_NOTHING);
private LiveVariable<ValidationResult> spaceResolutionStatus = new LiveVariable<>(ValidationResult.OK); // has an error if resolution failed.
private LiveVariable<OrgsAndSpaces> resolvedSpaces = new LiveVariable<>();
private String refreshToken = null;
private Validator credentialsValidator = new CredentialsValidator();
private Validator spaceValidator = new CloudSpaceValidator();
private Validator resolvedSpacesValidator = new ResolvedSpacesValidator();
private Validator storeCredentialsValidator = PasswordDialogModel.makeStoreCredentialsValidator(method, storeCredentials);
private CompositeValidator allPropertiesValidator = new CompositeValidator();
private CloudFoundryClientFactory clientFactory;
private ImmutableSet<RunTarget> existingTargets;
private WizardModelUserInteractions interactions;
public CloudFoundryTargetWizardModel(RunTargetType runTargetType, CloudFoundryClientFactory clientFactory,
ImmutableSet<RunTarget> existingTargets, BootDashModelContext context) {
this(runTargetType, clientFactory, existingTargets, context, null);
}
public CloudFoundryTargetWizardModel(RunTargetType runTargetType, CloudFoundryClientFactory clientFactory,
ImmutableSet<RunTarget> existingTargets, BootDashModelContext context, WizardModelUserInteractions interactions) {
this.runTargetType = runTargetType;
this.context = context;
Assert.isNotNull(clientFactory, "clientFactory should not be null");
this.interactions = interactions;
this.existingTargets = existingTargets == null ? ImmutableSet.<RunTarget>of() : existingTargets;
this.clientFactory = clientFactory;
// The credentials validator should be notified any time there are
// changes
// to url, username, password and selfsigned setting.
credentialsValidator.dependsOn(url);
credentialsValidator.dependsOn(selfsigned);
credentialsValidator.dependsOn(userName);
credentialsValidator.dependsOn(password);
credentialsValidator.dependsOn(method);
// Spaces validator is notified when there are changes to the space
// variable. This is a separate validator as space validation and spave
// value setting may only occur AFTER ALL credentials/URL are entered or
// validated, and different listeners may need to be registered for
// credential validation
// vs space validation
spaceValidator.dependsOn(space);
resolvedSpacesValidator.dependsOn(spaceResolutionStatus);
resolvedSpacesValidator.dependsOn(resolvedSpaces);
// Aggregate of the credentials and space validators.
allPropertiesValidator.addChild(credentialsValidator);
allPropertiesValidator.addChild(storeCredentialsValidator);
allPropertiesValidator.addChild(resolvedSpacesValidator);
allPropertiesValidator.addChild(spaceValidator);
url.setValue(getDefaultTargetUrl());
}
/**
* @param allPropertiesValidationListener
* listener that is notified when any property is validated
*/
public void addAllPropertiesListener(ValueListener<ValidationResult> allPropertiesValidationListener) {
allPropertiesValidator.addListener(allPropertiesValidationListener);
}
public void removeAllPropertiesListeners(ValueListener<ValidationResult> allPropertiesValidationListener) {
allPropertiesValidator.removeListener(allPropertiesValidationListener);
}
public void setUrl(String url) {
this.url.setValue(url);
}
public void setSelfsigned(boolean selfsigned) {
this.selfsigned.setValue(selfsigned);
}
public void skipSslValidation(boolean skipSsl) {
this.skipSslValidation.setValue(skipSsl);
}
public void setUsername(String userName) {
this.userName.setValue(userName);
}
public void setPassword(String password) throws CannotAccessPropertyException {
this.password.setValue(password);
}
public void setSpace(CFSpace space) {
this.space.setValue(space);
}
public String getPassword() throws CannotAccessPropertyException {
return password.getValue();
}
public String getUrl() {
return url.getValue();
}
public String getUsername() {
return userName.getValue();
}
public void setStoreCredentials(StoreCredentialsMode store) {
storeCredentials.setValue(store);
}
public StoreCredentialsMode getStoreCredentials() {
return storeCredentials.getValue();
}
protected String getDefaultTargetUrl() {
return "https://api.run.pivotal.io";
}
public OrgsAndSpaces resolveSpaces(IRunnableContext context) {
try {
boolean toFetchSpaces = true;
OrgsAndSpaces spaces = getCloudSpaces(createTargetProperties(toFetchSpaces), context);
resolvedSpaces.setValue(spaces);
spaceResolutionStatus.setValue(ValidationResult.OK);
return resolvedSpaces.getValue();
} catch (Exception e) {
if (CFExceptions.isAuthFailure(e) || CFExceptions.isSSLCertificateFailure(e)) {
//don't log, its expected if user just typed bad password,
//or didn't check ssl box when they should have.
} else {
Log.log(e);
}
resolvedSpaces.setValue(null);
spaceResolutionStatus.setValue(ValidationResult.error(ExceptionUtil.getMessage(e)));
return null;
}
}
private OrgsAndSpaces getCloudSpaces(final CloudFoundryTargetProperties targetProperties, IRunnableContext context)
throws Exception {
OrgsAndSpaces spaces = null;
Operation<List<CFSpace>> op = new Operation<List<CFSpace>>(
"Connecting to the Cloud Foundry target. Please wait while the list of spaces is resolved...") {
protected List<CFSpace> runOp(IProgressMonitor monitor) throws Exception, OperationCanceledException {
ClientRequests client = clientFactory.getClient(targetProperties);
List<CFSpace> spaces = client.getSpaces();
String t = client.getRefreshToken();
if (t!=null) {
refreshToken = t;
}
String effectiveUser = client.getUserName().block();
if (effectiveUser!=null) {
userName.setValue(effectiveUser);
}
return spaces;
}
};
List<CFSpace> actualSpaces = op.run(context, true);
if (actualSpaces != null && !actualSpaces.isEmpty()) {
spaces = new OrgsAndSpaces(actualSpaces);
}
return spaces;
}
/**
* Create target properties based on current input values in the wizard.
* <p>
* Note that there are two slightly different ways to produce these properties.
* <p>
* a) to create a intermediate client just to fetch orgs and spaces.
* <p>
* b) the final properties used to create the client after space is selected and the user
* clicks 'finish' button.
*/
private CloudFoundryTargetProperties createTargetProperties(boolean toFetchSpaces) throws CannotAccessPropertyException {
CloudFoundryTargetProperties targetProps = new CloudFoundryTargetProperties(runTargetType, context);
if (!toFetchSpaces) {
//Take care: when fetching spaces the space may not be known yet, so neither is the id
String id = CloudFoundryTargetProperties.getId(
this.getUsername(),
this.getUrl(),
this.getOrganizationName(),
this.getSpaceName()
);
targetProps.put(TargetProperties.RUN_TARGET_ID, id);
}
targetProps.setUrl(url.getValue());
targetProps.setSelfSigned(selfsigned.getValue());
targetProps.setSkipSslValidation(skipSslValidation.getValue());
targetProps.setUserName(userName.getValue());
if (toFetchSpaces) {
targetProps.setStoreCredentials(StoreCredentialsMode.STORE_NOTHING);
targetProps.setCredentials(CFCredentials.fromLogin(method.getValue(), password.getValue()));
} else {
//use credentials of a style that is consistent with the 'store mode'.
if (method.getValue()==LoginMethod.TEMPORARY_CODE && storeCredentials.getValue()==StoreCredentialsMode.STORE_PASSWORD) {
//The one token shouldn't be stored since its meaningless. Silently downgrade storemode to store
storeCredentials.setValue(StoreCredentialsMode.STORE_NOTHING);
}
StoreCredentialsMode mode = storeCredentials.getValue();
targetProps.setStoreCredentials(storeCredentials.getValue());
switch (mode) {
case STORE_NOTHING:
case STORE_TOKEN:
Assert.isTrue(refreshToken!=null);
targetProps.setCredentials(CFCredentials.fromRefreshToken(refreshToken));
break;
case STORE_PASSWORD:
targetProps.setCredentials(CFCredentials.fromPassword(password.getValue()));
break;
default:
throw new IllegalStateException("BUG: Missing switch case?");
}
}
targetProps.setSpace(space.getValue());
return targetProps;
}
public OrgsAndSpaces getSpaces() {
return resolvedSpaces.getValue();
}
protected RunTarget getExistingRunTarget(CFSpace space) {
if (space != null) {
String targetId = CloudFoundryTargetProperties.getId(getUsername(), getUrl(),
space.getOrganization().getName(), space.getName());
for (RunTarget target : existingTargets) {
if (targetId.equals(target.getId())) {
return target;
}
}
}
return null;
}
public CloudFoundryRunTarget finish() throws Exception {
CloudFoundryTargetProperties targetProps = null;
try {
targetProps = createTargetProperties(/*toFetchSpaces*/false);
} catch (Exception e) {
final StorageException storageException = getStorageException(e);
// Allow run target to be created on storage exceptions as the run target can still be created and connected
if (storageException != null) {
Log.log(storageException);
if (interactions != null) {
String message = "Failed to store credentials in secure storage. Please check your secure storage preferences. Error: "
+ storageException.getMessage();
interactions.informationPopup("Secure Storage Error", message);
}
storeCredentials.setValue(StoreCredentialsMode.STORE_NOTHING);
targetProps = createTargetProperties(/*toFetchSpaces*/false);
} else {
throw e;
}
}
return (CloudFoundryRunTarget) runTargetType.createRunTarget(targetProps);
}
public String getSpaceName() {
CFSpace space = this.space.getValue();
if (space!=null) {
return space.getName();
}
return null;
}
public String getOrganizationName() {
CFSpace space = this.space.getValue();
if (space!=null) {
return space.getOrganization().getName();
}
return null;
}
protected StorageException getStorageException(Exception e) {
if (e instanceof StorageException) {
return (StorageException) e;
}
if (e.getCause() instanceof StorageException) {
return (StorageException) e.getCause();
}
return null;
}
class CredentialsValidator extends Validator {
@Override
protected ValidationResult compute() {
if (isEmpty(userName.getValue()) && method.getValue()==LoginMethod.PASSWORD) {
return ValidationResult.info("Enter a username");
} else if (isEmpty(url.getValue())) {
try {
new URL(url.getValue());
return ValidationResult.info("Enter a target URL");
} catch (MalformedURLException e) {
return ValidationResult.error(e.getMessage());
}
} else if (method.getValue()==LoginMethod.PASSWORD) {
if (isEmpty(password.getValue())) {
return ValidationResult.info("Enter a password");
}
} else if (method.getValue()==LoginMethod.TEMPORARY_CODE) {
if (isEmpty(password.getValue())) {
return ValidationResult.info("Enter a Temporary Access Code");
}
}
return ValidationResult.OK;
}
protected boolean isEmpty(String value) {
return value == null || value.trim().length() == 0;
}
}
class CloudSpaceValidator extends Validator {
@Override
protected ValidationResult compute() {
if (getSpaceName() == null || getOrganizationName() == null) {
return ValidationResult.info("Select a Cloud space");
}
if (space.getValue() != null) {
RunTarget existing = CloudFoundryTargetWizardModel.this.getExistingRunTarget(space.getValue());
if (existing != null) {
return ValidationResult.error("A run target for that space already exists: '" + existing.getName()
+ "'. Please select another space.");
}
}
return ValidationResult.OK;
}
}
class ResolvedSpacesValidator extends Validator {
@Override
protected ValidationResult compute() {
ValidationResult resolveStatus = spaceResolutionStatus.getValue();
if (!resolveStatus.isOk()) {
return resolveStatus;
}
if (resolvedSpaces.getValue() == null || resolvedSpaces.getValue().getAllSpaces() == null) {
return ValidationResult.info("Select a space to validate the credentials.");
}
if (resolvedSpaces.getValue().getAllSpaces().isEmpty()) {
return ValidationResult.error(
"No spaces available to select. Please check that the credentials and target URL are correct, and spaces are defined in the target.");
}
return ValidationResult.OK;
}
}
/**
* @return A 'complete' validator that reflects the validation state of all the inputs in this 'ui'.
*/
public LiveExpression<ValidationResult> getValidator() {
return allPropertiesValidator;
}
public String getRefreshToken() {
return refreshToken;
}
public LiveVariable<LoginMethod> getMethodVar() {
return method;
}
public LiveVariable<String> getUserNameVar() {
return userName;
}
public LiveVariable<String> getPasswordVar() {
return password;
}
public LiveVariable<StoreCredentialsMode> getStoreVar() {
return storeCredentials;
}
public LiveVariable<Boolean> getSkipSslVar() {
return skipSslValidation;
}
public LiveVariable<String> getUrlVar() {
return url;
}
public LiveVariable<CFSpace> getSpaceVar() {
return space;
}
public LiveExpression<Boolean> getEnableSpacesUI() {
return credentialsValidator.apply((r) -> r.isOk());
}
public LiveExpression<Boolean> getEnableUserName() {
return method.apply((method) -> method==LoginMethod.PASSWORD);
}
public LiveExpression<ValidationResult> getSpaceValidator() {
return spaceValidator;
}
public LiveExpression<ValidationResult> getResolvedSpacesValidator() {
return resolvedSpacesValidator;
}
public LiveExpression<ValidationResult> getCredentialsValidator() {
return credentialsValidator;
}
public void setMethod(LoginMethod v) {
method.setValue(v);
}
public Validator getStoreCredentialsValidator() {
return storeCredentialsValidator;
}
}