/*******************************************************************************
* Copyright (c) 2015 IBH SYSTEMS GmbH.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBH SYSTEMS GmbH - initial API and implementation
*******************************************************************************/
package org.eclipse.packagedrone.sec.web.ui;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.servlet.annotation.HttpConstraint;
import javax.servlet.annotation.ServletSecurity.EmptyRoleSemantic;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.validation.Valid;
import org.eclipse.packagedrone.repo.channel.web.breadcrumbs.Breadcrumbs;
import org.eclipse.packagedrone.repo.channel.web.breadcrumbs.Breadcrumbs.Entry;
import org.eclipse.packagedrone.sec.CreateUser;
import org.eclipse.packagedrone.sec.DatabaseDetails;
import org.eclipse.packagedrone.sec.DatabaseUserInformation;
import org.eclipse.packagedrone.sec.UserDetails;
import org.eclipse.packagedrone.sec.UserStorage;
import org.eclipse.packagedrone.sec.web.controller.HttpConstraints;
import org.eclipse.packagedrone.sec.web.controller.HttpContraintControllerInterceptor;
import org.eclipse.packagedrone.sec.web.controller.Secured;
import org.eclipse.packagedrone.sec.web.controller.SecuredControllerInterceptor;
import org.eclipse.packagedrone.sec.web.filter.SecurityFilter;
import org.eclipse.packagedrone.web.Controller;
import org.eclipse.packagedrone.web.LinkTarget;
import org.eclipse.packagedrone.web.ModelAndView;
import org.eclipse.packagedrone.web.RequestMapping;
import org.eclipse.packagedrone.web.RequestMethod;
import org.eclipse.packagedrone.web.ViewResolver;
import org.eclipse.packagedrone.web.common.CommonController;
import org.eclipse.packagedrone.web.common.InterfaceExtender;
import org.eclipse.packagedrone.web.common.Modifier;
import org.eclipse.packagedrone.web.common.menu.MenuEntry;
import org.eclipse.packagedrone.web.common.page.Pagination;
import org.eclipse.packagedrone.web.controller.ControllerInterceptor;
import org.eclipse.packagedrone.web.controller.binding.BindingResult;
import org.eclipse.packagedrone.web.controller.binding.PathVariable;
import org.eclipse.packagedrone.web.controller.binding.RequestParameter;
import org.eclipse.packagedrone.web.controller.form.FormData;
@Controller
@ViewResolver ( "/WEB-INF/views/%s.jsp" )
@RequestMapping ( "/user" )
@Secured
@ControllerInterceptor ( SecuredControllerInterceptor.class )
@HttpConstraint ( rolesAllowed = "ADMIN" )
@ControllerInterceptor ( HttpContraintControllerInterceptor.class )
public class UserController extends AbstractUserCreationController implements InterfaceExtender
{
private static final Method METHOD_LIST = LinkTarget.getControllerMethod ( UserController.class, "list" );
private static final Method METHOD_ADD_USER = LinkTarget.getControllerMethod ( UserController.class, "addUser" );
@Override
public List<MenuEntry> getMainMenuEntries ( final HttpServletRequest request )
{
final List<MenuEntry> result = new LinkedList<> ();
if ( HttpConstraints.isCallAllowed ( METHOD_LIST, request ) )
{
result.add ( new MenuEntry ( "Administration", 10_000, "Users", 1_000, LinkTarget.createFromController ( METHOD_LIST ), null, null ) );
}
return result;
}
@Override
public List<MenuEntry> getActions ( final HttpServletRequest request, final Object object )
{
if ( UserStorage.ACTION_TAG_USERS.equals ( object ) )
{
final List<MenuEntry> result = new LinkedList<MenuEntry> ();
if ( HttpConstraints.isCallAllowed ( METHOD_ADD_USER, request ) )
{
result.add ( new MenuEntry ( "Add user", 100, LinkTarget.createFromController ( METHOD_ADD_USER ), Modifier.PRIMARY, null ) );
}
return result;
}
else if ( object instanceof DatabaseUserInformation )
{
final DatabaseDetails details = ( (DatabaseUserInformation)object ).getDetails ( DatabaseDetails.class );
if ( details != null )
{
final List<MenuEntry> result = new LinkedList<MenuEntry> ();
final Map<String, Object> model = new HashMap<> ( 1 );
final String userId = ( (DatabaseUserInformation)object ).getId ();
model.put ( "userId", userId );
final boolean you = isYou ( userId, request );
// TODO: check explicitly for methods
if ( HttpConstraints.isCallAllowed ( METHOD_ADD_USER, request ) )
{
result.add ( new MenuEntry ( "Edit user", 100, LinkTarget.createFromController ( UserController.class, "editUser" ).expand ( model ), Modifier.PRIMARY, null ) );
if ( !you )
{
if ( details.isLocked () )
{
result.add ( new MenuEntry ( "Unlock user", 200, LinkTarget.createFromController ( UserController.class, "unlockUser" ).expand ( model ), Modifier.SUCCESS, null ) );
}
else
{
result.add ( new MenuEntry ( "Lock user", 200, LinkTarget.createFromController ( UserController.class, "lockUser" ).expand ( model ), Modifier.WARNING, null ).makeModalMessage ( "Lock user", "This will prevent the user from logging in. It can be reveresed by unlocking the user." ) );
}
if ( !details.isDeleted () )
{
result.add ( new MenuEntry ( "Delete user", 300, LinkTarget.createFromController ( UserController.class, "deleteUser" ).expand ( model ), Modifier.DANGER, "trash" ).makeModalMessage ( "Delete user", "Are you sure you want to delete this user?" ) );
}
}
}
return result;
}
}
return null;
}
@RequestMapping ( method = RequestMethod.GET )
public ModelAndView list ( @RequestParameter ( required = false, value = "start" ) final Integer position)
{
final ModelAndView result = new ModelAndView ( "user/list" );
result.put ( "users", Pagination.paginate ( position, 25, this.storage::list ) );
return result;
}
@RequestMapping ( value = "/add", method = RequestMethod.GET )
public ModelAndView addUser ()
{
final ModelAndView model = new ModelAndView ( "user/add" );
model.put ( "command", new CreateUser () );
return model;
}
@RequestMapping ( value = "/add", method = RequestMethod.POST )
public ModelAndView addUserPost ( @Valid @FormData ( "command" ) final CreateUser data, final BindingResult result)
{
if ( result.hasErrors () )
{
final Map<String, Object> model = new HashMap<> ( 1 );
model.put ( "command", data );
return new ModelAndView ( "user/add", model );
}
final DatabaseUserInformation newUser = this.storage.createUser ( data, true );
return new ModelAndView ( String.format ( "redirect:/user/%s/view", newUser.getId () ) );
}
@RequestMapping ( value = "/{userId}/view", method = RequestMethod.GET )
@HttpConstraint ( value = EmptyRoleSemantic.PERMIT )
public ModelAndView viewUser ( @PathVariable ( "userId" ) final String userId, final HttpServletRequest request)
{
final boolean you = isYou ( userId, request );
if ( !you && !request.isUserInRole ( "ADMIN" ) )
{
return CommonController.createAccessDenied ();
}
final DatabaseUserInformation user = this.storage.getUserDetails ( userId );
if ( user == null || user.getDetails ( DatabaseDetails.class ) == null )
{
return CommonController.createNotFound ( "user", userId );
}
final ModelAndView model = new ModelAndView ( "user/view" );
model.put ( "user", user );
model.put ( "you", you );
return model;
}
protected boolean isYou ( final String userId, final HttpServletRequest request )
{
return userId.equals ( request.getRemoteUser () );
}
protected void addBreadcrumbs ( final String action, final String userId, final Map<String, Object> model )
{
model.put ( "breadcrumbs", new Breadcrumbs ( new Entry ( "Home", "/" ), Breadcrumbs.create ( "Users", UserController.class, "list" ), Breadcrumbs.create ( "User", UserController.class, "viewUser", "userId", userId ), new Entry ( action ) ) );
}
@RequestMapping ( value = "/{userId}/edit", method = RequestMethod.GET )
public ModelAndView editUser ( @PathVariable ( "userId" ) final String userId)
{
final DatabaseUserInformation user = this.storage.getUserDetails ( userId );
if ( user == null || user.getDetails ( DatabaseDetails.class ) == null )
{
return CommonController.createNotFound ( "user", userId );
}
final Map<String, Object> model = new HashMap<> ( 2 );
model.put ( "user", user );
final DatabaseDetails details = user.getDetails ( DatabaseDetails.class );
final UserDetailsBean bean = new UserDetailsBean ();
bean.setEmail ( details.getEmail () );
bean.setName ( details.getName () );
bean.setRoles ( new HashSet<> ( details.getRoles () ) /* we need a modifiable copy */ );
model.put ( "command", bean );
model.put ( "allRoles", makePossibleRoles ( details.getRoles () ) );
addBreadcrumbs ( "Edit", userId, model );
return new ModelAndView ( "user/edit", model );
}
private SortedSet<String> makePossibleRoles ( final Set<String> roles )
{
if ( roles == null )
{
return null;
}
final SortedSet<String> result = new TreeSet<> ();
result.addAll ( roles );
result.add ( "MANAGER" );
result.add ( "ADMIN" );
return result;
}
@RequestMapping ( value = "/{userId}/edit", method = RequestMethod.POST )
public ModelAndView editUserPost ( @PathVariable ( "userId" ) final String userId, @Valid @FormData ( "command" ) final UserDetailsBean data, final BindingResult result, final HttpSession session)
{
final DatabaseUserInformation user = this.storage.getUserDetails ( userId );
if ( user == null || user.getDetails ( DatabaseDetails.class ) == null )
{
return CommonController.createNotFound ( "user", userId );
}
if ( result.hasErrors () )
{
final Map<String, Object> model = new HashMap<> ( 2 );
model.put ( "command", data );
model.put ( "user", user );
model.put ( "allRoles", makePossibleRoles ( data.getRoles () ) );
addBreadcrumbs ( "Edit", userId, model );
return new ModelAndView ( "user/edit", model );
}
this.storage.updateUser ( userId, new UserDetails ( data.getName (), data.getEmail (), data.getRoles () ) );
// TODO: only reload if it was our own profile
SecurityFilter.markReloadDetails ( session );
return new ModelAndView ( String.format ( "redirect:/user/%s/view", userId ) );
}
@RequestMapping ( "/{userId}/lock" )
public ModelAndView lockUser ( @PathVariable ( "userId" ) final String userId)
{
this.storage.lockUser ( userId );
return new ModelAndView ( "redirect:/user/" + userId + "/view" );
}
@RequestMapping ( "/{userId}/unlock" )
public ModelAndView unlockUser ( @PathVariable ( "userId" ) final String userId)
{
this.storage.unlockUser ( userId );
return new ModelAndView ( "redirect:/user/" + userId + "/view" );
}
@RequestMapping ( "/{userId}/delete" )
public ModelAndView deleteUser ( @PathVariable ( "userId" ) final String userId)
{
this.storage.deleteUser ( userId );
return new ModelAndView ( "redirect:/user" );
}
@RequestMapping ( "/{userId}/newPassword" )
@HttpConstraint ( value = EmptyRoleSemantic.PERMIT )
public ModelAndView changePassword ( @PathVariable ( "userId" ) final String userId, final HttpServletRequest request)
{
final Map<String, Object> model = new HashMap<> ();
final boolean you = isYou ( userId, request );
if ( !you && !request.isUserInRole ( "ADMIN" ) )
{
return CommonController.createAccessDenied ();
}
final DatabaseUserInformation user = this.storage.getUserDetails ( userId );
if ( user == null )
{
return CommonController.createNotFound ( "user", userId );
}
final DatabaseDetails details = user.getDetails ( DatabaseDetails.class );
if ( details == null )
{
return CommonController.createNotFound ( "details", userId );
}
final NewPassword data = new NewPassword ();
data.setEmail ( details.getEmail () );
model.put ( "you", you );
model.put ( "command", data );
return new ModelAndView ( "user/newPassword", model );
}
@RequestMapping ( value = "/{userId}/newPassword", method = RequestMethod.POST )
@HttpConstraint ( value = EmptyRoleSemantic.PERMIT )
public ModelAndView changePasswordPost ( @PathVariable ( "userId" ) final String userId, @Valid @FormData ( "command" ) final NewPassword data, final BindingResult result, final HttpServletRequest request)
{
final boolean you = isYou ( userId, request );
if ( !you && !request.isUserInRole ( "ADMIN" ) )
{
return CommonController.createAccessDenied ();
}
final Map<String, Object> model = new HashMap<> ();
model.put ( "you", you );
if ( result.hasErrors () )
{
model.put ( "command", data );
return new ModelAndView ( "user/newPassword", model );
}
try
{
if ( !you /* but we are ADMIN */ )
{
this.storage.updatePassword ( userId, null, data.getPassword () );
}
else
{
this.storage.updatePassword ( userId, data.getCurrentPassword (), data.getPassword () );
}
return new ModelAndView ( "redirect:/user/" + userId + "/view" );
}
catch ( final Exception e )
{
return CommonController.createError ( "Error", "Failed to change password", e );
}
}
}