/** * Copyright (C) 2011 JTalks.org Team * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package org.jtalks.jcommune.plugin.auth.poulpe.service; import org.apache.commons.lang3.StringUtils; import org.jtalks.jcommune.model.dto.UserDto; import org.jtalks.jcommune.plugin.api.exceptions.NoConnectionException; import org.jtalks.jcommune.plugin.api.exceptions.UnexpectedErrorException; import org.jtalks.jcommune.plugin.auth.poulpe.dto.Authentication; import org.jtalks.jcommune.plugin.auth.poulpe.dto.Errors; import org.jtalks.jcommune.plugin.auth.poulpe.dto.User; import org.restlet.Context; import org.restlet.data.ChallengeScheme; import org.restlet.data.Status; import org.restlet.engine.header.Header; import org.restlet.engine.header.HeaderConstants; import org.restlet.representation.Representation; import org.restlet.resource.ClientResource; import org.restlet.resource.ResourceException; import org.restlet.util.Series; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import java.io.IOException; import java.util.*; import java.util.concurrent.ConcurrentMap; /** * This class contains method needed for communicate with Poulpe rest service. * * @author Andrey Pogorelov */ public class PoulpeAuthService { private static final int CONNECTION_TIMEOUT = 5000; private final Logger logger = LoggerFactory.getLogger(getClass()); private String regUrl; private String authUrl; private String activationUrl; private String login; private String password; public PoulpeAuthService(String url, String login, String password) { this.regUrl = url + "/rest/private/user"; this.activationUrl = url + "/rest/private/activate"; this.authUrl = url + "/rest/authenticate"; this.login = login; this.password = password; } /** * Register user with specified data via Poulpe. * Returns errors if request failed, otherwise return null. * * @param userDto user * @param dryRun do not register the user, just check if it is possible * @return errors */ public Map<String, String> registerUser(UserDto userDto, Boolean dryRun) throws IOException, NoConnectionException, JAXBException, UnexpectedErrorException { User user = createUser(userDto.getUsername(), userDto.getPassword(), userDto.getEmail()); ClientResource clientResource = sendRegistrationRequest(user, dryRun); Map<String, String> result = getRegistrationResult(clientResource, userDto.getLanguage().getLocale()); closeRestletConnection(clientResource); return result; } /** * Authenticate user with specified data via Poulpe. * Returns true if auth success, otherwise return false. * * @param username username * @param passwordHash password hash * @return map with user details */ public Map<String, String> authenticate(String username, String passwordHash) throws JAXBException, IOException, NoConnectionException { ClientResource clientResource = sendAuthRequest(username, passwordHash); Map<String, String> result = getAuthResult(clientResource); closeRestletConnection(clientResource); return result; } /** * Activate user with specified username in Poulpe. * @param username username */ public void activate(String username) { ClientResource clientResource = null; try { clientResource = sendActivationRequest(username); } finally { if (clientResource != null) closeRestletConnection(clientResource); } } private void closeRestletConnection(ClientResource clientResource) { try { clientResource.getResponseEntity().exhaust(); } catch (IOException e) { logger.warn("Error closing connection: {}", e.getMessage() ); } } /** * Gets authentication result from response entity. * * @param clientResource response container * @return map with user details * @throws org.jtalks.jcommune.plugin.api.exceptions.NoConnectionException * */ private Map<String, String> getAuthResult(ClientResource clientResource) throws NoConnectionException, JAXBException, IOException { if (clientResource.getStatus().getCode() == Status.SUCCESS_OK.getCode() && clientResource.getResponseEntity() != null) { return parseUserDetails(clientResource.getResponseEntity()); } else if (clientResource.getStatus().getCode() == Status.CLIENT_ERROR_NOT_FOUND.getCode()) { return Collections.emptyMap(); } else { throw new NoConnectionException(clientResource.getStatus().toString()); } } private Map<String, String> parseUserDetails(Representation repr) throws JAXBException, IOException { JAXBContext context = JAXBContext.newInstance(Authentication.class); Unmarshaller unmarshaller = context.createUnmarshaller(); Authentication auth = (Authentication) unmarshaller.unmarshal(repr.getStream()); Map<String, String> authInfo = new HashMap<>(); authInfo.put("username", auth.getCredintals().getUsername()); authInfo.put("email", auth.getProfile().getEmail()); authInfo.put("firstName", auth.getProfile().getFirstName()); authInfo.put("lastName", auth.getProfile().getLastName()); authInfo.put("enabled", String.valueOf(auth.getProfile().isEnabled())); return authInfo; } /** * Gets errors from response if request wasn't successful, otherwise return null. * * @param clientResource response container * @param locale locale * @return errors * @throws org.jtalks.jcommune.plugin.api.exceptions.NoConnectionException * * @throws java.io.IOException */ private Map<String, String> getRegistrationResult(ClientResource clientResource, Locale locale) throws NoConnectionException, IOException, JAXBException, UnexpectedErrorException { if (clientResource.getStatus().getCode() == Status.SUCCESS_OK.getCode() && clientResource.getResponseEntity() != null) { return Collections.emptyMap(); } else if (clientResource.getStatus().getCode() == Status.CLIENT_ERROR_BAD_REQUEST.getCode()) { return parseErrors(clientResource.getResponseEntity(), locale); } else if (clientResource.getStatus().getCode() == Status.SERVER_ERROR_INTERNAL.getCode()) { throw new UnexpectedErrorException(); } else { throw new NoConnectionException(clientResource.getStatus().toString()); } } /** * Parse bad response representation for errors. * * @param repr response representation * @param locale locale * @return errors * @throws java.io.IOException */ private Map<String, String> parseErrors(Representation repr, Locale locale) throws IOException, JAXBException { JAXBContext context = JAXBContext.newInstance(Errors.class); Unmarshaller unmarshaller = context.createUnmarshaller(); Errors errorsRepr = (Errors) unmarshaller.unmarshal(repr.getStream()); Map<String, String> errors = new HashMap<>(); ResourceBundle resourceBundle = ResourceBundle.getBundle("ValidationMessages", locale); for (org.jtalks.jcommune.plugin.auth.poulpe.dto.Error error : errorsRepr.getErrorList()) { if (error.getCode() != null && !error.getCode().isEmpty()) { Map.Entry<String, String> errorEntry = parseErrorCode(error.getCode(), resourceBundle); if (errorEntry != null) { errors.put(errorEntry.getKey(), errorEntry.getValue()); } } } return errors; } /** * Parse error code with specified {@link ResourceBundle}. * * @param errorCode error code * @param resourceBundle used {@link ResourceBundle} * @return parsed error as pair field - error message */ private Map.Entry<String, String> parseErrorCode(String errorCode, ResourceBundle resourceBundle) { Map.Entry<String, String> error = null; if (resourceBundle.containsKey(errorCode)) { String errorMessage = resourceBundle.getString(errorCode); if (errorCode.contains("email")) { errorMessage = errorMessage .replace("{max}", String.valueOf(org.jtalks.common.model.entity.User.EMAIL_MAX_LENGTH)); error = new HashMap.SimpleEntry<>("userDto.email", errorMessage); } else if (errorCode.contains("username")) { errorMessage = errorMessage .replace("{min}", String.valueOf(org.jtalks.common.model.entity.User.USERNAME_MIN_LENGTH)) .replace("{max}", String.valueOf(org.jtalks.common.model.entity.User.USERNAME_MAX_LENGTH)); error = new HashMap.SimpleEntry<>("userDto.username", errorMessage); } else if (errorCode.contains("password")) { errorMessage = errorMessage .replace("{min}", String.valueOf(org.jtalks.common.model.entity.User.PASSWORD_MIN_LENGTH)) .replace("{max}", String.valueOf(org.jtalks.common.model.entity.User.PASSWORD_MAX_LENGTH)); error = new HashMap.SimpleEntry<>("userDto.password", errorMessage); } } return error; } /** * Creates user entity by specified username, password and email. * * @param username username * @param passwordHash password hash * @param email user email * @return user entity */ private User createUser(String username, String passwordHash, String email) { User user = new User(); user.setUsername(username == null ? "" : username); user.setEmail(email == null ? "" : email); user.setPasswordHash(passwordHash); return user; } private void addHeaderAttribute(ClientResource clientResource, String attrName, String attrValue) { ConcurrentMap<String, Object> attrs = clientResource.getRequest().getAttributes(); Series<Header> headers = (Series<Header>) attrs.get(HeaderConstants.ATTRIBUTE_HEADERS); if (headers == null) { headers = new Series<>(Header.class); Series<Header> prev = (Series<Header>) attrs.putIfAbsent(HeaderConstants.ATTRIBUTE_HEADERS, headers); if (prev != null) { headers = prev; } } headers.add(attrName, attrValue); } /** * Sends registration post request. * * @param user user entity for registration * @param dryRun do not register the user, just check if it is possible * @return ClientResource result */ protected ClientResource sendRegistrationRequest(User user, Boolean dryRun) { ClientResource clientResource = createClientResource(regUrl, true); addCredentialsIfAny(clientResource); if (dryRun) { addHeaderAttribute(clientResource, "dryRun", "true"); } writeRequestInfoToLog(clientResource); try { clientResource.post(user); } catch (ResourceException e) { logger.warn("Poulpe registration request error: {}", e.getStatus()); } return clientResource; } /** * Sends registration post request. * * @param username user name * @param passwordHash password hash * @return ClientResource result */ protected ClientResource sendAuthRequest(String username, String passwordHash) { String url = authUrl + "?username=" + username + "&passwordHash=" + passwordHash; ClientResource clientResource = createClientResource(url, false); addCredentialsIfAny(clientResource); writeRequestInfoToLog(clientResource); try { clientResource.get(); } catch (ResourceException e) { logger.warn("Poulpe authentication request error: {}", e.getStatus()); } return clientResource; } /** * Send user activation request for specified username * @param username username * @return ClientResource result */ protected ClientResource sendActivationRequest(String username){ String url = activationUrl + "?username=" + username; ClientResource clientResource = createClientResource(url, false); addCredentialsIfAny(clientResource); writeRequestInfoToLog(clientResource); try { clientResource.get(); } catch (ResourceException e) { logger.warn("Poulpe activation request error: {}", e.getStatus()); } return clientResource; } @SuppressWarnings("unchecked") private void writeRequestInfoToLog(ClientResource clientResource) { ConcurrentMap<String, Object> attrs = clientResource.getRequest().getAttributes(); Series<Header> headers = (Series<Header>) attrs.get(HeaderConstants.ATTRIBUTE_HEADERS); logger.info("Request to Poulpe: requested URI - {}, request headers - {}, request body - {}", new Object[]{clientResource.getRequest().getResourceRef(), headers, clientResource.getRequest()}); } private ClientResource createClientResource(String url, boolean buffering) { ClientResource clientResource = new ClientResource(new Context(), url); clientResource.getContext().getParameters().add("socketConnectTimeoutMs", String.valueOf(CONNECTION_TIMEOUT)); clientResource.getContext().getParameters().add("maxIoIdleTimeMs", String.valueOf(CONNECTION_TIMEOUT)); clientResource.setEntityBuffering(buffering); return clientResource; } private void addCredentialsIfAny(ClientResource clientResource){ if (!StringUtils.isAnyBlank(login, password)) clientResource.setChallengeResponse(ChallengeScheme.HTTP_BASIC, login, password); } }