Project

General

Profile

Download (14.1 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
* Copyright (C) 2017 EDIT
3
* European Distributed Institute of Taxonomy
4
* http://www.e-taxonomy.eu
5
*
6
* The contents of this file are subject to the Mozilla Public License Version 1.1
7
* See LICENSE.TXT at the top of this package for the full license terms.
8
*/
9
package eu.etaxonomy.cdm.vaadin.view;
10

    
11
import java.net.MalformedURLException;
12
import java.net.URL;
13
import java.util.ArrayList;
14
import java.util.List;
15
import java.util.concurrent.CountDownLatch;
16
import java.util.concurrent.ExecutionException;
17
import java.util.concurrent.TimeUnit;
18

    
19
import javax.mail.internet.AddressException;
20

    
21
import org.apache.commons.lang.StringUtils;
22
import org.apache.logging.log4j.LogManager;
23
import org.apache.logging.log4j.Logger;
24
import org.springframework.beans.factory.annotation.Autowired;
25
import org.springframework.beans.factory.annotation.Qualifier;
26
import org.springframework.core.env.Environment;
27
import org.springframework.mail.MailException;
28
import org.springframework.security.authentication.AuthenticationManager;
29
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
30
import org.springframework.security.core.Authentication;
31
import org.springframework.security.core.AuthenticationException;
32
import org.springframework.util.concurrent.ListenableFuture;
33
import org.vaadin.spring.events.Event;
34
import org.vaadin.spring.events.EventBus;
35
import org.vaadin.spring.events.EventBusListener;
36
import org.vaadin.spring.events.annotation.EventBusListenerMethod;
37

    
38
import com.vaadin.data.validator.AbstractStringValidator;
39
import com.vaadin.spring.annotation.SpringComponent;
40
import com.vaadin.spring.annotation.ViewScope;
41
import com.vaadin.ui.themes.ValoTheme;
42

    
43
import eu.etaxonomy.cdm.api.application.ICdmRepository;
44
import eu.etaxonomy.cdm.api.config.CdmConfigurationKeys;
45
import eu.etaxonomy.cdm.api.service.security.AccountSelfManagementException;
46
import eu.etaxonomy.cdm.api.service.security.EmailAddressNotFoundException;
47
import eu.etaxonomy.cdm.vaadin.event.AuthenticationAttemptEvent;
48
import eu.etaxonomy.cdm.vaadin.event.AuthenticationSuccessEvent;
49
import eu.etaxonomy.cdm.vaadin.event.UserAccountEvent;
50
import eu.etaxonomy.cdm.vaadin.ui.UserAccountSelfManagementUI;
51
import eu.etaxonomy.cdm.vaadin.util.VaadinServletUtilities;
52
import eu.etaxonomy.vaadin.mvp.AbstractPresenter;
53
import eu.etaxonomy.vaadin.ui.navigation.NavigationEvent;
54
import eu.etaxonomy.vaadin.ui.navigation.NavigationManager;
55

    
56
/**
57
 * The {@link LoginView} is used as replacement view in the scope of other views.
58
 * Therefore the LoginPresenter must be in <b>UIScope</b> so that the LoginPresenter
59
 * is available to all Views.
60
 * <p>
61
 * The LoginPresenter offers a <b>auto login feature for developers</b>. To activate the auto login
62
 * you need to provide the <code>user name</code> and <code>password</code> using the environment variables
63
 * <code>cdm-vaadin.login.usr</code> and <code>cdm-vaadin.login.pwd</code>, e.g.:
64
 * <pre>
65
 * -Dcdm-vaadin.login.usr=admin -Dcdm-vaadin.login.pwd=00000
66
 * </pre>
67
 *
68
 * @author a.kohlbecker
69
 * @since Apr 25, 2017
70
 *
71
 */
72
@SpringComponent
73
@ViewScope
74
public class LoginPresenter extends AbstractPresenter<LoginView> implements EventBusListener<AuthenticationAttemptEvent> {
75

    
76
    private static final long serialVersionUID = 4020699735656994791L;
77

    
78
    private final static Logger logger = LogManager.getLogger();
79

    
80
    private final static String PROPNAME_USER = "cdm-vaadin.login.usr";
81

    
82
    private final static String PROPNAME_PASSWORD = "cdm-vaadin.login.pwd";
83

    
84
    private String redirectToState;
85

    
86
    protected EventBus.UIEventBus uiEventBus;
87

    
88
    @Autowired
89
    @Qualifier("cdmRepository")
90
    private ICdmRepository repo;
91

    
92
    @Autowired
93
    protected Environment env;
94

    
95
//    @Override
96
//    protected void eventViewBusSubscription(ViewEventBus viewEventBus) {
97
//        viewEventBus.subscribe(this);
98
//    }
99

    
100
    @Autowired
101
    protected void setUIEventBus(EventBus.UIEventBus uiEventBus){
102
        this.uiEventBus = uiEventBus;
103
        uiEventBus.subscribe(this);
104
    }
105

    
106
    public boolean authenticate(String userName, String password) {
107

    
108
        getView().clearMessage();
109

    
110
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userName, password);
111
        AuthenticationManager authenticationManager = getRepo().getAuthenticationManager();
112
        try {
113
            Authentication authentication = authenticationManager.authenticate(token);
114
            if(authentication != null && authentication.isAuthenticated()) {
115
                logger.debug("user '" + userName + "' authenticated");
116
                currentSecurityContext().setAuthentication(authentication);
117
                if(NavigationManager.class.isAssignableFrom(getNavigationManager().getClass())){
118
                    uiEventBus.publish(this, new AuthenticationSuccessEvent(userName));
119
                    logger.debug("redirecting to " + redirectToState);
120
                    uiEventBus.publish(this, new NavigationEvent(redirectToState));
121
                }
122
            }
123
        } catch (AuthenticationException e){
124
            getView().showErrorMessage("Login failed! Please check your username and password.");
125
        }
126
        return false;
127
    }
128

    
129
    @Override
130
    public void handleViewEntered() {
131

    
132
        List<String> redirectToStateTokens = getNavigationManager().getCurrentViewParameters();
133
        String currentViewName = getNavigationManager().getCurrentViewName();
134

    
135
        if(currentViewName.equals(LoginViewBean.NAME) && redirectToStateTokens.isEmpty()){
136
            // login view is shown in turn to an explicit login request of the user (e.g. login button pressed)
137
            // use the redirectToStateTokens 1-n as redirectToState
138
            //FIXME implement : redirectToState = UserView.NAME
139

    
140
        } else {
141
            // the login view is shown instead of the requested view for which the user needs to login
142
            redirectToState = String.join("/", redirectToStateTokens);
143
        }
144

    
145
        getView().getLoginDialog().getEmail().addValidator(new AbstractStringValidator("An account for this email address already exits. You may want to use the \"Password Revovery\" tab intsead?") {
146
            private static final long serialVersionUID = 1L;
147
            @Override
148
            protected boolean isValidValue(String value) {
149
                return !repo.getAccountRegistrationService().emailAddressExists(value);
150
            }
151
        });
152

    
153
        // attempt to auto login
154
        if(StringUtils.isNotEmpty(System.getProperty(PROPNAME_USER)) && StringUtils.isNotEmpty(System.getProperty(PROPNAME_PASSWORD))){
155
            logger.warn("Performing autologin with user " + System.getProperty(PROPNAME_USER));
156
            authenticate(System.getProperty(PROPNAME_USER), System.getProperty(PROPNAME_PASSWORD));
157
        }
158

    
159
    }
160

    
161
    @Override
162
    public void onEvent(Event<AuthenticationAttemptEvent> event) {
163
        if(getView()!= null){
164
            authenticate(event.getPayload().getUserName(), getView().getLoginDialog().getPassword().getValue());
165
        } else {
166
            logger.info("view is NULL, not yet disposed LoginPresenter?");
167
        }
168
    }
169

    
170
    @EventBusListenerMethod
171
    public void onPasswordRevoveryEvent(UserAccountEvent event) throws MalformedURLException, ExecutionException, MailException, AddressException, AccountSelfManagementException {
172

    
173
        if(event.getAction().equals(UserAccountEvent.UserAccountAction.REQUEST_PASSWORD_RESET)) {
174
            requestPasswordReset();
175
        } else if(event.getAction().equals(UserAccountEvent.UserAccountAction.REGISTER_ACCOUNT)) {
176
            requestAccountCreation();
177
        }
178
    }
179

    
180
    private void requestPasswordReset() throws MalformedURLException, ExecutionException {
181
        String userNameOrEmail = getView().getLoginDialog().getUserNameOrEmail().getValue();
182
        URL servletBaseUrl = VaadinServletUtilities.getServletBaseUrl();
183
        logger.debug("UserAccountAction.REQUEST_PASSWORD_RESET for " + servletBaseUrl + ", userNameOrEmail:" + userNameOrEmail);
184
        // Implementation note: UI modifications allied in the below callback methods will not affect the UI
185
        // immediately, therefore we use a CountDownLatch
186
        CountDownLatch finshedSignal = new CountDownLatch(1);
187
        List<Throwable> asyncException = new ArrayList<>(1);
188
        ListenableFuture<Boolean> futureResult = repo.getPasswordResetService().emailResetToken(
189
                userNameOrEmail,
190
                servletBaseUrl.toString() + "/app/" + UserAccountSelfManagementUI.NAME + "#!" + PasswordResetViewBean.NAME + "/%s");
191
        futureResult.addCallback(
192
                    successFuture -> {
193
                        finshedSignal.countDown();
194
                    },
195
                    exception -> {
196
                        // possible MailException
197
                        asyncException.add(exception);
198
                        finshedSignal.countDown();
199
                    }
200
                );
201
        boolean asyncTimeout = false;
202
        Boolean result = false;
203
        try {
204
            finshedSignal.await(2, TimeUnit.SECONDS);
205
            result = futureResult.get();
206
        } catch (InterruptedException e) {
207
            asyncTimeout = true;
208
        } catch (Exception e) {
209
            // in case executing getUserNameOrEmail() causes an exception faster
210
            // than futureResult.addCallback( can be processed, the exception
211
            // can not be caught asynchronously
212
            // so we are adding all these exceptions here
213
            asyncException.add(e);
214
        }
215
        if(!asyncException.isEmpty()) {
216
            String messageText = "An unknown error has occurred.";
217
            if(asyncException.get(0) instanceof MailException) {
218
                String supportText = "the support";
219
                String supportEmail = env.getProperty(CdmConfigurationKeys.MAIL_ADDRESS_SUPPORT);
220
                if(supportEmail != null) {
221
                    supportText = "<a href=\"mailto:" + supportEmail +"\">" + supportEmail + "</a>";
222
                }
223
                messageText = "Sending the password reset email to you has failed. Please try again later or contact " + supportText + " in case this error persists.";
224
            }
225
            if(asyncException.get(0) instanceof EmailAddressNotFoundException) {
226
                messageText = "There is no user accout for this email address.";
227
            }
228
            getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue(messageText);
229
            getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme.LABEL_FAILURE);
230
        } else {
231
            if(!asyncTimeout && result) {
232
                getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue("An email with a password reset link has been sent to you.");
233
                getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme.LABEL_SUCCESS);
234
                getView().getLoginDialog().getSendOnetimeLogin().setEnabled(false);
235
                getView().getLoginDialog().getUserNameOrEmail().setEnabled(false);
236
                getView().getLoginDialog().getUserNameOrEmail().setReadOnly(true);
237

    
238
            } else {
239
                getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setValue("A timeout has occured, please try again.");
240
                getView().getLoginDialog().getMessageSendRecoveryEmailLabel().setStyleName(ValoTheme.LABEL_FAILURE);
241
            }
242
        }
