Project

General

Profile

« Previous | Next » 

Revision 002f394d

Added by Andreas Kohlbecker over 2 years ago

fix #9497 User self registration service implemented

  • more interfaces and abstract classes
  • AccountRegistrationService implemented
  • test implemented

View differences:

cdmlib-services/src/main/java/eu/etaxonomy/cdm/api/service/security/PasswordResetService.java
8 8
*/
9 9
package eu.etaxonomy.cdm.api.service.security;
10 10

  
11
import java.time.Duration;
12 11
import java.util.HashMap;
13 12
import java.util.Map;
14 13
import java.util.Optional;
......
16 15
import javax.mail.internet.AddressException;
17 16
import javax.mail.internet.InternetAddress;
18 17

  
19
import org.apache.commons.collections4.map.HashedMap;
20
import org.apache.commons.text.StringSubstitutor;
21
import org.apache.log4j.Logger;
22 18
import org.springframework.beans.factory.annotation.Autowired;
23
import org.springframework.core.env.Environment;
19
import org.springframework.beans.factory.annotation.Qualifier;
24 20
import org.springframework.dao.DataAccessException;
25 21
import org.springframework.mail.MailException;
26
import org.springframework.mail.SimpleMailMessage;
27
import org.springframework.mail.javamail.JavaMailSender;
28 22
import org.springframework.scheduling.annotation.Async;
29 23
import org.springframework.scheduling.annotation.AsyncResult;
30 24
import org.springframework.security.core.userdetails.UserDetails;
......
34 28
import org.springframework.util.Assert;
35 29
import org.springframework.util.concurrent.ListenableFuture;
36 30

  
37
import com.google.common.util.concurrent.RateLimiter;
38

  
39
import eu.etaxonomy.cdm.api.config.CdmConfigurationKeys;
40
import eu.etaxonomy.cdm.api.config.SendEmailConfigurer;
41 31
import eu.etaxonomy.cdm.api.security.AbstractRequestToken;
42
import eu.etaxonomy.cdm.api.security.IPasswordResetTokenStore;
32
import eu.etaxonomy.cdm.api.security.IAbstractRequestTokenStore;
43 33
import eu.etaxonomy.cdm.api.security.PasswordResetRequest;
44
import eu.etaxonomy.cdm.api.service.IUserService;
45 34
import eu.etaxonomy.cdm.model.permission.User;
46
import eu.etaxonomy.cdm.persistence.dao.permission.IUserDao;
47 35

  
48 36
/**
49 37
 * @author a.kohlbecker
......
51 39
 */
