/**
* Copyright 2005-2014 Restlet
*
* The contents of this file are subject to the terms of one of the following
* open source licenses: Apache 2.0 or or EPL 1.0 (the "Licenses"). You can
* select the license that you prefer but you may not use this file except in
* compliance with one of these Licenses.
*
* You can obtain a copy of the Apache 2.0 license at
* http://www.opensource.org/licenses/apache-2.0
*
* You can obtain a copy of the EPL 1.0 license at
* http://www.opensource.org/licenses/eclipse-1.0
*
* See the Licenses for the specific language governing permissions and
* limitations under the Licenses.
*
* Alternatively, you can obtain a royalty free commercial license with less
* limitations, transferable or non-transferable, directly at
* http://restlet.com/products/restlet-framework
*
* Restlet is a registered trademark of Restlet S.A.S.
*/
package org.restlet.ext.apispark.internal.agent.module;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import org.restlet.Application;
import org.restlet.Context;
import org.restlet.Request;
import org.restlet.Response;
import org.restlet.data.ChallengeScheme;
import org.restlet.data.Status;
import org.restlet.ext.apispark.internal.ApiSparkConfig;
import org.restlet.ext.apispark.internal.agent.AgentException;
import org.restlet.ext.apispark.internal.agent.AgentUtils;
import org.restlet.ext.apispark.internal.agent.bean.AuthenticationSettings;
import org.restlet.ext.apispark.internal.agent.bean.Credentials;
import org.restlet.ext.apispark.internal.agent.bean.ModulesSettings;
import org.restlet.ext.apispark.internal.agent.bean.User;
import org.restlet.ext.apispark.internal.agent.resource.AuthenticationAuthenticateResource;
import org.restlet.resource.ResourceException;
import org.restlet.security.ChallengeAuthenticator;
import org.restlet.security.Role;
import org.restlet.security.Verifier;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.UncheckedExecutionException;
/**
* Authentication module for the agent. This class extends {@link ChallengeAuthenticator} and is responsible to fill
* {@link org.restlet.data.ClientInfo} on the request.
*
* @author Manuel Boillod
*/
public class AuthenticationModule extends ChallengeAuthenticator {
private class AgentVerifier implements Verifier {
@Override
public int verify(Request request, Response response) {
int result;
if (request.getChallengeResponse() == null) {
result = RESULT_MISSING;
} else {
String identifier = request.getChallengeResponse()
.getIdentifier();
char[] secret = request.getChallengeResponse().getSecret();
UserIdentifier userIdentifier = new UserIdentifier(identifier,
secret);
try {
// we have to add secret in cache key because cache loader
// needs the secret.
UserInfo userInfo = userLoadingCache
.getUnchecked(userIdentifier);
if (userInfo == null) {
throw new AgentException("User could not be null");
}
// verify password (only after getting user from cache).
// See UserIdenfifier javadoc for more details.
if (!Arrays.equals(secret, userInfo.getSecret())) {
result = RESULT_INVALID;
} else {
// set user on request client info
User user = userInfo.getUser();
org.restlet.security.User securityUser = new org.restlet.security.User(
identifier, (char[]) null, user.getFirstName(),
user.getLastName(), user.getEmail());
request.getClientInfo().setUser(securityUser);
// set roles on request client info
List<Role> securityRoles = new ArrayList<>();
Application application = Application.getCurrent();
if (user.getGroups() != null) {
for (String role : user.getGroups()) {
securityRoles.add(new Role(application, role));
}
}
request.getClientInfo().setRoles(securityRoles);
result = RESULT_VALID;
}
} catch (UncheckedExecutionException e) {
if (e.getCause() instanceof ResourceException) {
// client resource exception (status 401 is normal)
ResourceException rex = (ResourceException) e
.getCause();
if (Status.CLIENT_ERROR_UNAUTHORIZED.equals(rex
.getStatus())) {
response.setStatus(Status.CLIENT_ERROR_UNAUTHORIZED);
result = RESULT_INVALID;
} else {
throw new ResourceException(
Status.SERVER_ERROR_INTERNAL,
"Agent service error during user authentication of user: "
+ identifier, rex);
}
} else {
throw new AgentException(
"Unexpected error during user authentication error of user: "
+ identifier, e);
}
}
}
return result;
}
}
/**
* This class is used as Cache Key. The {@link #secret} is not used in the
* key, but the {@link CacheLoader} need it.
*
* Warning: The {@link #hashCode()} and {@link #equals(Object)} methods only
* use the {@link #identifier} attribute. The secret should be compared
* separately.
*/
public static class UserIdentifier {
private String identifier;
private char[] secret;
public UserIdentifier(String identifier, char[] secret) {
this.identifier = identifier;
this.secret = secret;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof UserIdentifier) {
UserIdentifier userIdentifier = (UserIdentifier) obj;
return Objects.equals(identifier, userIdentifier.identifier);
}
return false;
}
public String getIdentifier() {
return identifier;
}
public char[] getSecret() {
return secret;
}
@Override
public int hashCode() {
return Objects.hash(identifier);
}
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
public void setSecret(char[] secret) {
this.secret = secret;
}
}
private static class UserInfo {
private char[] secret;
private User user;
private UserInfo(User user, char[] secret) {
this.user = user;
this.secret = secret;
}
public char[] getSecret() {
return secret;
}
public User getUser() {
return user;
}
}
public static final String AUTHENTICATE_PATH = "/authentication/authenticate";
/** Internal logger. */
protected static Logger LOGGER = Logger
.getLogger(AuthenticationModule.class.getName());
private AuthenticationAuthenticateResource authenticateClientResource;
private AuthenticationSettings authenticationSettings;
private LoadingCache<UserIdentifier, UserInfo> userLoadingCache;
/**
* Create a new Authentication module with the specified settings.
*
* @param apiSparkConfig
* The agent configuration.
* @param modulesSettings
* The modules settings.
*/
public AuthenticationModule(ApiSparkConfig apiSparkConfig,
ModulesSettings modulesSettings) {
this(apiSparkConfig, modulesSettings, null);
}
/**
* Create a new Authentication module with the specified settings.
*
* @param apiSparkConfig
* The agent configuration.
* @param modulesSettings
* The modules settings.
* @param context
* The context
*/
public AuthenticationModule(ApiSparkConfig apiSparkConfig,
ModulesSettings modulesSettings, Context context) {
super(context, ChallengeScheme.HTTP_BASIC, "realm");
authenticationSettings = new AuthenticationSettings();
authenticationSettings.setOptional(modulesSettings.isAuthorizationModuleEnabled());
authenticateClientResource = AgentUtils.getClientResource(
apiSparkConfig, modulesSettings,
AuthenticationAuthenticateResource.class, AUTHENTICATE_PATH);
// config ChallengeAuthenticator
setOptional(authenticationSettings.isOptional());
setVerifier(new AgentVerifier());
// Initialize the cache
initializeCache();
}
/**
* Initializes the user cache and the cache loader instance.
*/
private void initializeCache() {
// Cache loader get user from apispark. Never returns null
CacheLoader<UserIdentifier, UserInfo> userLoader = new CacheLoader<UserIdentifier, UserInfo>() {
public UserInfo load(UserIdentifier userIdentifier) {
Credentials credentials = new Credentials(
userIdentifier.getIdentifier(),
userIdentifier.getSecret());
User user = authenticateClientResource
.authenticate(credentials);
if (user == null) {
// Authentication should throw an error instead of
// returning
// null
throw new AgentException(
"Authentication should not return null");
}
return new UserInfo(user, userIdentifier.getSecret());
}
};
userLoadingCache = CacheBuilder
.newBuilder()
.maximumSize(authenticationSettings.getCacheSize())
.expireAfterWrite(
authenticationSettings.getCacheTimeToLiveSeconds(),
TimeUnit.SECONDS).build(userLoader);
}
}