243
    }
244

    
245
    private void requestAccountCreation() throws MalformedURLException, MailException, AddressException, AccountSelfManagementException, ExecutionException {
246
        String emailAddress = getView().getLoginDialog().getEmail().getValue();
247
        URL servletBaseUrl = VaadinServletUtilities.getServletBaseUrl();
248

    
249
        logger.debug("UserAccountAction.REGISTER_ACCOUNT for " + servletBaseUrl + ", emailAddress:" + emailAddress);
250

    
251
        CountDownLatch finshedSignal = new CountDownLatch(1);
252
        List<Throwable> asyncException = new ArrayList<>(1);
253
        ListenableFuture<Boolean> futureResult = repo.getAccountRegistrationService().emailAccountRegistrationRequest(emailAddress,
254
                servletBaseUrl.toString() + "/app/" + UserAccountSelfManagementUI.NAME + "#!" + AccountRegistrationViewBean.NAME + "/%s");
255
        futureResult.addCallback(
256
                    successFuture -> {
257
                        finshedSignal.countDown();
258
                    },
259
                    exception -> {
260
                        // possible MailException
261
                        asyncException.add(exception);
262
                        finshedSignal.countDown();
263
                    }
264
                );
265
        boolean asyncTimeout = false;
266
        Boolean result = false;
267
        try {
268
            finshedSignal.await(2, TimeUnit.SECONDS);
269
            result = futureResult.get();
270
        } catch (InterruptedException e) {
271
            asyncTimeout = true;
272
        } catch (Exception e) {
273
            // in case executing emailAccountRegistrationRequest() causes an exception faster
274
            // than futureResult.addCallback( can be processed, the exception
275
            // can not be caught asynchronously
276
            // so we are adding all these exceptions here
277
            asyncException.add(e);
278
        }
279
        if(!asyncException.isEmpty()) {
280
            getView().getLoginDialog().getRegisterMessageLabel()
281
            .setValue("Sending the account registration email to you has failed. Please try again later or contect the support in case this error persists.");
282
            getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme.LABEL_FAILURE);
283
        } else {
284
            if(!asyncTimeout && result) {
285
                getView().getLoginDialog().getRegisterMessageLabel().setValue("An email with with further instructions has been sent to you.");
286
                getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme.LABEL_SUCCESS);
287
                getView().getLoginDialog().getEmail().setEnabled(false);
288
                getView().getLoginDialog().getRegisterButton().setEnabled(false);
289

    
290
            } else {
291
                getView().getLoginDialog().getRegisterMessageLabel().setValue("A timeout has occured, please try again.");
292
                getView().getLoginDialog().getRegisterMessageLabel().setStyleName(ValoTheme.LABEL_FAILURE);
293
            }
294
        }
295
    }
296

    
297
}
(11-11/18)