/*
This file is part of Cyclos.
Cyclos is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Cyclos 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with Cyclos; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package nl.strohalm.cyclos.webservices.rest;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import nl.strohalm.cyclos.entities.access.Channel;
import nl.strohalm.cyclos.entities.access.PrincipalType;
import nl.strohalm.cyclos.entities.exceptions.EntityNotFoundException;
import nl.strohalm.cyclos.entities.members.Element;
import nl.strohalm.cyclos.entities.members.Member;
import nl.strohalm.cyclos.services.access.AccessServiceLocal;
import nl.strohalm.cyclos.services.access.ChannelServiceLocal;
import nl.strohalm.cyclos.services.access.exceptions.BlockedCredentialsException;
import nl.strohalm.cyclos.services.access.exceptions.CredentialsException;
import nl.strohalm.cyclos.services.access.exceptions.InvalidCredentialsException;
import nl.strohalm.cyclos.services.access.exceptions.UserNotFoundException;
import nl.strohalm.cyclos.services.elements.ElementServiceLocal;
import nl.strohalm.cyclos.services.transactions.exceptions.InvalidChannelException;
import nl.strohalm.cyclos.utils.access.LoggedUser;
import nl.strohalm.cyclos.webservices.WebServiceContext;
import nl.strohalm.cyclos.webservices.model.ServerErrorVO;
import org.apache.commons.lang.StringUtils;
import org.codehaus.jackson.map.ObjectMapper;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
/**
* An {@link AuthenticationProvider} which validates the username / password in Cyclos. WARN: Can't throw AuthenticationException cause it returns 401
* UNAUTHORIZED and that is captured by the browser, pops up an authentication form and we don't want that. A 401 freezes the mobile app.
* @author luis
*/
public class RestAuthenticationProvider implements AuthenticationProvider {
private ElementServiceLocal elementService;
private AccessServiceLocal accessService;
private ChannelServiceLocal channelService;
private static final String INVALID_CREDENTIALS = "INVALID_CREDENTIALS";
private static final String CHANNEL_DISABLED = "CHANNEL_DISABLED";
private static final String BLOCKED_CREDENTIALS = "BLOCKED_CREDENTIALS";
private static final String UNKNOWN_AUTHENTICATION_ERROR = "UNKNOWN_AUTHENTICATION_ERROR";
@Override
public Authentication authenticate(final Authentication authentication) throws AuthenticationException {
// Get / validate the principal / credentials
String principal = authentication.getName();
String credentials = (String) authentication.getCredentials();
if (StringUtils.isEmpty(principal) || StringUtils.isEmpty(credentials)) {
sendError("Empty username / password", INVALID_CREDENTIALS);
throw new InvalidCredentialsException();
}
// Get the request
HttpServletRequest request = WebServiceContext.getRequest();
if (request == null) {
sendError("Couldn't resolve the current request", UNKNOWN_AUTHENTICATION_ERROR);
throw new IllegalStateException("Couldn't resolve the current request");
}
final String remoteAddr = request.getRemoteAddr();
// Load the channel
Channel channel = channelService.loadByInternalName(Channel.REST);
final PrincipalType principalType = channelService.resolvePrincipalType(Channel.REST, channel.getDefaultPrincipalType().getPrincipal().name());
// Validate the user
String usernameToVerify = principal;
Member member = null;
try {
member = elementService.loadByPrincipal(principalType, principal, Element.Relationships.USER, Element.Relationships.GROUP);
usernameToVerify = member.getUsername();
} catch (final EntityNotFoundException e) {
usernameToVerify = "";
}
// Verify username
try {
accessService.verifyLogin(null, usernameToVerify, remoteAddr);
} catch (UserNotFoundException e) {
sendError("Invalid username / password", INVALID_CREDENTIALS);
throw new InvalidCredentialsException();
}
// Check if the channel is enabled for the specific member
if (!accessService.isChannelEnabledForMember(channel, member)) {
sendError("Channel disabled for the member", CHANNEL_DISABLED);
throw new InvalidChannelException(member.getUsername(), channel.getInternalName());
}
// Check the credentials
try {
accessService.checkCredentials(channel, member.getMemberUser(), credentials, remoteAddr, null);
} catch (BlockedCredentialsException e) {
sendError("Credentials blocked", BLOCKED_CREDENTIALS);
throw e;
} catch (CredentialsException e) {
sendError("Invalid username / password", INVALID_CREDENTIALS);
throw e;
}
// Initialize the LoggedUser, so it is accessible from the services
WebServiceContext.setRestMember(member);
LoggedUser.init(member.getUser(), remoteAddr);
// Authentication succeeded
Collection<SimpleGrantedAuthority> authority = Collections.singleton(new SimpleGrantedAuthority("ROLE_REST"));
return new UsernamePasswordAuthenticationToken(principal, credentials, authority);
}
public void setAccessServiceLocal(final AccessServiceLocal accessService) {
this.accessService = accessService;
}
public void setChannelServiceLocal(final ChannelServiceLocal channelService) {
this.channelService = channelService;
}
public void setElementServiceLocal(final ElementServiceLocal elementService) {
this.elementService = elementService;
}
@Override
public boolean supports(final Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
private void sendError(final String message, final String errorCode) {
HttpServletResponse response = WebServiceContext.getResponse();
ObjectMapper mapper = new ObjectMapper();
try {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
mapper.writeValue(response.getWriter(), new ServerErrorVO(errorCode, message));
} catch (IOException e) {
e.printStackTrace();
}
}
}