package gov.nysenate.openleg.service.auth; import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.EventBus; import com.google.common.eventbus.Subscribe; import gov.nysenate.openleg.dao.auth.ApiUserDao; import gov.nysenate.openleg.model.auth.ApiUser; import gov.nysenate.openleg.model.auth.ApiUserAuthEvictEvent; import gov.nysenate.openleg.model.cache.CacheEvictEvent; import gov.nysenate.openleg.model.cache.CacheEvictIdEvent; import gov.nysenate.openleg.model.cache.CacheWarmEvent; import gov.nysenate.openleg.model.cache.ContentCache; import gov.nysenate.openleg.model.notification.Notification; import gov.nysenate.openleg.model.notification.NotificationType; import gov.nysenate.openleg.service.base.data.CachingService; import gov.nysenate.openleg.service.mail.MimeSendMailService; import net.sf.ehcache.Cache; import net.sf.ehcache.CacheManager; import net.sf.ehcache.Ehcache; import net.sf.ehcache.config.CacheConfiguration; import org.apache.commons.lang3.RandomStringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.ehcache.EhCacheCache; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.time.LocalDateTime; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @Service public class CachedSqlApiUserService implements ApiUserService, CachingService<String> { @Autowired protected ApiUserDao apiUserDao; @Autowired protected MimeSendMailService sendMailService; @Value("${domain.url}") private String domainUrl; @Autowired private CacheManager cacheManager; @Autowired private EventBus eventBus; // private static final String apiUserCacheName = ; private EhCacheCache apiUserCache; private static final Logger logger = LoggerFactory.getLogger(CachedSqlApiUserService.class); @PostConstruct private void init() { eventBus.register(this); setupCaches(); } @PreDestroy private void cleanUp() { evictCaches(); cacheManager.removeCache(ContentCache.APIUSER.name()); } /*** --- CachingService Implementation --- */ @Override public void setupCaches() { Cache cache = new Cache(new CacheConfiguration().name(ContentCache.APIUSER.name()) .eternal(true) .sizeOfPolicy(defaultSizeOfPolicy())); cacheManager.addCache(cache); this.apiUserCache = new EhCacheCache(cache); } @Override public List<Ehcache> getCaches() { return Arrays.asList(apiUserCache.getNativeCache()); } @Override @Subscribe public void handleCacheEvictEvent(CacheEvictEvent evictEvent) { if (evictEvent.affects(ContentCache.APIUSER)) { evictCaches(); } } /** {@inheritDoc} */ @Subscribe @Override public void handleCacheEvictIdEvent(CacheEvictIdEvent<String> evictIdEvent) { if (evictIdEvent.affects(ContentCache.APIUSER)) { evictContent(evictIdEvent.getContentId()); } } @Override public void evictContent(String key) { apiUserCache.evict(key); } @Override public void warmCaches() { evictCaches(); logger.info("Warming up API User Cache"); // Feed in all the api users from the database into the cache apiUserDao.getAllUsers().forEach(this::cacheApiUser); } @Override @Subscribe public void handleCacheWarmEvent(CacheWarmEvent warmEvent) { if (warmEvent.affects(ContentCache.APIUSER)) { warmCaches(); } } /** --- ApiUserService Implementation --- */ /** {@inheritDoc} */ @Override public ApiUser registerNewUser(String email, String name, String orgName) { Pattern emailRegex = Pattern.compile("^[a-zA-Z\\d-._]+@[a-zA-Z\\d-._]+.[a-zA-Z]{2,4}$"); Matcher patternMatcher = emailRegex.matcher(email); if (!patternMatcher.find()) throw new IllegalArgumentException("Invalid email address format used used in registration attempt!"); try { if (apiUserDao.getApiUserFromEmail(email) != null) throw new UsernameExistsException(email); } catch (EmptyResultDataAccessException e) { // User does not exist as expected } ApiUser newUser = new ApiUser(email); newUser.setName(name); newUser.setOrganizationName(orgName); newUser.setAuthenticated(false); newUser.setRegistrationToken(RandomStringUtils.randomAlphanumeric(32)); newUser.setActive(true); apiUserDao.insertUser(newUser); sendRegistrationEmail(newUser); return newUser; } /** {@inheritDoc} */ @Override public ApiUser getUser(String email) { return apiUserDao.getApiUserFromEmail(email); } /** {@inheritDoc} */ @Override public boolean validateKey(String apikey) { return getUserByKey(apikey) .map(ApiUser::isAuthenticated) .orElse(false); } /** {@inheritDoc} */ @Override public void activateUser(String registrationToken) { try { ApiUser user = apiUserDao.getApiUserFromToken(registrationToken); if (!user.isAuthenticated()) { user.setActive(true); user.setAuthenticated(true); apiUserDao.updateUser(user); cacheApiUser(user); sendNewApiUserNotification(user); } sendApikeyEmail(user); } catch (EmptyResultDataAccessException e) { throw new IllegalArgumentException("Invalid registration token supplied!"); } } /** {@inheritDoc} */ @Override public ImmutableSet<OpenLegRole> getRoles(String key) { return getUserByKey(key) .map(ApiUser::getGrantedRoles) .orElse(ImmutableSet.of()); } /** {@inheritDoc} */ @Override public void grantRole(String apiKey, OpenLegRole role) { apiUserDao.grantRole(apiKey, role); getCachedApiUser(apiKey).ifPresent(apiUser -> apiUser.addRole(role)); eventBus.post(new ApiUserAuthEvictEvent(apiKey)); } /** {@inheritDoc} */ @Override public void revokeRole(String apiKey, OpenLegRole role) { apiUserDao.revokeRole(apiKey, role); getCachedApiUser(apiKey).ifPresent(apiUser -> apiUser.removeRole(role)); eventBus.post(new ApiUserAuthEvictEvent(apiKey)); } /** * Attempt to get an api user as an optional value * If the user does not exist in the cache, attempt to retrieve the user from the database * Return an empty optional if it is not in the database * @param apiKey String * @return Optional<ApiUser> */ public Optional<ApiUser> getUserByKey(String apiKey) { Optional<ApiUser> userOpt = getCachedApiUser(apiKey); if (userOpt.isPresent()) { return userOpt; } try { ApiUser user = apiUserDao.getApiUserFromKey(apiKey); cacheApiUser(user); return Optional.of(user); } catch (EmptyResultDataAccessException ex) { return Optional.empty(); } } /** --- Internal Methods --- */ /*** * Inserts the given api user into the cache */ private void cacheApiUser(ApiUser apiUser) { if (apiUser != null) { apiUserCache.put(apiUser.getApiKey(), apiUser); } } /** * Attempt to get an api user from the cache as an optional value * If no user with the given key exists in the cache, return an empty optional * @param apiKey String * @return Optional<ApiUser> */ private Optional<ApiUser> getCachedApiUser(String apiKey) { return Optional.ofNullable(apiUserCache.get(apiKey)) .map(valueWrapper -> (ApiUser) valueWrapper.get()); } /** * This method will send a user a confirmation email, containing a link holding their registration token, * which will allow them to activate their account. * * @param user The user to send the registration information to */ private void sendRegistrationEmail(ApiUser user) { String message = String.format("Hello %s,\n\n\tThank you for your interest in Open Legislation. " + "In order to receive your API key you must first activate your account by visiting the link below. " + "Once you have confirmed your email address, an email will be sent to you containing your API Key.\n\n" + "Activate your account here:\n%s/%s" + "\n\n-- NY Senate Development Team", user.getName(), domainUrl + "/register/token", user.getRegistrationToken()); sendMailService.sendMessage(user.getEmail(), "Open Legislation API Account Registration", message); } /** * This method will send a user an email containing their API Key. * It is called whenever a user confirms their email address via their registration token. * @param user The user to send the API Key to. */ private void sendApikeyEmail(ApiUser user) { String message = String.format("Hello %s,\n\n\tThank you for your interest in Open Legislation.\n\n\t" + "Here's your API Key:\n%s\n\n-- NY Senate Development Team", user.getName(), user.getApiKey()); sendMailService.sendMessage(user.getEmail(), "Your Open Legislation API Key", message); } private void sendNewApiUserNotification(ApiUser user) { boolean named = user.getName() != null; String summary = (named ? user.getName() : user.getEmail()) + " is now registered as an API user!"; String message = summary + "\n" + (named ? "name: " + user.getName() + "\n" : "") + (user.getOrganizationName() != null ? "organization: " + user.getOrganizationName() + "\n" : "") + "email: " + user.getEmail(); eventBus.post(new Notification(NotificationType.NEW_API_KEY, LocalDateTime.now(), summary, message)); } }