//
// Copyright 2010 Cinch Logic Pty Ltd.
//
// http://www.chililog.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package org.chililog.server.workbench.workers;
import org.apache.commons.lang.StringUtils;
import org.chililog.server.common.ChiliLogException;
import org.chililog.server.common.JsonTranslator;
import org.chililog.server.common.Log4JLogger;
import org.chililog.server.data.MongoConnection;
import org.chililog.server.data.UserBO;
import org.chililog.server.data.UserController;
import org.chililog.server.data.UserBO.Status;
import org.chililog.server.workbench.Strings;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import com.mongodb.DB;
/**
* <p>
* Authentication API handles:
* <ul>
* <li>login - HTTP POST /api/authentication</li>
* <li>logout - HTTP DELETE /api/authentication</li>
* <li>get user associated with token - HTTP GET /api/authentication</li>
* <li>update user profile - HTTP PUT /api/authentication?action=update_profile</li>
* <li>change password - HTTP PUT /api/authentication?action=change_password</li>
* </p>
*/
public class AuthenticationWorker extends Worker {
private static Log4JLogger _logger = Log4JLogger.getLogger(AuthenticationWorker.class);
public static final String ACTION_URI_QUERYSTRING_PARAMETER_NAME = "action";
public static final String UPDATE_PROFILE_OPERATION = "update_profile";
public static final String CHANGE_PASSWORD_OPERATION = "change_password";
/**
* Constructor
*/
public AuthenticationWorker(HttpRequest request) {
super(request);
return;
}
/**
* Supported HTTP methods
*/
@Override
public HttpMethod[] getSupportedMethods() {
return new HttpMethod[] { HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE, HttpMethod.GET };
}
/**
* Need special processing because for POST (login), there is no authentication token as yet
*/
@Override
protected ApiResult validateAuthenticationToken() {
if (this.getRequest().getMethod() == HttpMethod.POST) {
return new ApiResult();
}
return super.validateAuthenticationToken();
}
/**
* Anyone can login/logout. No need check authorization.
*
* @return {@link ApiResult}
*/
@Override
protected ApiResult validateAuthenticatedUserRole() {
return new ApiResult();
}
/**
* There are no URI parameters so no need to check
*
* @return {@link ApiResult}
*/
@Override
protected ApiResult validateURI() {
return new ApiResult();
}
/**
* <p>
* Login. If error, 401 Unauthorized is returned to the caller.
* </p>
* <p>
* If the password = refresh, then the supplied authentication header is refreshed
* </p>
*
* @throws Exception
*/
@Override
public ApiResult processPost(Object requestContent) throws Exception {
try {
AuthenticationAO requestApiObject = JsonTranslator.getInstance().fromJson(
bytesToString((byte[]) requestContent), AuthenticationAO.class);
UserBO user = null;
// See if we need to refresh the token
boolean isRefreshing = !StringUtils.isBlank(this.getRequest().getHeader(AUTHENTICATION_TOKEN_HEADER));
if (isRefreshing) {
ApiResult result = super.validateAuthenticationToken();
if (!result.isSuccess()) {
return result;
}
user = this.getAuthenticatedUser();
} else {
// Check request data
if (StringUtils.isBlank(requestApiObject.getUsername())) {
return new ApiResult(HttpResponseStatus.BAD_REQUEST, new ChiliLogException(
Strings.REQUIRED_FIELD_ERROR, "Username"));
}
if (StringUtils.isBlank(requestApiObject.getPassword())) {
return new ApiResult(HttpResponseStatus.BAD_REQUEST, new ChiliLogException(
Strings.REQUIRED_FIELD_ERROR, "Password"));
}
// Check if user exists
DB db = MongoConnection.getInstance().getConnection();
user = UserController.getInstance().tryGetByUsername(db, requestApiObject.getUsername());
if (user == null) {
user = UserController.getInstance().tryGetByEmailAddress(db, requestApiObject.getUsername());
if (user == null) {
_logger.error("Authentication failed. Cannot find username '%s'",
requestApiObject.getUsername());
return new ApiResult(HttpResponseStatus.UNAUTHORIZED, new ChiliLogException(
Strings.AUTHENTICAITON_BAD_USERNAME_PASSWORD_ERROR));
}
}
// Check password
if (!user.validatePassword(requestApiObject.getPassword())) {
// TODO lockout user
_logger.error("Authentication failed. Invalid password for user '%s'",
requestApiObject.getUsername());
return new ApiResult(HttpResponseStatus.UNAUTHORIZED, new ChiliLogException(
Strings.AUTHENTICAITON_BAD_USERNAME_PASSWORD_ERROR));
}
}
// Check if the user is enabled
if (user.getStatus() != Status.ENABLED) {
_logger.error("Authentication failed. User '%s' status not enabled: '%s'.",
requestApiObject.getUsername(), user.getStatus());
if (user.getStatus() == Status.DISABLED) {
return new ApiResult(HttpResponseStatus.UNAUTHORIZED, new ChiliLogException(
Strings.AUTHENTICAITON_ACCOUNT_DISABLED_ERROR));
} else if (user.getStatus() == Status.LOCKED) {
return new ApiResult(HttpResponseStatus.UNAUTHORIZED, new ChiliLogException(
Strings.AUTHENTICAITON_ACCOUNT_LOCKED_ERROR));
} else {
// Catch all just in-case
return new ApiResult(HttpResponseStatus.UNAUTHORIZED, new ChiliLogException(
Strings.AUTHENTICAITON_BAD_USERNAME_PASSWORD_ERROR));
}
}
// Check if the user has access (must be system administrator, repo admin or repo workbench user)
boolean allowed = false;
for (String role : user.getRoles()) {
if (role.equals(UserBO.SYSTEM_ADMINISTRATOR_ROLE_NAME)) {
allowed = true;
break;
} else if (role.startsWith(UserBO.REPOSITORY_ROLE_PREFIX)) {
if (role.endsWith(UserBO.REPOSITORY_ADMINISTRATOR_ROLE_SUFFIX)
|| role.endsWith(UserBO.REPOSITORY_WORKBENCH_ROLE_SUFFIX)) {
allowed = true;
break;
}
}
}
if (!allowed) {
return new ApiResult(HttpResponseStatus.UNAUTHORIZED, new ChiliLogException(
Strings.AUTHENTICAITON_ACCESS_DENIED_ERROR));
}
// Log the login
String msg = String.format("User '%s' logged in.", requestApiObject.getUsername());
if (isRefreshing) {
msg = msg
+ String.format(" Token refreshed for another %s seconds", requestApiObject.getExpirySeconds());
}
_logger.info(msg);
// Generate token
AuthenticationTokenAO token = new AuthenticationTokenAO(user, requestApiObject);
// Return response
return new ApiResult(token, JSON_CONTENT_TYPE, new AuthenticatedUserAO(user));
} catch (Exception ex) {
return new ApiResult(HttpResponseStatus.BAD_REQUEST, ex);
}
}
/**
* Update user profile or password.
*
* @throws Exception
*/
@Override
public ApiResult processPut(Object requestContent) throws Exception {
try {
UserBO user = this.getAuthenticatedUser();
String action = this.getUriQueryStringParameter(ACTION_URI_QUERYSTRING_PARAMETER_NAME, false);
if (action.equalsIgnoreCase(UPDATE_PROFILE_OPERATION)) {
AuthenticatedUserAO requestApiObject = JsonTranslator.getInstance().fromJson(
bytesToString((byte[]) requestContent), AuthenticatedUserAO.class);
// The logged in user must be doing this operation
if (!requestApiObject.getDocumentID().equals(user.getDocumentID().toString())) {
return new ApiResult(HttpResponseStatus.UNAUTHORIZED, new ChiliLogException(
Strings.NOT_AUTHORIZED_ERROR));
}
// Update profile details
requestApiObject.toBO(user);
} else if (action.equalsIgnoreCase(CHANGE_PASSWORD_OPERATION)) {
AuthenticatedUserPasswordAO requestApiObject = JsonTranslator.getInstance().fromJson(
bytesToString((byte[]) requestContent), AuthenticatedUserPasswordAO.class);
// The logged in user must be doing this operation
if (!requestApiObject.getDocumentID().equals(user.getDocumentID().toString())) {
return new ApiResult(HttpResponseStatus.UNAUTHORIZED, new ChiliLogException(
Strings.NOT_AUTHORIZED_ERROR));
}
// Check old password
if (!user.validatePassword(requestApiObject.getOldPassword())) {
_logger.error("Authentication failed. Invalid password for user '%s'", user.getUsername());
return new ApiResult(HttpResponseStatus.UNAUTHORIZED, new ChiliLogException(
Strings.AUTHENTICAITON_BAD_USERNAME_PASSWORD_ERROR));
}
// Check new passwords
if (!requestApiObject.getNewPassword().equals(requestApiObject.getConfirmNewPassword())) {
_logger.error("New password confirmation failed.", user.getUsername());
return new ApiResult(HttpResponseStatus.UNAUTHORIZED, new ChiliLogException(
Strings.AUTHENTICAITON_BAD_USERNAME_PASSWORD_ERROR));
}
// Update
user.setPassword(requestApiObject.getNewPassword(), true);
} else {
throw new UnsupportedOperationException(String.format("Action '%s' not supported.", action));
}
// Save changes
DB db = MongoConnection.getInstance().getConnection();
UserController.getInstance().save(db, user);
// Return updated user details
return new ApiResult(this.getAuthenticationToken(), JSON_CONTENT_TYPE, new AuthenticatedUserAO(user));
} catch (Exception ex) {
return new ApiResult(HttpResponseStatus.BAD_REQUEST, ex);
}
}
/**
* Get user details associated with a token. Typically used in association with a "remember me" function. The
* authentication token is saved so that when the user next starts up the browser, the token can be used to retrieve
* the user's details.
*
* @throws Exception
*/
@Override
public ApiResult processGet() throws Exception {
return new ApiResult(this.getAuthenticationToken(), JSON_CONTENT_TYPE, new AuthenticatedUserAO(
this.getAuthenticatedUser()));
}
/**
* Placeholder API for if we ever decide to keep server side sessions. DELETE will remove the session data.
*/
@Override
public ApiResult processDelete() throws Exception {
return new ApiResult(this.getAuthenticationToken(), null, null);
}
}