package com.intrbiz.bergamot.ui.api; import java.util.concurrent.TimeUnit; import org.apache.log4j.Logger; import com.intrbiz.Util; import com.intrbiz.balsa.engine.route.Router; import com.intrbiz.balsa.engine.security.credentials.GenericAuthenticationToken; import com.intrbiz.balsa.error.BalsaConversionError; import com.intrbiz.balsa.error.BalsaSecurityException; import com.intrbiz.balsa.error.BalsaValidationError; import com.intrbiz.balsa.error.http.BalsaBadRequest; import com.intrbiz.balsa.error.http.BalsaNotFound; import com.intrbiz.balsa.http.HTTP.HTTPStatus; import com.intrbiz.balsa.metadata.WithDataAdapter; import com.intrbiz.bergamot.data.BergamotDB; import com.intrbiz.bergamot.metadata.IgnoreBinding; import com.intrbiz.bergamot.model.APIToken; import com.intrbiz.bergamot.model.Contact; import com.intrbiz.bergamot.model.message.AuthTokenMO; import com.intrbiz.bergamot.model.message.api.error.APIError; import com.intrbiz.bergamot.ui.BergamotApp; import com.intrbiz.converter.ConversionException; import com.intrbiz.metadata.Any; import com.intrbiz.metadata.Before; import com.intrbiz.metadata.Catch; import com.intrbiz.metadata.CheckStringLength; import com.intrbiz.metadata.IgnorePaths; import com.intrbiz.metadata.JSON; import com.intrbiz.metadata.Order; import com.intrbiz.metadata.Param; import com.intrbiz.metadata.Prefix; import com.intrbiz.metadata.RequireValidPrincipal; import com.intrbiz.metadata.XML; import com.intrbiz.validator.ValidationException; @Prefix("/api/") public class APIRouter extends Router<BergamotApp> { private Logger logger = Logger.getLogger(APIRouter.class); /** * Default global API 404 error handler for XML responses (config) */ @Catch(BalsaNotFound.class) @Any("**\\.xml") @Order(10) @XML(status = HTTPStatus.NotFound) public APIError notFoundXML() { return new APIError("Not found"); } /** * Default global API 403 error handler for XML responses (config) */ @Catch(BalsaSecurityException.class) @Any("**\\.xml") @Order(10) @XML(status = HTTPStatus.Forbidden) public APIError accessDeniedXML() { return new APIError("Access denied"); } /** * Default global API 404 error handler */ @Catch(BalsaNotFound.class) @Any("**") @Order(20) @JSON(status = HTTPStatus.NotFound) public APIError notFound() { return new APIError("Not found"); } /** * Default global API 403 error handler */ @Catch(BalsaSecurityException.class) @Any("**") @Order(20) @JSON(status = HTTPStatus.Forbidden) public APIError accessDenied() { return new APIError("Access denied"); } /** * Validation and Conversion error handler */ @Catch({ BalsaValidationError.class, BalsaConversionError.class }) @Any("**") @Order(30) @JSON(status = HTTPStatus.BadRequest) public APIError invalideRequest() { for (ConversionException cex : balsa().getConversionErrors()) { logger.error("Conversion exception on request", cex); } for (ValidationException vex : balsa().getValidationErrors()) { logger.error("Validation exception on request", vex); } return new APIError("Bad Request"); } /** * Validation and Conversion error handler */ @Catch(BalsaBadRequest.class) @Any("**") @Order(40) @JSON(status = HTTPStatus.BadRequest) public APIError badRequest() { Throwable error = balsa().getException(); if (error != null) { logger.error("Caught internal bad request error: " + error.getMessage(), error); } return new APIError("Bad Request: " + (error == null || Util.isEmpty(error.getMessage()) ? "Not sure what happened here!" : error.getMessage())); } /** * Default global API 500 error handler */ @Catch() @Any("**") @Order(Order.LAST) @JSON(status = HTTPStatus.InternalServerError) public APIError internalServerError() { Throwable error = balsa().getException(); if (error != null) { logger.error("Caught internal server error: " + error.getMessage(), error); } return new APIError(error == null || Util.isEmpty(error.getMessage()) ? "Not sure what happened here!" : error.getMessage()); } /** * Authenticate an API Request based on the authentication * token given on the request (if any). */ @Before @Any("**") /* We don't want to filter the authentication routes */ @IgnorePaths({"/auth-token", "/extend-auth-token", "/test/hello/world", "/app/auth-token"}) @Order(10) @WithDataAdapter(BergamotDB.class) public void authenticateRequest(BergamotDB db) { // perform a token based request authentication // we may already have the auth from the session, if shared with a UI session if (! this.validPrincipal()) { authenticateRequest(new GenericAuthenticationToken(Util.coalesceEmpty(header("X-Bergamot-Auth"), cookie("bergamot.api.key"), param("key")))); } // assert that the contact is permitted API access require(permission("api.access")); // setup the site based on the authenticated principal Contact contact = var("contact", currentPrincipal()); var("site", contact.getSite()); } /** * Authenticate a user for API access on behalf of an application, * this will generate a perpetual auth token */ @Any("/app/auth-token") @JSON() @WithDataAdapter(BergamotDB.class) public AuthTokenMO getAppAuthToken(BergamotDB db, @Param("app") @CheckStringLength(mandatory = true, min = 3, max = 80) String appName, @Param("username") String username, @Param("password") String password) { authenticateRequest(username, password); String token = app().getSecurityEngine().generatePerpetualAuthenticationTokenForPrincipal(currentPrincipal()); db.setAPIToken(new APIToken(token, currentPrincipal(), Util.coalesceEmpty("Application: " + appName))); return new AuthTokenMO(token, 0L); } /** * Authenticate a user for API access, */ @Any("/auth-token") @JSON() @IgnoreBinding public AuthTokenMO getAuthToken(@Param("username") String username, @Param("password") String password) { authenticateRequest(username, password); long expiresAt = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); String token = app().getSecurityEngine().generateAuthenticationTokenForPrincipal(currentPrincipal(), expiresAt); return new AuthTokenMO(token, expiresAt); } /** * Extend an authentication token */ @Any("/extend-auth-token") @JSON() public AuthTokenMO extendAuthToken(@Param("auth-token") String token) { authenticateRequest(new GenericAuthenticationToken(token)); long expiresAt = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); String newToken = app().getSecurityEngine().generateAuthenticationTokenForPrincipal(currentPrincipal(), expiresAt); return new AuthTokenMO(newToken, expiresAt); } /** * Change the current users password */ @Any("/change-password") @JSON() @RequireValidPrincipal() @WithDataAdapter(BergamotDB.class) public Boolean changePassword( BergamotDB db, @Param("current-password") @CheckStringLength(min = 1, max = 80, mandatory = true) String currentPassword, @Param("new-password") @CheckStringLength(min = 1, max = 80, mandatory = true) String newPassword ) { Contact contact = currentPrincipal(); // verify the given current password before changing the password if (! contact.verifyPassword(currentPassword)) throw new BalsaSecurityException("Failed to verify current password"); // change the password contact.hashPassword(newPassword); // update the contact db.setContact(contact); return true; } }