52 40
@Service
53 41
@Transactional(readOnly = false)
54
public class PasswordResetService implements IPasswordResetService {
55

  
56
    private static Logger logger = Logger.getLogger(PasswordResetRequest.class);
57

  
58
    @Autowired
59
    private IUserDao userDao;
60

  
61
    @Autowired
62
    private IUserService userService;
42
public class PasswordResetService extends AccountSelfManagementService implements IPasswordResetService {
63 43

  
64 44
    @Autowired
65
    private IPasswordResetTokenStore passwordResetTokenStore;
66

  
67
    @Autowired
68
    private JavaMailSender emailSender;
69

  
70
    @Autowired
71
    private Environment env;
72

  
73
    private Duration rateLimiterTimeout = null;
74
    private RateLimiter emailResetToken_rateLimiter = RateLimiter.create(PERMITS_PER_SECOND);
75
    private RateLimiter resetPassword_rateLimiter = RateLimiter.create(PERMITS_PER_SECOND);
45
    @Qualifier("passwordResetTokenStore")
46
    IAbstractRequestTokenStore<PasswordResetRequest> passwordResetTokenStore;
76 47

  
77 48
    /**
78 49
     * Create a request token and send it to the user via email.
......
120 91
                Map<String, String> additionalValues = new HashMap<>();
121 92
                additionalValues.put("linkUrl", passwordRequestFormUrl);
122 93
                sendEmail(user.getEmailAddress(), user.getUsername(),
123
                        PasswordResetTemplates.RESET_REQUEST_EMAIL_SUBJECT_TEMPLATE,
124
                        PasswordResetTemplates.RESET_REQUEST_EMAIL_BODY_TEMPLATE, additionalValues);
94
                        UserAccountEmailTemplates.RESET_REQUEST_EMAIL_SUBJECT_TEMPLATE,
95
                        UserAccountEmailTemplates.REGISTRATION_REQUEST_EMAIL_BODY_TEMPLATE, additionalValues);
125 96
                logger.info("A password reset request for  " + user.getUsername() + " has been send to "
126 97
                        + user.getEmailAddress());
127 98
            } catch (UsernameNotFoundException e) {
......
137 108
    }
138 109

  
139 110
    /**
140
     * Uses the {@link StringSubstitutor} as simple template engine.
141
     * Below named values are automatically resolved, more can be added via the
142
     * <code>valuesMap</code> parameter.
143
     *
144
     * @param userEmail
145
     *  The TO-address
146
     * @param userName
147
     *  Used to set the value for <code>${userName}</code>
148
     * @param subjectTemplate
149
     *  A {@link StringSubstitutor} template for the email subject
150
     * @param bodyTemplate
151
     *  A {@link StringSubstitutor} template for the email body
152
     * @param additionalValuesMap
153
     *  Additional named values for to be replaced in the template strings.
154
     */
155
    public void sendEmail(String userEmail, String userName, String subjectTemplate, String bodyTemplate, Map<String, String> additionalValuesMap) throws MailException {
156

  
157
        String from = env.getProperty(SendEmailConfigurer.FROM_ADDRESS);
158
        String dataSourceBeanId = env.getProperty(CdmConfigurationKeys.CDM_DATA_SOURCE_ID);
159
        String supportEmailAddress = env.getProperty(CdmConfigurationKeys.MAIL_ADDRESS_SUPPORT);
160
        if(additionalValuesMap == null) {
161
            additionalValuesMap = new HashedMap<>();
162
        }
163
        if(supportEmailAddress != null) {
164
            additionalValuesMap.put("supportEmailAddress", supportEmailAddress);
165
        }
166
        additionalValuesMap.put("userName", userName);
167
        additionalValuesMap.put("dataBase", dataSourceBeanId);
168
        StringSubstitutor substitutor = new StringSubstitutor(additionalValuesMap);
169

  
170
        // TODO use MimeMessages for better email layout?
171
        // TODO user Thymeleaf instead for HTML support?
172
        SimpleMailMessage message = new SimpleMailMessage();
173

  
174
        message.setFrom(from);
175
        message.setTo(userEmail);
176

  
177
        message.setSubject(substitutor.replace(subjectTemplate));
178
        message.setText(substitutor.replace(bodyTemplate));
179

  
180
        emailSender.send(message);
181
    }
111
    *
112
    * @param token
113
    *            the token string
114
    * @param newPassword
115
    *            The new password to set
116
    * @return A <code>Future</code> for a <code>Boolean</code> flag. The
117
    *         boolean value will be <code>false</code> in case the max access
118
    *         rate for this method has been exceeded and a time out has
119
    *         occurred.
120
    * @throws AccountSelfManagementException
121
    *             in case an invalid token has been used
122
    * @throws MailException
123
    *             in case sending the email has failed
124
    */
125
   @Override
126
   @Async
127
   public ListenableFuture<Boolean> resetPassword(String token, String newPassword) throws AccountSelfManagementException, MailException {
128

  
129
       if (resetPassword_rateLimiter.tryAcquire(getRateLimiterTimeout())) {
130

  
131
           Optional<PasswordResetRequest> resetRequest = passwordResetTokenStore.findResetRequest(token);
132
           if (resetRequest.isPresent()) {
133
               try {
134
                   UserDetails user = userService.loadUserByUsername(resetRequest.get().getUserName());
135
                   Assert.isAssignable(user.getClass(), User.class);
136
                   userService.encodeUserPassword((User)user, newPassword);
137
                   userDao.saveOrUpdate((User)user);
138
                   passwordResetTokenStore.remove(token);
139
                   sendEmail(resetRequest.get().getUserEmail(), resetRequest.get().getUserName(),
140
                           UserAccountEmailTemplates.RESET_SUCCESS_EMAIL_SUBJECT_TEMPLATE,
141
                           UserAccountEmailTemplates.RESET_SUCCESS_EMAIL_BODY_TEMPLATE, null);
142
                   return new AsyncResult<Boolean>(true);
143
               } catch (DataAccessException | IllegalArgumentException | UsernameNotFoundException e) {
144
                   logger.error("Failed to change password of User " + resetRequest.get().getUserName(), e);
145
                   sendEmail(resetRequest.get().getUserEmail(), resetRequest.get().getUserName(),
146
                           UserAccountEmailTemplates.RESET_FAILED_EMAIL_SUBJECT_TEMPLATE,
147
                           UserAccountEmailTemplates.RESET_FAILED_EMAIL_BODY_TEMPLATE, null);
148
               }
149
           } else {
150
               throw new AccountSelfManagementException("Invalid password reset token");
151
           }
152
       }
153
       return new AsyncResult<Boolean>(false);
154
   }
182 155

  
183 156
    /**
184 157
     *
......
204 177
        }
205 178
        return user;
206 179
    }
207

  
208
    /**
209
     *
210
     * @param token
211
     *            the token string
212
     * @param newPassword
213
     *            The new password to set
214
     * @return A <code>Future</code> for a <code>Boolean</code> flag. The
215
     *         boolean value will be <code>false</code> in case the max access
216
     *         rate for this method has been exceeded and a time out has
217
     *         occurred.
218
     * @throws PasswordResetException
219
     *             in case an invalid token has been used
220
     * @throws MailException
221
     *             in case sending the email has failed
222
     */
223
    @Override
224
    @Async
225
    public ListenableFuture<Boolean> resetPassword(String token, String newPassword) throws PasswordResetException, MailException {
226

  
227
        if (resetPassword_rateLimiter.tryAcquire(getRateLimiterTimeout())) {
228

  
229
            Optional<PasswordResetRequest> resetRequest = passwordResetTokenStore.findResetRequest(token);
230
            if (resetRequest.isPresent()) {
231
                try {
232
                    UserDetails user = userService.loadUserByUsername(resetRequest.get().getUserName());
233
                    Assert.isAssignable(user.getClass(), User.class);
234
                    userService.encodeUserPassword((User)user, newPassword);
235
                    userDao.saveOrUpdate((User)user);
236
                    passwordResetTokenStore.remove(token);
237
                    sendEmail(resetRequest.get().getUserEmail(), resetRequest.get().getUserName(),
238
                            PasswordResetTemplates.RESET_SUCCESS_EMAIL_SUBJECT_TEMPLATE,
239
                            PasswordResetTemplates.RESET_SUCCESS_EMAIL_BODY_TEMPLATE, null);
240
                    return new AsyncResult<Boolean>(true);
241
                } catch (DataAccessException | IllegalArgumentException | UsernameNotFoundException e) {
242
                    logger.error("Failed to change password of User " + resetRequest.get().getUserName(), e);
243
                    sendEmail(resetRequest.get().getUserEmail(), resetRequest.get().getUserName(),
244
                            PasswordResetTemplates.RESET_FAILED_EMAIL_SUBJECT_TEMPLATE,
245
                            PasswordResetTemplates.RESET_FAILED_EMAIL_BODY_TEMPLATE, null);
246
                }
247
            } else {
248
                throw new PasswordResetException("Invalid password reset token");
249
            }
250
        }
251
        return new AsyncResult<Boolean>(false);
252
    }
253

  
254
    @Override
255
    public Duration getRateLimiterTimeout() {
256
        if(rateLimiterTimeout == null) {
257
            rateLimiterTimeout = Duration.ofSeconds(RATE_LIMTER_TIMEOUT_SECONDS);
258
        }
259
        return rateLimiterTimeout;
260
    }
261

  
262
    @Override
263
    public void setRateLimiterTimeout(Duration timeout) {
264
        this.rateLimiterTimeout = timeout;
265
    }
266

  
267
    @Override
268
    public void setRate(double rate) {
269
        resetPassword_rateLimiter.setRate(rate);
270
        emailResetToken_rateLimiter.setRate(rate);
271
    }
272 180
}

Also available in: Unified diff