/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.usergrid.rest.applications.users;
import java.util.Map;
import java.util.UUID;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import org.apache.usergrid.rest.security.annotations.CheckPermissionsForPath;
import org.glassfish.jersey.server.mvc.Viewable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.apache.amber.oauth2.common.exception.OAuthProblemException;
import org.apache.amber.oauth2.common.message.OAuthResponse;
import org.apache.commons.lang.StringUtils;
import org.apache.usergrid.management.ActivationState;
import org.apache.usergrid.persistence.CredentialsInfo;
import org.apache.usergrid.persistence.EntityManager;
import org.apache.usergrid.persistence.entities.User;
import org.apache.usergrid.persistence.index.query.Identifier;
import org.apache.usergrid.rest.AbstractContextResource;
import org.apache.usergrid.rest.ApiResponse;
import org.apache.usergrid.rest.applications.ServiceResource;
import org.apache.usergrid.rest.exceptions.RedirectionException;
import org.apache.usergrid.rest.security.annotations.RequireApplicationAccess;
import org.apache.usergrid.rest.security.annotations.RequireSystemAccess;
import org.apache.usergrid.security.oauth.AccessInfo;
import org.apache.usergrid.security.tokens.exceptions.TokenException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.jaxrs.json.annotation.JSONP;
import net.tanesha.recaptcha.ReCaptchaImpl;
import net.tanesha.recaptcha.ReCaptchaResponse;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import static org.apache.usergrid.security.shiro.utils.SubjectUtils.*;
import static org.apache.usergrid.utils.ConversionUtils.string;
@Component("org.apache.usergrid.rest.applications.users.UserResource")
@Scope("prototype")
@Produces(MediaType.APPLICATION_JSON)
public class UserResource extends ServiceResource {
public static final String USER_EXTENSION_RESOURCE_PREFIX = "org.apache.usergrid.rest.applications.users.extensions.";
private static final Logger logger = LoggerFactory.getLogger( UserResource.class );
User user;
Identifier userIdentifier;
String errorMsg;
String token;
public UserResource() {
}
public UserResource init( Identifier userIdentifier ) throws Exception {
this.userIdentifier = userIdentifier;
return this;
}
@CheckPermissionsForPath
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@JSONP
@Produces({MediaType.APPLICATION_JSON, "application/javascript"})
public ApiResponse executePut( @Context UriInfo ui, String body,
@QueryParam("callback") @DefaultValue("callback") String callback )
throws Exception {
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> json = mapper.readValue( body, mapTypeReference );
if ( json != null ) {
if ( "me".equals( json.get("username") ) ) {
throw new IllegalArgumentException( "Username 'me' is reserved" );
}
json.remove( "password" );
json.remove( "pin" );
}
return super.executePutWithMap( ui, json, callback );
}
// no access token needed
@PUT
@Path("password")
@JSONP
@Produces({MediaType.APPLICATION_JSON, "application/javascript"})
public ApiResponse setUserPasswordPut( @Context UriInfo ui, Map<String, Object> json,
@QueryParam("callback") @DefaultValue("callback") String callback )
throws Exception {
if (logger.isTraceEnabled()) {
logger.trace("UserResource.setUserPassword");
}
if ( json == null ) {
return null;
}
ApiResponse response = createApiResponse();
response.setAction( "set user password" );
String oldPassword = string( json.get( "oldpassword" ) );
String newPassword = string( json.get( "newpassword" ) );
if ( newPassword == null ) {
throw new IllegalArgumentException( "newpassword is required" );
}
UUID applicationId = getApplicationId();
UUID targetUserId = getUserUuid();
if ( targetUserId == null ) {
response.setError( "User not found" );
return response;
}
if ( isApplicationAdmin() ) {
management.setAppUserPassword( applicationId, targetUserId, newPassword );
}
// we're not an admin user, we can only update the password ourselves
else {
management.setAppUserPassword( getApplicationId(), targetUserId, oldPassword, newPassword );
}
return response;
}
@GET
@RequireSystemAccess
@Path("credentials")
public ApiResponse getUserCredentials(@QueryParam("callback") @DefaultValue("callback") String callback )
throws Exception {
if (logger.isTraceEnabled()) {
logger.trace("UserResource.getUserCredentials");
}
final ApiResponse response = createApiResponse();
response.setAction( "get user credentials" );
final UUID applicationId = getApplicationId();
final UUID targetUserId = getUserUuid();
if ( applicationId == null ) {
response.setError( "Application not found" );
return response;
}
if ( targetUserId == null ) {
response.setError( "User not found" );
return response;
}
final CredentialsInfo credentialsInfo = management.getAppUserCredentialsInfo( applicationId, targetUserId );
response.setProperty( "credentials", credentialsInfo );
return response;
}
@PUT
@RequireSystemAccess
@Path("credentials")
public ApiResponse setUserCredentials( @Context UriInfo ui, Map<String, Object> json,
@QueryParam("callback") @DefaultValue("callback") String callback )
throws Exception {
if (logger.isTraceEnabled()) {
logger.trace("UserResource.setUserCredentials");
}
if ( json == null ) {
return null;
}
if ( "me".equals( json.get("username") ) ) {
throw new IllegalArgumentException( "Username 'me' is reserved" );
}
ApiResponse response = createApiResponse();
response.setAction( "set user credentials" );
@SuppressWarnings("unchecked")
Map<String, Object> credentialsJson = ( Map<String, Object> ) json.get( "credentials" );
if ( credentialsJson == null ) {
throw new IllegalArgumentException( "credentials sub object is required" );
}
final CredentialsInfo credentials = CredentialsInfo.fromJson( credentialsJson );
UUID applicationId = getApplicationId();
UUID targetUserId = getUserUuid();
if ( targetUserId == null ) {
response.setError( "User not found" );
return response;
}
management.setAppUserCredentialsInfo( applicationId, targetUserId, credentials );
return response;
}
// no access token needed
@POST
@Path("password")
@JSONP
@Produces({MediaType.APPLICATION_JSON, "application/javascript"})
public ApiResponse setUserPasswordPost( @Context UriInfo ui, Map<String, Object> json,
@QueryParam("callback") @DefaultValue("callback") String callback )
throws Exception {
return setUserPasswordPut( ui, json, callback );
}
@CheckPermissionsForPath
@POST
@Path("deactivate")
@JSONP
@Produces({MediaType.APPLICATION_JSON, "application/javascript"})
public ApiResponse deactivate( @Context UriInfo ui, Map<String, Object> json,
@QueryParam("callback") @DefaultValue("") String callback ) throws Exception {
ApiResponse response = createApiResponse();
response.setAction( "Deactivate user" );
User user = management.deactivateUser( getApplicationId(), getUserUuid() );
response.withEntity( user );
return response;
}
@CheckPermissionsForPath
@GET
@Path("sendpin")
@JSONP
@Produces({MediaType.APPLICATION_JSON, "application/javascript"})
public ApiResponse sendPin( @Context UriInfo ui,
@QueryParam("callback") @DefaultValue("callback") String callback )
throws Exception {
if (logger.isTraceEnabled()) {
logger.trace("UserResource.sendPin");
}
ApiResponse response = createApiResponse();
response.setAction( "retrieve user pin" );
if ( getUser() != null ) {
management.sendAppUserPin( getApplicationId(), getUserUuid() );
}
else {
response.setError( "User not found" );
}
return response;
}
@CheckPermissionsForPath
@POST
@Path("sendpin")
@JSONP
@Produces({ MediaType.APPLICATION_JSON, "application/javascript"})
public ApiResponse postSendPin( @Context UriInfo ui,
@QueryParam("callback") @DefaultValue("callback") String callback )
throws Exception {
return sendPin( ui, callback );
}
@CheckPermissionsForPath
@GET
@Path("setpin")
@JSONP
@Produces({MediaType.APPLICATION_JSON, "application/javascript"})
public ApiResponse setPin( @Context UriInfo ui, @QueryParam("pin") String pin,
@QueryParam("callback") @DefaultValue("callback") String callback )
throws Exception {
if (logger.isTraceEnabled()) {
logger.trace("UserResource.setPin");
}
ApiResponse response = createApiResponse();
response.setAction( "set user pin" );
if ( getUser() != null ) {
management.setAppUserPin( getApplicationId(), getUserUuid(), pin );
}
else {
response.setError( "User not found" );
}
return response;
}
@CheckPermissionsForPath
@POST
@Path("setpin")
@Consumes("application/x-www-form-urlencoded")
@JSONP
@Produces({MediaType.APPLICATION_JSON, "application/javascript"})
public ApiResponse postPin( @Context UriInfo ui, @FormParam("pin") String pin,
@QueryParam("callback") @DefaultValue("callback") String callback )
throws Exception {
if (logger.isTraceEnabled()) {
logger.trace("UserResource.postPin");
}
ApiResponse response = createApiResponse();
response.setAction( "set user pin" );
if ( getUser() != null ) {
management.setAppUserPin( getApplicationId(), getUserUuid(), pin );
}
else {
response.setError( "User not found" );
}
return response;
}
@CheckPermissionsForPath
@POST
@Path("setpin")
@Consumes(MediaType.APPLICATION_JSON)
@JSONP
@Produces({MediaType.APPLICATION_JSON, "application/javascript"})
public ApiResponse jsonPin( @Context UriInfo ui, JsonNode json,
@QueryParam("callback") @DefaultValue("callback") String callback )
throws Exception {
if (logger.isTraceEnabled()) {
logger.trace("UserResource.jsonPin");
}
ApiResponse response = createApiResponse();
response.setAction( "set user pin" );
if ( getUser() != null ) {
String pin = json.path( "pin" ).textValue();
management.setAppUserPin( getApplicationId(), getUserUuid(), pin );
}
else {
response.setError( "User not found" );
}
return response;
}
// no access token needed
@GET
@Path("resetpw")
@Produces(MediaType.TEXT_HTML)
public Viewable showPasswordResetForm( @Context UriInfo ui, @QueryParam("token") String token ) {
if (logger.isTraceEnabled()) {
logger.trace( "UserResource.showPasswordResetForm" );
}
this.token = token;
try {
if ( management.checkPasswordResetTokenForAppUser( getApplicationId(), getUserUuid(), token ) ) {
return handleViewable( "resetpw_set_form", this, getOrganizationName() );
}
else {
return handleViewable( "resetpw_email_form", this, getOrganizationName() );
}
}
catch ( RedirectionException e ) {
throw e;
}
catch ( Exception e ) {
return handleViewable( "error", e, getOrganizationName() );
}
}
// no access token needed, reset token required
@POST
@Path("resetpw")
@Consumes("application/x-www-form-urlencoded")
@Produces(MediaType.TEXT_HTML)
public Viewable handlePasswordResetForm( @Context UriInfo ui, @FormParam("token") String token,
@FormParam("password1") String password1,
@FormParam("password2") String password2,
@FormParam("recaptcha_challenge_field") String challenge,
@FormParam("recaptcha_response_field") String uresponse ) {
try {
if (logger.isTraceEnabled()) {
logger.trace("UserResource.handlePasswordResetForm");
}
this.token = token;
if ( ( password1 != null ) || ( password2 != null ) ) {
if ( management.checkPasswordResetTokenForAppUser( getApplicationId(), getUserUuid(), token ) ) {
if ( ( password1 != null ) && password1.equals( password2 ) ) {
management.setAppUserPassword( getApplicationId(), getUser().getUuid(), password1 );
management.revokeAccessTokenForAppUser( token );
return handleViewable( "resetpw_set_success", this, getOrganizationName() );
}
else {
errorMsg = "Passwords didn't match, let's try again...";
return handleViewable( "resetpw_set_form", this, getOrganizationName() );
}
}
else {
errorMsg = "Sorry, you have an invalid token. Let's try again...";
return handleViewable( "resetpw_email_form", this, getOrganizationName() );
}
}
if ( !useReCaptcha() ) {
management.startAppUserPasswordResetFlow( getApplicationId(), getUser() );
return handleViewable( "resetpw_email_success", this, getOrganizationName() );
}
ReCaptchaImpl reCaptcha = new ReCaptchaImpl();
reCaptcha.setPrivateKey( properties.getRecaptchaPrivate() );
ReCaptchaResponse reCaptchaResponse =
reCaptcha.checkAnswer( httpServletRequest.getRemoteAddr(), challenge, uresponse );
if ( reCaptchaResponse.isValid() ) {
management.startAppUserPasswordResetFlow( getApplicationId(), getUser() );
return handleViewable( "resetpw_email_success", this, getOrganizationName() );
}
else {
errorMsg = "Incorrect Captcha";
return handleViewable( "resetpw_email_form", this, getOrganizationName() );
}
}
catch ( RedirectionException e ) {
throw e;
}
catch ( Exception e ) {
return handleViewable( "error", e, getOrganizationName() );
}
}
public String getErrorMsg() {
return errorMsg;
}
public String getToken() {
return token;
}
public User getUser() {
if ( user == null ) {
EntityManager em = getServices().getEntityManager();
try {
user = em.get( em.getUserByIdentifier( userIdentifier ), User.class );
}
catch ( Exception e ) {
logger.error( "Unable go get user", e );
}
}
return user;
}
public UUID getUserUuid() {
user = getUser();
if ( user == null ) {
return null;
}
return user.getUuid();
}
// no access token needed, activation token required
@GET
@Path("activate")
@Produces(MediaType.TEXT_HTML)
public Viewable activate( @Context UriInfo ui, @QueryParam("token") String token ) {
try {
management.handleActivationTokenForAppUser( getApplicationId(), getUserUuid(), token );
return handleViewable( "activate", this, getOrganizationName() );
}
catch ( TokenException e ) {
return handleViewable( "bad_activation_token", this, getOrganizationName() );
}
catch ( RedirectionException e ) {
throw e;
}
catch ( Exception e ) {
return handleViewable( "error", e, getOrganizationName() );
}
}
// no access token needed, confirmation token required
@GET
@Path("confirm")
@Produces(MediaType.TEXT_HTML)
public Viewable confirm( @Context UriInfo ui, @QueryParam("token") String token ) {
try {
ActivationState state =
management.handleConfirmationTokenForAppUser( getApplicationId(), getUserUuid(), token );
if ( state == ActivationState.CONFIRMED_AWAITING_ACTIVATION ) {
return handleViewable( "confirm", this, getOrganizationName() );
}
return handleViewable( "activate", this, getOrganizationName() );
}
catch ( TokenException e ) {
return handleViewable( "bad_confirmation_token", this, getOrganizationName() );
}
catch ( RedirectionException e ) {
throw e;
}
catch ( Exception e ) {
return handleViewable( "error", e, getOrganizationName() );
}
}
@GET
@Path("reactivate")
@JSONP
@Produces({MediaType.APPLICATION_JSON, "application/javascript"})
public ApiResponse reactivate( @Context UriInfo ui,
@QueryParam("callback") @DefaultValue("callback") String callback )
throws Exception {
if (logger.isTraceEnabled()) {
logger.trace("Send activation email for user: {}", getUserUuid());
}
ApiResponse response = createApiResponse();
management.startAppUserActivationFlow( getApplicationId(), user );
response.setAction( "reactivate user" );
return response;
}
@POST
@Path("revoketokens")
@JSONP
@Produces({MediaType.APPLICATION_JSON, "application/javascript"})
public ApiResponse revokeTokensPost( @Context UriInfo ui,
@QueryParam("callback") @DefaultValue("callback") String callback )
throws Exception {
if (logger.isTraceEnabled()) {
logger.trace("Revoking user tokens for {}", getUserUuid());
}
ApiResponse response = createApiResponse();
management.revokeAccessTokensForAppUser( getApplicationId(), getUserUuid() );
response.setAction( "revoked user tokens" );
return response;
}
@CheckPermissionsForPath
@PUT
@Path("revoketokens")
@JSONP
@Produces({MediaType.APPLICATION_JSON, "application/javascript"})
public ApiResponse revokeTokensPut( @Context UriInfo ui,
@QueryParam("callback") @DefaultValue("callback") String callback )
throws Exception {
return revokeTokensPost( ui, callback );
}
@CheckPermissionsForPath
@POST
@Path("revoketoken")
@JSONP
@Produces({MediaType.APPLICATION_JSON, "application/javascript"})
public ApiResponse revokeTokenPost( @Context UriInfo ui,
@QueryParam("callback") @DefaultValue("callback") String callback,
@QueryParam("token") String token ) throws Exception {
if (logger.isTraceEnabled()) {
logger.trace( "Revoking user token for {}", getUserUuid() );
}
ApiResponse response = createApiResponse();
management.revokeAccessTokenForAppUser( token );
response.setAction( "revoked user token" );
return response;
}
@CheckPermissionsForPath
@PUT
@Path("revoketoken")
@JSONP
@Produces({MediaType.APPLICATION_JSON, "application/javascript"})
public ApiResponse revokeTokenPut( @Context UriInfo ui,
@QueryParam("callback") @DefaultValue("callback") String callback,
@QueryParam("token") String token ) throws Exception {
return revokeTokenPost( ui, callback, token );
}
@CheckPermissionsForPath
@GET
@Path("token")
@RequireApplicationAccess
public Response getAccessToken( @Context UriInfo ui, @QueryParam("ttl") long ttl,
@QueryParam("callback") @DefaultValue("") String callback ) throws Exception {
if (logger.isTraceEnabled()) {
logger.trace("UserResource.getAccessToken");
}
try {
// don't allow application user tokens to be exchanged for new tokens (possibly increasing ttl)
if ( isApplicationUser() ) {
OAuthResponse res = OAuthResponse.errorResponse( SC_FORBIDDEN ).buildJSONMessage();
return Response.status( res.getResponseStatus() ).type( jsonMediaType( callback ) )
.entity( wrapWithCallback( res.getBody(), callback ) ).build();
}
String token = management.getAccessTokenForAppUser( services.getApplicationId(), getUserUuid(), ttl );
AccessInfo access_info =
new AccessInfo().withExpiresIn( tokens.getMaxTokenAgeInSeconds( token ) ).withAccessToken( token )
.withProperty( "user", getUser() );
return Response.status( SC_OK ).type( jsonMediaType( callback ) )
.entity( wrapWithCallback( access_info, callback ) ).build();
}
catch ( OAuthProblemException e ) {
logger.error( "OAuth Error", e );
OAuthResponse res = OAuthResponse.errorResponse( SC_BAD_REQUEST ).error( e ).buildJSONMessage();
return Response.status( res.getResponseStatus() ).type( jsonMediaType( callback ) )
.entity( wrapWithCallback( res.getBody(), callback ) ).build();
}
}
@Override
@Path("{itemName}")
public AbstractContextResource addNameParameter( @Context UriInfo ui, @PathParam("itemName") PathSegment itemName )
throws Exception {
// check for user extension
String resourceClass =
USER_EXTENSION_RESOURCE_PREFIX + StringUtils.capitalize( itemName.getPath() ) + "Resource";
AbstractUserExtensionResource extensionResource = null;
try {
@SuppressWarnings("unchecked") Class<AbstractUserExtensionResource> extensionCls =
( Class<AbstractUserExtensionResource> ) Class.forName( resourceClass );
extensionResource = getSubResource( extensionCls );
}
catch ( Exception e ) {
// intentionally empty
}
if ( extensionResource != null ) {
return extensionResource;
}
return super.addNameParameter( ui, itemName );
}
}