/******************************************************************************* * Copyright (c) 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.dialogs; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.CloudFoundryTargetProperties; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.CloudFoundryTargetWizardModel; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.CloudFoundryTargetWizardModel.LoginMethod; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFClientParams; import org.springframework.ide.eclipse.boot.dash.cloudfoundry.client.CFCredentials; 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.model.runtargettypes.TargetProperties; import org.springframework.ide.eclipse.editor.support.util.StringUtil; 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.ui.OkButtonHandler; import org.springsource.ide.eclipse.commons.livexp.util.ExceptionUtil; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoSink; /** * Password dialog model. Provides ability to specify password and whether it * needs to be stored. * * @author Alex Boyko * @author Kris De Volder */ public class PasswordDialogModel implements OkButtonHandler { private static final ValidationResult REQUEST_VALIDATION_MESSAGE = ValidationResult.info( "Click the 'Validate' button to verify the credentials."); private static final ValidationResult VALIDATION_IN_PROGRESS_MESSAGE = ValidationResult.info( "Please wait. Concacting CF to verify the credentials..." ); private CloudFoundryTargetProperties currentParams; private CloudFoundryClientFactory clientFactory; private final LiveVariable<String> refreshToken = new LiveVariable<>(); //This is set when credentials are succesfully validated. private final LiveVariable<Boolean> needValidationRequest = new LiveVariable<>(true); private LiveVariable<ValidationResult> credentialsValidationResult; final private LiveVariable<LoginMethod> fMethod; final private LiveVariable<String> fPasswordVar; final private LiveVariable<StoreCredentialsMode> fStoreVar; private boolean okButtonPressed = false; private Validator passwordValidator; private LiveExpression<ValidationResult> storeValidator; private <T> void credentialsChangedHandler(LiveExpression<T> exp, T value) { needValidationRequest.setValue(true); } public PasswordDialogModel(CloudFoundryClientFactory cfFactory, CloudFoundryTargetProperties currentParams, StoreCredentialsMode storeMode) { super(); this.clientFactory = cfFactory; this.currentParams = currentParams; fPasswordVar = new LiveVariable<>(""); fStoreVar = new LiveVariable<>(storeMode); fMethod = new LiveVariable<>(LoginMethod.PASSWORD); storeValidator = makeStoreCredentialsValidator(getMethodVar(), fStoreVar); credentialsValidationResult = new LiveVariable<>(REQUEST_VALIDATION_MESSAGE); fMethod.addListener(this::credentialsChangedHandler); fPasswordVar.addListener(this::credentialsChangedHandler); } public String getUser() { return currentParams.getUsername(); } public String getTargetId() { return CloudFoundryTargetProperties.getId(currentParams); } public LiveVariable<String> getPasswordVar() { return fPasswordVar; } public LiveVariable<StoreCredentialsMode> getStoreVar() { return fStoreVar; } public boolean isOk() { return okButtonPressed; } @Override public void performOk() throws Exception { okButtonPressed = true; } public LiveExpression<ValidationResult> getPasswordValidator() { if (passwordValidator==null) { passwordValidator = new Validator() { { dependsOn(fPasswordVar); dependsOn(needValidationRequest); dependsOn(credentialsValidationResult); } @Override protected ValidationResult compute() { String pw = fPasswordVar.getValue(); if (!StringUtil.hasText(pw)) { return ValidationResult.error("Password can not be empty"); } if (needValidationRequest.getValue()) { return REQUEST_VALIDATION_MESSAGE; } return credentialsValidationResult.getValue(); } }; } return passwordValidator; } public LiveExpression<ValidationResult> getStoreValidator() { return storeValidator; } /** * Determines the 'effective' StoreCredentialsMode. This may be different * from what the user explicitly chose. If the user choice is 'invalid' * we ignore it (with a warning) and replace it with STORE_NOTHING. */ public StoreCredentialsMode getEffectiveStoreMode() { if (storeValidator.getValue().isOk()) { return fStoreVar.getValue(); } return StoreCredentialsMode.STORE_NOTHING; } public static Validator makeStoreCredentialsValidator(LiveExpression<CloudFoundryTargetWizardModel.LoginMethod> method, LiveExpression<StoreCredentialsMode> storeCredentials ) { return new Validator() { { dependsOn(method); dependsOn(storeCredentials); } @Override protected ValidationResult compute() { if ( method.getValue()==CloudFoundryTargetWizardModel.LoginMethod.TEMPORARY_CODE && storeCredentials.getValue()==StoreCredentialsMode.STORE_PASSWORD ) { return ValidationResult.warning("'Store Password' is useless for a 'Temporary Code'. This option will be ignored!"); } return ValidationResult.OK; } }; } /** * Validates credentials currently entered in the dialog fields, and update * other dialog model elements in the process (validation result / status and * refreshToken needed to produce effective credential object. */ public Mono<ValidationResult> validateCredentials() { return validateCredentialsHelper(CFCredentials.fromLogin(this.fMethod.getValue(), this.fPasswordVar.getValue())) .doOnSubscribe((e) -> { needValidationRequest.setValue(false); refreshToken.setValue(null); credentialsValidationResult.setValue(VALIDATION_IN_PROGRESS_MESSAGE); }) .otherwise((e) -> Mono.just(ValidationResult.error(ExceptionUtil.getMessage(e)))) .doOnNext((result) -> { credentialsValidationResult.setValue(result); }); } /** * Validates a given credential object and returns a validation result (asynchornously). */ private Mono<ValidationResult> validateCredentialsHelper(CFCredentials creds) { return Mono.defer(() -> { if (!StringUtil.hasText(creds.getSecret())) { //Don't bother verifying empty passwords. return Mono.just(REQUEST_VALIDATION_MESSAGE); } CFClientParams params = new CFClientParams( currentParams.getUrl(), currentParams.getUsername(), creds, currentParams.isSelfsigned(), null, null, currentParams.skipSslValidation() ); ClientRequests client = clientFactory.getClient(params); return client.getUserName() .then(actualUserName -> { refreshToken.setValue(client.getRefreshToken()); if (!currentParams.getUsername().equals(actualUserName)) { return Mono.just(ValidationResult.error("The credentials belong to a different user!")); } return Mono.just(ValidationResult.OK); }); }); } public LiveVariable<LoginMethod> getMethodVar() { return fMethod; } public CFCredentials getCredentials() { String refreshToken = this.refreshToken.getValue(); if (refreshToken==null) { throw new IllegalStateException("Credentials must be validated before retrieving them from the model"); } //Produce credential object consistent with store credentials mode StoreCredentialsMode storeMode = getEffectiveStoreMode(); switch (storeMode) { case STORE_NOTHING: case STORE_TOKEN: return CFCredentials.fromRefreshToken(refreshToken); case STORE_PASSWORD: return CFCredentials.fromPassword(fPasswordVar.getValue()); default: throw new IllegalStateException("Bug! Missing case?"); } } }