Skip to content
Snippets Groups Projects
UserWebService.java 44.8 KiB
Newer Older
Alan Moran's avatar
Alan Moran committed
/**
 * <a href="http://www.openolat.org">
 * OpenOLAT - Online Learning and Training</a><br>
Alan Moran's avatar
Alan Moran committed
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License"); <br>
 * you may not use this file except in compliance with the License.<br>
 * You may obtain a copy of the License at the
 * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
Alan Moran's avatar
Alan Moran committed
 * <p>
 * Unless required by applicable law or agreed to in writing,<br>
 * software distributed under the License is distributed on an "AS IS" BASIS, <br>
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
 * See the License for the specific language governing permissions and <br>
 * limitations under the License.
 * <p>
 * Initial code contributed and copyrighted by<br>
 * frentix GmbH, http://www.frentix.com
Alan Moran's avatar
Alan Moran committed
 * <p>
 */
package org.olat.user.restapi;

import static org.olat.restapi.security.RestSecurityHelper.getIdentity;
Alan Moran's avatar
Alan Moran committed
import static org.olat.restapi.security.RestSecurityHelper.getLocale;
import static org.olat.restapi.security.RestSecurityHelper.getRoles;
Alan Moran's avatar
Alan Moran committed
import static org.olat.restapi.security.RestSecurityHelper.getUserRequest;
import static org.olat.restapi.security.RestSecurityHelper.itself;
Alan Moran's avatar
Alan Moran committed
import static org.olat.user.restapi.UserVOFactory.formatDbUserProperty;
import static org.olat.user.restapi.UserVOFactory.get;
import static org.olat.user.restapi.UserVOFactory.getManaged;
Alan Moran's avatar
Alan Moran committed
import static org.olat.user.restapi.UserVOFactory.parseUserProperty;
import static org.olat.user.restapi.UserVOFactory.post;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
Alan Moran's avatar
Alan Moran committed
import javax.ws.rs.GET;
Alan Moran's avatar
Alan Moran committed
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.WebApplicationException;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Request;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
Alan Moran's avatar
Alan Moran committed

srosse's avatar
srosse committed
import org.olat.admin.user.UserShortDescription;
Alan Moran's avatar
Alan Moran committed
import org.olat.basesecurity.Authentication;
import org.olat.basesecurity.BaseSecurity;
import org.olat.basesecurity.BaseSecurityModule;
import org.olat.basesecurity.IdentityPowerSearchQueries;
import org.olat.basesecurity.OrganisationRoles;
import org.olat.basesecurity.OrganisationService;
import org.olat.basesecurity.SearchIdentityParams;
import org.olat.basesecurity.model.IdentityRefImpl;
import org.olat.core.CoreSpringFactory;
import org.olat.core.commons.persistence.DB;
Alan Moran's avatar
Alan Moran committed
import org.olat.core.gui.components.form.ValidationError;
import org.olat.core.gui.translator.PackageTranslator;
import org.olat.core.gui.translator.Translator;
import org.olat.core.id.Identity;
import org.olat.core.id.Organisation;
import org.olat.core.id.Preferences;
import org.olat.core.id.Roles;
import org.olat.core.id.RolesByOrganisation;
Alan Moran's avatar
Alan Moran committed
import org.olat.core.id.User;
import org.olat.core.id.UserConstants;
import org.olat.core.util.StringHelper;
srosse's avatar
srosse committed
import org.olat.core.util.Util;
import org.olat.restapi.group.MyGroupWebService;
import org.olat.restapi.support.MultipartReader;
Alan Moran's avatar
Alan Moran committed
import org.olat.restapi.support.vo.ErrorVO;
import org.olat.user.DisplayPortraitManager;
import org.olat.user.UserLifecycleManager;
Alan Moran's avatar
Alan Moran committed
import org.olat.user.UserManager;
import org.olat.user.propertyhandlers.UserPropertyHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
Alan Moran's avatar
Alan Moran committed

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;

Alan Moran's avatar
Alan Moran committed
/**
 * This web service handles functionalities related to <code>User</code>.
 * 
 * @author srosse, stephane.rosse@frentix.com
 */

@Tag (name = "Users")
Alan Moran's avatar
Alan Moran committed
@Path("users")
Alan Moran's avatar
Alan Moran committed
public class UserWebService {
	
	private static final String VERSION = "1.0";
	
	public static final String PROPERTY_HANDLER_IDENTIFIER = UserWebService.class.getName();
	
	public static final CacheControl cc = new CacheControl();
Alan Moran's avatar
Alan Moran committed
	static {
		cc.setMaxAge(-1);
	}
	
	@Autowired
	private DB dbInstance;
	@Autowired
	private UserManager userManager;
	@Autowired
	private BaseSecurity securityManager;
	@Autowired
	private BaseSecurityModule securityModule;
	@Autowired
	private DisplayPortraitManager portraitManager;
	@Autowired
	private OrganisationService organisationService;
	@Autowired
	private UserLifecycleManager userLifecycleManager;
	@Autowired
	private IdentityPowerSearchQueries identitySearchQueries;
	
Alan Moran's avatar
Alan Moran committed
	/**
	 * The version of the User Web Service
Alan Moran's avatar
Alan Moran committed
	 * @return The version number
	 */
	@GET
	@Path("version")
	@Operation(summary = "The version of the User Web Service", description = "The version of the User Web Service")
	@ApiResponse(responseCode = "200", description = "The version of this specific Web Service")
Alan Moran's avatar
Alan Moran committed
	@Produces(MediaType.TEXT_PLAIN)
	public Response getVersion() {
		return Response.ok(VERSION).build();
	}
	
	/**
	 * Search users and return them in a simple form (without user properties). User properties
	 * can be added two the query parameters. If the authUsername and the authProvider are set,
	 * the search is made only with these two parameters because they are sufficient to return
	 * The search with login and user properties are made default with wild cards. If an exact
	 * match is needed, the parameter msut be quoted:<br>
	 * users?login="username"<br>
	 * Don't forget the right escaping in the URL!<br>
	 * You can make a search with the user properties like this:<br>
	 * users?telMobile=39847592&login=test
	 * <br >/ The lookup is possible for authors, usermanagers and system administrators. Normal
	 * users are not allowed to use the lookup service.
Alan Moran's avatar
Alan Moran committed
	 * @param login The login (search with like)
	 * @param authProvider An authentication provider (optional)
	 * @param authUsername An specific username from the authentication provider
	 * @param uriInfo The URI infos
	 * @param httpRequest The HTTP request
Alan Moran's avatar
Alan Moran committed
	 * @return An array of users
	 */
	@GET
	@Operation(summary = "Search users and return them in a simple form ", description = "Search users and return them in a simple form (without user properties). User properties\n" + 
			"	  can be added two the query parameters. If the authUsername and the authProvider are set,\n" + 
			"	  the search is made only with these two parameters because they are sufficient to return\n" + 
			"	  a single user.<br>\n" + 
			"	  The search with login and user properties are made default with wild cards. If an exact\n" + 
			"	  match is needed, the parameter msut be quoted:<br>\n" + 
			"	  users?login=\"username\"<br>\n" + 
			"	  Don't forget the right escaping in the URL!<br>\n" + 
			"	  You can make a search with the user properties like this:<br>\n" + 
			"	  users?telMobile=39847592&login=test\n" + 
			"	  <br >/ The lookup is possible for authors, usermanagers and system administrators. Normal\n" + 
			"	  users are not allowed to use the lookup service.")
	@ApiResponse(responseCode = "200", description = "The list of all users in the OLAT system", content = {
			@Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = UserVO.class))),
			@Content(mediaType = "application/xml", array = @ArraySchema(schema = @Schema(implementation = UserVO.class))) })
	@ApiResponse(responseCode = "401", description = "The roles of the authenticated user are not sufficient")
Alan Moran's avatar
Alan Moran committed
	@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
	public Response getUserListQuery(@QueryParam("login") String login, @QueryParam("externalId") String externalId,
			@QueryParam("authProvider") String authProvider, @QueryParam("authUsername") String authUsername,
			@QueryParam("statusVisibleLimit") String statusVisibleLimit,
Alan Moran's avatar
Alan Moran committed
			@Context UriInfo uriInfo, @Context HttpServletRequest httpRequest) {
		// User lookup allowed for authors, usermanagers and admins. For
		// usernamanger and up are considered "administrative" when it comes to
		// lookup of the user properties
		Roles roles = getRoles(httpRequest);
		if(!roles.isAdministrator() && !roles.isUserManager() && !roles.isRolesManager() && !roles.isAuthor()) {
Alan Moran's avatar
Alan Moran committed
			return Response.serverError().status(Status.UNAUTHORIZED).build();
		}
		
		MultivaluedMap<String,String> params = uriInfo.getQueryParameters();
Alan Moran's avatar
Alan Moran committed
		List<Identity> identities;
		//make only a search by authUsername
		if(StringHelper.containsNonWhitespace(authProvider) && StringHelper.containsNonWhitespace(authUsername)) {
			Authentication auth = securityManager.findAuthenticationByAuthusername(authUsername, authProvider);
Alan Moran's avatar
Alan Moran committed
			if(auth == null) {
				identities = Collections.emptyList();
			} else {
				identities = Collections.singletonList(auth.getIdentity());
			}
		} else {

			boolean isAdministrativeUser = securityModule.isUserAllowedAdminProps(roles);
			
Alan Moran's avatar
Alan Moran committed
			String[] authProviders = null;
			if(StringHelper.containsNonWhitespace(authProvider)) {
				authProviders = new String[]{authProvider};
			}
			
			//retrieve and convert the parameters value
			Map<String,String> userProps = new HashMap<>();
Alan Moran's avatar
Alan Moran committed
			if(!params.isEmpty()) {
				Locale locale = getLocale(httpRequest);
				List<UserPropertyHandler> propertyHandlers = userManager
						.getUserPropertyHandlersFor(PROPERTY_HANDLER_IDENTIFIER, isAdministrativeUser);
Alan Moran's avatar
Alan Moran committed
				for(UserPropertyHandler handler:propertyHandlers) {
					if(!params.containsKey(handler.getName())) continue;
					
					List<String> values = params.get(handler.getName());
					if(!values.isEmpty()) {
						String value = formatDbUserProperty(values.get(0), handler, locale);
						userProps.put(handler.getName(), value);
					}
			
			Integer status = Identity.STATUS_VISIBLE_LIMIT;
			if(isAdministrativeUser && "all".equalsIgnoreCase(statusVisibleLimit)) {
				status = null;
			}
			
			SearchIdentityParams searchParams = new SearchIdentityParams(login, userProps, true,
					null, authProviders, null, null, null, null, status);
			if(StringHelper.containsNonWhitespace(externalId)) {
				searchParams.setExternalId(externalId);
			}
			identities = securityManager.getIdentitiesByPowerSearch(searchParams, 0, -1);
Alan Moran's avatar
Alan Moran committed
		}
		
		int count = 0;
		UserVO[] userVOs = new UserVO[identities.size()];
		for(Identity identity:identities) {
			userVOs[count++] = get(identity);
Alan Moran's avatar
Alan Moran committed
		}
		return Response.ok(userVOs).build();
	}
	
	@Operation(summary = "Get managed Users", description = "Get managed Users")
	@ApiResponse(responseCode = "200", description = "List of all managed users", content = {
			@Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = ManagedUserVO.class))),
			@Content(mediaType = "application/xml", array = @ArraySchema(schema = @Schema(implementation = ManagedUserVO.class))) })
	@ApiResponse(responseCode = "401", description = "The roles of the authenticated user are not sufficient")
	@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
	public Response getManagedUsers(@Context HttpServletRequest httpRequest) {
		if(!isUserManager(httpRequest)) {
			return Response.serverError().status(Status.UNAUTHORIZED).build();
		}
		
		SearchIdentityParams params = new SearchIdentityParams();
		params.setManaged(Boolean.TRUE);
		List<Identity> identities = identitySearchQueries.getIdentitiesByPowerSearch(params, 0, -1);
		int count = 0;
		ManagedUserVO[] userVOs = new ManagedUserVO[identities.size()];
		for(Identity identity:identities) {
			userVOs[count++] = getManaged(identity);
		}
		return Response.ok(userVOs).build();
	}
	
Alan Moran's avatar
Alan Moran committed
	/**
	 * Creates and persists a new user entity
Alan Moran's avatar
Alan Moran committed
	 * @param user The user to persist
	 * @param request The HTTP request
	 * @return the new persisted <code>User</code>
	 */
	@PUT
	@Operation(summary = "Creates and persists a new user entity", description = "Creates and persists a new user entity")
	@ApiResponse(responseCode = "200", description = "The persisted user", content = {
			@Content(mediaType = "application/json", schema = @Schema(implementation = UserVO.class)),
			@Content(mediaType = "application/xml", schema = @Schema(implementation = UserVO.class)) })
	@ApiResponse(responseCode = "401", description = "The roles of the authenticated user are not sufficient")
	@ApiResponse(responseCode = "406", description = "The list of errors", content = {
			@Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = ErrorVO.class))),
			@Content(mediaType = "application/xml", array = @ArraySchema(schema = @Schema(implementation = ErrorVO.class))) })
Alan Moran's avatar
Alan Moran committed
	@Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
	@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
	public Response create(UserVO user, @Context HttpServletRequest request) {
		if(!isUserManager(request)) {
			return Response.serverError().status(Status.UNAUTHORIZED).build();
		}
		
		if (!syntaxCheckOlatLogin(user)) {
			Locale locale = getLocale(request);
			Translator translator = Util.createPackageTranslator(UserShortDescription.class, locale);
			String translation = translator.translate("new.error.loginname.empty");
			ErrorVO[] errorVos = new ErrorVO[]{
				new ErrorVO("org.olat.admin.user", "new.error.loginname.empty", translation)
			};
			return Response.ok(errorVos).status(Status.NOT_ACCEPTABLE).build();
		}
			
Alan Moran's avatar
Alan Moran committed
		// Check if login is still available
		Identity identity = securityManager.findIdentityByLogin(user.getLogin());
Alan Moran's avatar
Alan Moran committed
		if (identity != null) {
			Locale locale = getLocale(request);
srosse's avatar
srosse committed
			Translator translator = Util.createPackageTranslator(UserShortDescription.class, locale);
Alan Moran's avatar
Alan Moran committed
			String translation = translator.translate("new.error.loginname.choosen");
			ErrorVO[] errorVos = new ErrorVO[]{
				new ErrorVO("org.olat.admin.user", "new.error.loginname.choosen", translation)
			};
			return Response.ok(errorVos).status(Status.NOT_ACCEPTABLE).build();
		}
		
		List<ErrorVO> errors = validateUser(null, user, request);
Alan Moran's avatar
Alan Moran committed
		if(errors.isEmpty()) {
			User newUser = userManager.createUser(user.getFirstName(), user.getLastName(), user.getEmail());
			
			String identityName = securityModule.isIdentityNameAutoGenerated() ? null : user.getLogin();
			Identity id = securityManager
					.createAndPersistIdentityAndUserWithOrganisation(identityName, user.getLogin(), user.getExternalId(), newUser,
							BaseSecurityModule.getDefaultAuthProviderIdentifier(), user.getLogin(), user.getPassword(), null);
Alan Moran's avatar
Alan Moran committed
			post(newUser, user, getLocale(request));
			userManager.updateUser(newUser);
Alan Moran's avatar
Alan Moran committed
			return Response.ok(get(id)).build();
		}
		
		//content not ok
		ErrorVO[] errorVos = new ErrorVO[errors.size()];
		errors.toArray(errorVos);
		return Response.ok(errorVos).status(Status.NOT_ACCEPTABLE).build();
	}
	
	/**
	 * This a minimal syntax check specific to the REST API.
	 * 
	 * @param user
	 * @return
	 */
	private boolean syntaxCheckOlatLogin(UserVO user) {
		String login = user.getLogin();
		if(!StringHelper.containsNonWhitespace(login)) {
			return false;
		}
		
		char[] charArr = login.toCharArray();
		for(char ch:charArr) {
			if(ch == '/') {
				return false;
			}
			if(ch < 32) {
				return false;
			}
			
		}
		if(login.indexOf('/') >= 0) {
			return false;
		}
		return login.length() >= 3;
	}
	
Alan Moran's avatar
Alan Moran committed
	/**
	 * Retrieves the roles of a user given its unique key identifier
	 * @param identityKey The user key identifier of the user being searched
	 * @param httpRequest The HTTP request
	 * @return an xml or json representation of a the roles being search.
Alan Moran's avatar
Alan Moran committed
	 */
	@GET
	@Path("{identityKey}/roles")
	@Operation(summary = "Retrieve the roles", description = "Retrieves the roles of a user given its unique key identifier")
	@ApiResponse(responseCode = "200", description = "The user", content = {
			@Content(mediaType = "application/json", schema = @Schema(implementation = RolesVO.class)),
			@Content(mediaType = "application/xml", schema = @Schema(implementation = RolesVO.class)) })
	@ApiResponse(responseCode = "401", description = "The roles of the authenticated user are not sufficient")
	@ApiResponse(responseCode = "404", description = "The identity not found")
	@Produces({MediaType.APPLICATION_XML ,MediaType.APPLICATION_JSON})
	public Response getIdentityRoles(@PathParam("identityKey") Long identityKey, @Context HttpServletRequest request) {
		if(!isUserManagerOf(identityKey, request) && !itself(identityKey, request)) {
			return Response.serverError().status(Status.FORBIDDEN).build();
		}
		Identity identity = securityManager.loadIdentityByKey(identityKey, false);
		if(identity == null) {
			return Response.serverError().status(Status.NOT_FOUND).build();
		Roles roles = securityManager.getRoles(identity);
		return Response.ok(new RolesVO(roles)).build();
	/**
	 * Update the roles of a user given its unique key identifier
	 * @param identityKey The user key identifier of the user being searched
	 * @param roles The updated roles
	 * @param httpRequest The HTTP request
	 * @return an xml or json representation of a the roles being search.
	 */
Alan Moran's avatar
Alan Moran committed
	@POST
	@Path("{identityKey}/roles")
	@Operation(summary = "Update the roles", description = "Update the roles of a user given its unique key identifier")
	@ApiResponse(responseCode = "200", description = "The user", content = {
			@Content(mediaType = "application/json", schema = @Schema(implementation = RolesVO.class)),
			@Content(mediaType = "application/xml", schema = @Schema(implementation = RolesVO.class)) })
	@ApiResponse(responseCode = "401", description = "The roles of the authenticated user are not sufficient")
	@ApiResponse(responseCode = "404", description = "The identity not found")
	@Consumes({MediaType.APPLICATION_XML ,MediaType.APPLICATION_JSON})
	@Produces({MediaType.APPLICATION_XML ,MediaType.APPLICATION_JSON})
	public Response updateRoles(@PathParam("identityKey") Long identityKey, RolesVO roles, @Context HttpServletRequest request) {
		boolean isUserManager = isUserManagerOf(identityKey, request);
		if(!isUserManager) {
			return Response.serverError().status(Status.FORBIDDEN).build();
		Identity identity = securityManager.loadIdentityByKey(identityKey, false);
		if(identity == null) {
			return Response.serverError().status(Status.NOT_FOUND).build();
		}
		
		Identity actingIdentity = getIdentity(request);
		Organisation defOrganisation = organisationService.getDefaultOrganisation();
		boolean userRole = !roles.isGuestOnly() && !roles.isInvitee();
		RolesByOrganisation modifiedRoles = RolesByOrganisation.roles(defOrganisation,
				roles.isGuestOnly(), roles.isInvitee(), userRole, roles.isAuthor(),
				roles.isGroupManager(), roles.isPoolAdmin(), roles.isCurriculumManager(),
				roles.isUserManager(), roles.isInstitutionalResourceManager(), roles.isOlatAdmin());
		securityManager.updateRoles(actingIdentity, identity, modifiedRoles);
		dbInstance.commit();// make sure all is committed before loading the roles again
		Roles updatedRoles = securityManager.getRoles(identity);
		return Response.ok(new RolesVO(updatedRoles)).build();
Alan Moran's avatar
Alan Moran committed
	}
	/**
	 * Retrieves the status of a user given its unique key identifier
	 * @param identityKey The user key identifier of the user being searched
	 * @param httpRequest The HTTP request
	 * @return an xml or json representation of a the status being search.
	 */
	@GET
	@Path("{identityKey}/status")
	@Operation(summary = "Retrieve the status", description = "Retrieves the status of a user given its unique key identifier")
	@ApiResponse(responseCode = "200", description = "The user", content = {
			@Content(mediaType = "application/json", schema = @Schema(implementation = StatusVO.class)),
			@Content(mediaType = "application/xml", schema = @Schema(implementation = StatusVO.class)) })
	@ApiResponse(responseCode = "401", description = "The roles of the authenticated user are not sufficient")
	@ApiResponse(responseCode = "404", description = "The identity not found")
	@Produces({MediaType.APPLICATION_XML ,MediaType.APPLICATION_JSON})
	public Response getStatus(@PathParam("identityKey") Long identityKey, @Context HttpServletRequest request) {
		boolean isUserManager = isUserManagerOf(identityKey, request);
		if(!isUserManager) {
			return Response.serverError().status(Status.FORBIDDEN).build();
		}
		Identity identity = securityManager.loadIdentityByKey(identityKey, false);
		if(identity == null) {
			return Response.serverError().status(Status.NOT_FOUND).build();

		StatusVO status = new StatusVO();
		status.setStatus(identity.getStatus());
		return Response.ok(status).build();
	}
	
	/**
	 * Update the roles of a user given its unique key identifier:
	 * <ul>
	 * 	<li>1: Permanent user</li> 
	 * 	<li>2: activ</li> 
	 *  <li>101: login denied</li> 
	 *  <li>199: deleted</li> 
	 * </ul>
	 * 
	 * @param identityKey The user key identifier of the user being searched
	 * @param status The status to update
	 * @param httpRequest The HTTP request
	 * @return An xml or json representation of a the status after update.
	 */
	@POST
	@Path("{identityKey}/status")
	@Operation(summary = "Update the roles of a user", description = "Update the roles of a user given its unique key identifier:\n" + 
			"	  <ul>\n" + 
			"	  	<li>1: Permanent user</li> \n" + 
			"	  	<li>2: activ</li> \n" + 
			"	   <li>101: login denied</li> \n" + 
			"	   <li>199: deleted</li> \n" + 
			"	  </ul>\n" + 
			"	  ")
	@ApiResponse(responseCode = "200", description = "The user", content = {
			@Content(mediaType = "application/json", schema = @Schema(implementation = StatusVO.class)),
			@Content(mediaType = "application/xml", schema = @Schema(implementation = StatusVO.class)) })
	@ApiResponse(responseCode = "401", description = "The roles of the authenticated user are not sufficient")
	@ApiResponse(responseCode = "404", description = "The identity not found")
	@Consumes({MediaType.APPLICATION_XML ,MediaType.APPLICATION_JSON})
	@Produces({MediaType.APPLICATION_XML ,MediaType.APPLICATION_JSON})
	public Response updateStatus(@PathParam("identityKey") Long identityKey, StatusVO status, @Context HttpServletRequest request) {
		Identity actingIdentity = getIdentity(request);
		boolean isUserManager = isUserManagerOf(identityKey, request);
		if(actingIdentity == null || !isUserManager) {
			return Response.serverError().status(Status.FORBIDDEN).build();
		}
		Identity identity = securityManager.loadIdentityByKey(identityKey, false);
		if(identity == null) {
			return Response.serverError().status(Status.NOT_FOUND).build();
		
		Integer newStatus = status.getStatus();
		identity = securityManager.saveIdentityStatus(identity, newStatus, actingIdentity);
		StatusVO reloadedStatus = new StatusVO();
		reloadedStatus.setStatus(identity.getStatus());
		return Response.ok(reloadedStatus).build();
	/**
	 * Retrieves the preferences of a user given its unique key identifier
	 * @param identityKey The user key identifier of the user being searched
	 * @param httpRequest The HTTP request
	 * @return an xml or json representation of a the roles being search.
	 */
	@GET
	@Path("{identityKey}/preferences")
	@Operation(summary = "Retrieve the preferences", description = "Retrieves the preferences of a user given its unique key identifier")
	@ApiResponse(responseCode = "200", description = "The preferences", content = {
			@Content(mediaType = "application/json", schema = @Schema(implementation = PreferencesVO.class)),
			@Content(mediaType = "application/xml", schema = @Schema(implementation = PreferencesVO.class)) })
	@ApiResponse(responseCode = "401", description = "The roles of the authenticated user are not sufficient")
	@ApiResponse(responseCode = "404", description = "The identity not found")
	@Produces({MediaType.APPLICATION_XML ,MediaType.APPLICATION_JSON})
	public Response getUserPreferences(@PathParam("identityKey") Long identityKey, @Context HttpServletRequest request) {
		boolean isUserManager = isUserManagerOf(identityKey, request);
		if(!isUserManager) {
			return Response.serverError().status(Status.FORBIDDEN).build();
		}
		
		Identity identity = securityManager.loadIdentityByKey(identityKey, false);
		if(identity == null) {
			return Response.serverError().status(Status.NOT_FOUND).build();
		}
		
		Preferences prefs = identity.getUser().getPreferences();
		return Response.ok(new PreferencesVO(prefs)).build();
	}
	
	/**
	 * Update the preferences of a user given its unique key identifier
	 * @param identityKey The user key identifier of the user being searched
	 * @param preferences The updated preferences
	 * @param httpRequest The HTTP request
	 * @return an xml or json representation of a the roles being search.
	 */
	@POST
	@Path("{identityKey}/preferences")
	@Operation(summary = "Update the preferences", description = "Update the preferences of a user given its unique key identifier")
	@ApiResponse(responseCode = "200", description = "The user", content = {
			@Content(mediaType = "application/json", schema = @Schema(implementation = PreferencesVO.class)),
			@Content(mediaType = "application/xml", schema = @Schema(implementation = PreferencesVO.class)) })
	@ApiResponse(responseCode = "401", description = "The roles of the authenticated user are not sufficient")
	@ApiResponse(responseCode = "404", description = "The identity not found")
	@Consumes({MediaType.APPLICATION_XML ,MediaType.APPLICATION_JSON})
	@Produces({MediaType.APPLICATION_XML ,MediaType.APPLICATION_JSON})
	public Response updatePreferences(@PathParam("identityKey") Long identityKey, PreferencesVO preferences, @Context HttpServletRequest request) {
		boolean isUserManager = isUserManagerOf(identityKey, request);
		if(!isUserManager) {
			return Response.serverError().status(Status.FORBIDDEN).build();
		}
		Identity identity = securityManager.loadIdentityByKey(identityKey, false);
		if(identity == null) {
			return Response.serverError().status(Status.NOT_FOUND).build();

		Preferences prefs = identity.getUser().getPreferences();
		prefs.setLanguage(preferences.getLanguage());
		userManager.updateUserFromIdentity(identity);
		return Response.ok(new PreferencesVO(prefs)).build();

	@Path("{identityKey}/relations")
	public IdentityToIdentityRelationsWebService getRelations(@PathParam("identityKey") Long identityKey, @Context HttpServletRequest request) {
		boolean isUserManager = isUserManagerOf(identityKey, request);
		if(!isUserManager) {
			throw new WebApplicationException(Status.FORBIDDEN);
		}
		Identity identity = securityManager.loadIdentityByKey(identityKey, false);
		if(identity == null) {
			throw new WebApplicationException(Status.NOT_FOUND);
		}
		return new IdentityToIdentityRelationsWebService(identity);
	}
Alan Moran's avatar
Alan Moran committed

	/**
	 * Retrieves an user given its unique key identifier
Alan Moran's avatar
Alan Moran committed
	 * @param identityKey The user key identifier of the user being searched
	 * @param withPortrait If true return the portrait as Base64 (default false)
Alan Moran's avatar
Alan Moran committed
	 * @param httpRequest The HTTP request
	 * @return an xml or json representation of a the user being search. The xml
	 *         correspond to a <code>UserVO</code>. <code>UserVO</code> is a
	 *         simplified representation of the <code>User</code> and <code>Identity</code>
	 */
	@GET
	@Path("{identityKey}")
	@Operation(summary = "Retrieve a user", description = "Retrieves an user given its unique key identifier")
	@ApiResponse(responseCode = "200", description = "The user", content = {
			@Content(mediaType = "application/json", schema = @Schema(implementation = UserVO.class)),
			@Content(mediaType = "application/xml", schema = @Schema(implementation = UserVO.class)) })
	@ApiResponse(responseCode = "401", description = "The roles of the authenticated user are not sufficient")
	@ApiResponse(responseCode = "404", description = "The identity not found")
Alan Moran's avatar
Alan Moran committed
	@Produces({MediaType.APPLICATION_XML ,MediaType.APPLICATION_JSON})
	public Response findById(@PathParam("identityKey") Long identityKey, @QueryParam("withPortrait") @DefaultValue("false") Boolean withPortrait,
			@Context HttpServletRequest httpRequest) {
		Identity identity = securityManager.loadIdentityByKey(identityKey, false);
		if(identity == null) {
			return Response.serverError().status(Status.NOT_FOUND).build();
Alan Moran's avatar
Alan Moran committed
		}
		boolean isUserManager = isUserManagerOf(identityKey, httpRequest);
		UserVO userVO = get(identity, null, true, isUserManager, withPortrait);
		return Response.ok(userVO).build();
	@Operation(summary = "Retrieve folders", description = "Retrieves folders from a user given its unique key identifier")
	@ApiResponse(responseCode = "200", description = "The folders")
	@ApiResponse(responseCode = "401", description = "The roles of the authenticated user are not sufficient")
	@ApiResponse(responseCode = "404", description = "The identity not found")
	public UserFoldersWebService getFoldersWebService(@PathParam("identityKey") Long identityKey) {
		Identity identity = securityManager.loadIdentityByKey(identityKey, false);
		if(identity == null) {
			throw new WebApplicationException(Response.serverError().status(Status.NOT_FOUND).build());
		}
		return new UserFoldersWebService(identity);
	}
	
	@Path("{identityKey}/courses")
	@Operation(summary = "Retrieve courses", description = "Retrieves courses from a user given its unique key identifier")
	@ApiResponse(responseCode = "200", description = "The courses")
	@ApiResponse(responseCode = "401", description = "The roles of the authenticated user are not sufficient")
	@ApiResponse(responseCode = "404", description = "The identity not found")
	public UserCoursesWebService getCoursesWebService(@PathParam("identityKey") Long identityKey,
			@Context HttpServletRequest httpRequest) {
		Identity identity = securityManager.loadIdentityByKey(identityKey, false);
		if(identity == null) {
			throw new WebApplicationException(Response.serverError().status(Status.NOT_FOUND).build());
		}

		Identity ureqIdentity = getIdentity(httpRequest);
		if(ureqIdentity == null || !ureqIdentity.equals(identity)) {
			throw new WebApplicationException(Response.serverError().status(Status.UNAUTHORIZED).build());
		}
		UserCoursesWebService ws = new UserCoursesWebService(identity);
		CoreSpringFactory.autowireObject(ws);
		return ws;
Alan Moran's avatar
Alan Moran committed
	/**
	 * Retrieves the portrait of an user
	 * @param identityKey The identity key of the user being searched
	 * @return The image
	 */
	@HEAD
	@Path("{identityKey}/portrait")
	@Operation(summary = "Retrieves the portrait of an user", description = "Retrieves the portrait of an user")
	@ApiResponse(responseCode = "200", description = "The portrait as image")
	@ApiResponse(responseCode = "404", description = "The identity or the portrait not found")
	@Produces({"image/jpeg","image/jpg",MediaType.APPLICATION_OCTET_STREAM})
	public Response getPortraitHead(@PathParam("identityKey") Long identityKey) {
		Identity identity = securityManager.loadIdentityByKey(identityKey);
		if(identity == null) {
			return Response.serverError().status(Status.NOT_FOUND).build();
		}
		
		File portrait = portraitManager.getBigPortrait(identity);
		if(portrait == null || !portrait.exists()) {
			return Response.serverError().status(Status.NOT_FOUND).build();

		Date lastModified = new Date(portrait.lastModified());
		return Response.ok().lastModified(lastModified).build();
	/**
	 * Retrieves the portrait of an user
	 * @param identityKey The identity key of the user being searched
	 * @return The image
	 */
	@HEAD
	@Path("{identityKey}/portrait/{size}")
	@Operation(summary = "Retrieves the portrait of an user", description = "Retrieves the portrait of an user")
	@ApiResponse(responseCode = "200", description = "The portrait as image")
	@ApiResponse(responseCode = "404", description = "The identity or the portrait not found")
	@Produces({"image/jpeg","image/jpg",MediaType.APPLICATION_OCTET_STREAM})
	public Response getOriginalPortraitHead(@PathParam("identityKey") Long identityKey, @PathParam("size") String size) {
		Identity identity = securityManager.loadIdentityByKey(identityKey);
		if(identity == null) {
			return Response.serverError().status(Status.NOT_FOUND).build();
		}
		File portrait = null;
		if("master".equals(size)) {
			portrait = portraitManager.getMasterPortrait(identity);
			portrait = portraitManager.getBigPortrait(identity);
			portrait = portraitManager.getSmallPortrait(identity);
		if(portrait == null || !portrait.exists()) {
			return Response.serverError().status(Status.NOT_FOUND).build();
		Date lastModified = new Date(portrait.lastModified());
		return Response.ok().lastModified(lastModified).build();
Alan Moran's avatar
Alan Moran committed
	 * @param identityKey The identity key of the user being searched
	 * @param request The REST request
	 * @return The image
	 */
	@GET
	@Path("{identityKey}/portrait")
	@Operation(summary = "Retrieves the portrait of an user", description = "Retrieves the portrait of an user")
	@ApiResponse(responseCode = "200", description = "The portrait as image")
	@ApiResponse(responseCode = "404", description = "The identity or the portrait not found")
Alan Moran's avatar
Alan Moran committed
	@Produces({"image/jpeg","image/jpg",MediaType.APPLICATION_OCTET_STREAM})
	public Response getPortrait(@PathParam("identityKey") Long identityKey, @Context Request request) {
		Identity identity = securityManager.loadIdentityByKey(identityKey);
		if(identity == null) {
			return Response.serverError().status(Status.NOT_FOUND).build();
		}
		
		File portrait = portraitManager.getBigPortrait(identity);
		if(portrait == null || !portrait.exists()) {
			return Response.serverError().status(Status.NOT_FOUND).build();
		}
		Date lastModified = new Date(portrait.lastModified());
		Response.ResponseBuilder response = request.evaluatePreconditions(lastModified);
		if(response == null) {
			response = Response.ok(portrait).lastModified(lastModified).cacheControl(cc);
Alan Moran's avatar
Alan Moran committed
		}
Alan Moran's avatar
Alan Moran committed
	}
	
	/**
	 * Upload the portrait of an user
Alan Moran's avatar
Alan Moran committed
	 * @param identityKey The user key identifier of the user being searched
	 * @param file The image
	 * @param request The REST request
	 * @return The image
	 */
	@POST
	@Path("{identityKey}/portrait")
	@Operation(summary = "Upload the portrait of an user", description = "Upload the portrait of an user")
	@ApiResponse(responseCode = "200", description = "The portrait as image")
	@ApiResponse(responseCode = "401", description = "Not authorized")
	@ApiResponse(responseCode = "404", description = "The identity or the portrait not found")
Alan Moran's avatar
Alan Moran committed
	@Consumes({MediaType.MULTIPART_FORM_DATA})
	public Response postPortrait(@PathParam("identityKey") Long identityKey, @Context HttpServletRequest request) {
		MultipartReader partsReader = null;
Alan Moran's avatar
Alan Moran committed
		try {
			Identity identity = securityManager.loadIdentityByKey(identityKey);
Alan Moran's avatar
Alan Moran committed
			if(identity == null) {
				return Response.serverError().status(Status.NOT_FOUND).build();
			}
			
			Identity authIdentity = getUserRequest(request).getIdentity();
			if(!isUserManagerOf(identityKey, request) && !identity.getKey().equals(authIdentity.getKey())) {
Alan Moran's avatar
Alan Moran committed
				return Response.serverError().status(Status.UNAUTHORIZED).build();
			}
			partsReader = new MultipartReader(request);
			File tmpFile = partsReader.getFile();
			String filename = partsReader.getFilename();
			portraitManager.setPortrait(tmpFile, filename, identity);
Alan Moran's avatar
Alan Moran committed
			return Response.ok().build();
Alan Moran's avatar
Alan Moran committed
			throw new WebApplicationException(e);
			MultipartReader.closeQuietly(partsReader);
		}
Alan Moran's avatar
Alan Moran committed
	}
	
	/**
	 * Deletes the portrait of an user
Alan Moran's avatar
Alan Moran committed
	 * @param identityKey The identity key identifier of the user being searched
	 * @param request The REST request
	 * @return The image
	 */
	@DELETE
	@Path("{identityKey}/portrait")
	@Operation(summary = "Deletes the portrait of an user", description = "Deletes the portrait of an user")
	@ApiResponse(responseCode = "200", description = "The portrait deleted")
	@ApiResponse(responseCode = "401", description = "Not authorized")
Alan Moran's avatar
Alan Moran committed
	public Response deletePortrait(@PathParam("identityKey") Long identityKey, @Context HttpServletRequest request) {
		Identity authIdentity = getUserRequest(request).getIdentity();
		Identity identity = securityManager.loadIdentityByKey(identityKey, false);
		if(identity == null) {
			return Response.serverError().status(Status.NOT_FOUND).build();
		} else if(!isUserManagerOf(identityKey, request) && !identity.equalsByPersistableKey(authIdentity)) {
			return Response.serverError().status(Status.UNAUTHORIZED).build();
		}
	
		portraitManager.deletePortrait(identity);
Alan Moran's avatar
Alan Moran committed
	}

	@Path("{identityKey}/groups")
	public MyGroupWebService getUserGroupList(@PathParam("identityKey") Long identityKey) {
		Identity retrievedUser = securityManager.loadIdentityByKey(identityKey, false);
Alan Moran's avatar
Alan Moran committed
		if(retrievedUser == null) {
Alan Moran's avatar
Alan Moran committed
		}
		MyGroupWebService ws = new MyGroupWebService(retrievedUser);
		CoreSpringFactory.autowireObject(ws);
		return ws;
Alan Moran's avatar
Alan Moran committed
	}

	/**
	 * Update an user
Alan Moran's avatar
Alan Moran committed
	 * @param identityKey The user key identifier
	 * @param user The user datas
	 * @param request The HTTP request
	 * @return <code>User</code> object. The operation status (success or fail)
	 */
	@POST
	@Path("{identityKey}")
	@Operation(summary = "Update an user", description = "Update an user")
	@ApiResponse(responseCode = "200", description = "The user", content = {
			@Content(mediaType = "application/json", schema = @Schema(implementation = UserVO.class)),
			@Content(mediaType = "application/xml", schema = @Schema(implementation = UserVO.class)) })
	@ApiResponse(responseCode = "401", description = "The roles of the authenticated user are not sufficient")
	@ApiResponse(responseCode = "404", description = "The identity not found")
	@ApiResponse(responseCode = "406", description = "The list of validation errors", content = {
			@Content(mediaType = "application/json", array = @ArraySchema(schema = @Schema(implementation = ErrorVO.class))),
			@Content(mediaType = "application/xml", array = @ArraySchema(schema = @Schema(implementation = ErrorVO.class))) })
Alan Moran's avatar
Alan Moran committed
	@Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
	@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
	public Response update(@PathParam("identityKey") Long identityKey, UserVO user, @Context HttpServletRequest request) {
		if(user == null) {
			return Response.serverError().status(Status.NO_CONTENT).build();
		}
		if(!isUserManagerOf(identityKey, request)) {
			return Response.serverError().status(Status.UNAUTHORIZED).build();
		}
		Identity retrievedIdentity = securityManager.loadIdentityByKey(identityKey, false);
		if(retrievedIdentity == null) {
			return Response.serverError().status(Status.NOT_FOUND).build();
		}
		
		User retrievedUser = retrievedIdentity.getUser();
		List<ErrorVO> errors = validateUser(retrievedUser, user, request);
		if(errors.isEmpty()) {
			if(StringHelper.containsNonWhitespace(user.getExternalId())
					&& !user.getExternalId().equals(retrievedIdentity.getExternalId())) {
				retrievedIdentity = securityManager.setExternalId(retrievedIdentity, user.getExternalId());
				retrievedUser = retrievedIdentity.getUser();
Alan Moran's avatar
Alan Moran committed
			}
			String oldEmail = retrievedUser.getEmail();
			post(retrievedUser, user, getLocale(request));
			userManager.updateUser(retrievedUser);
			securityManager.deleteInvalidAuthenticationsByEmail(oldEmail);
			return Response.ok(get(retrievedIdentity, true, true)).build();
Alan Moran's avatar
Alan Moran committed
		}
		
		//content not ok
		ErrorVO[] errorVos = new ErrorVO[errors.size()];
		errors.toArray(errorVos);
		return Response.ok(errorVos).status(Status.NOT_ACCEPTABLE).build();
Alan Moran's avatar
Alan Moran committed
	}
	private List<ErrorVO> validateUser(User user, UserVO userVo, HttpServletRequest request) {
Alan Moran's avatar
Alan Moran committed
		Locale locale = getLocale(request);
		List<UserPropertyHandler> propertyHandlers = userManager.getUserPropertyHandlersFor(PROPERTY_HANDLER_IDENTIFIER, false);
		validateProperty(user, UserConstants.FIRSTNAME, userVo.getFirstName(), propertyHandlers, errors, userManager, locale);
		validateProperty(user, UserConstants.LASTNAME, userVo.getLastName(), propertyHandlers, errors, userManager, locale);
		validateProperty(user, UserConstants.EMAIL, userVo.getEmail(), propertyHandlers, errors, userManager, locale);
Alan Moran's avatar
Alan Moran committed
		for (UserPropertyHandler propertyHandler : propertyHandlers) {
			if(!UserConstants.FIRSTNAME.equals(propertyHandler.getName())
					&& !UserConstants.LASTNAME.equals(propertyHandler.getName())
					&& !UserConstants.EMAIL.equals(propertyHandler.getName())) {
				validateProperty(user, userVo, propertyHandler, errors, userManager, locale);
Alan Moran's avatar
Alan Moran committed
			}
		}
		return errors;
	}
	
	private boolean validateProperty(User user, String name, String value, List<UserPropertyHandler> handlers, List<ErrorVO> errors, UserManager um, Locale locale) {
Alan Moran's avatar
Alan Moran committed
		for(UserPropertyHandler handler:handlers) {
			if(handler.getName().equals(name)) {
				return validateProperty(user, value, handler, errors, um, locale);
Alan Moran's avatar
Alan Moran committed
			}
		}
		return true;
	}
	
	private boolean validateProperty(User user, UserVO userVo, UserPropertyHandler userPropertyHandler, List<ErrorVO> errors, UserManager um, Locale locale) {
		String value = userVo.getProperty(userPropertyHandler.getName());
		return validateProperty(user, value, userPropertyHandler, errors, um, locale);
	private boolean validateProperty(User user, String value, UserPropertyHandler userPropertyHandler, List<ErrorVO> errors, UserManager um, Locale locale) {
Alan Moran's avatar
Alan Moran committed
		ValidationError error = new ValidationError();
		if(!StringHelper.containsNonWhitespace(value) && um.isMandatoryUserProperty(PROPERTY_HANDLER_IDENTIFIER, userPropertyHandler)) {
			Translator translator = new PackageTranslator("org.olat.core", locale);
			String translation = translator.translate("new.form.mandatory");
			errors.add(new ErrorVO("org.olat.core:new.form.mandatory:" + userPropertyHandler.getName(), translation));
Alan Moran's avatar
Alan Moran committed
			return false;
		}
		
		value = parseUserProperty(value, userPropertyHandler, locale);
		
		if (!userPropertyHandler.isValidValue(user, value, error, locale)) {
Alan Moran's avatar
Alan Moran committed
			String pack = userPropertyHandler.getClass().getPackage().getName();
			Translator translator = new PackageTranslator(pack, locale);
			String translation = translator.translate(error.getErrorKey(), error.getArgs());
Alan Moran's avatar
Alan Moran committed
			errors.add(new ErrorVO(pack, error.getErrorKey(), translation));
			return false;
		} else if((userPropertyHandler.getName().equals(UserConstants.INSTITUTIONALEMAIL) && StringHelper.containsNonWhitespace(value)) 
				|| userPropertyHandler.getName().equals(UserConstants.EMAIL)) {
			if (!userManager.isEmailAllowed(value, user)) {
				String pack = userPropertyHandler.getClass().getPackage().getName();
				Translator translator = new PackageTranslator(pack, locale);
				String translation = translator.translate("form.name." + userPropertyHandler.getName() + ".error.exists", new String[] { value });
				translation += " (" + value + ")";
				errors.add(new ErrorVO("org.olat.user.propertyhandlers:new.form.name." + userPropertyHandler.getName() + ".exists", translation));
			}
Alan Moran's avatar
Alan Moran committed
		}
		
		return true;
	}

	/**
	 * Delete an user from the system
Alan Moran's avatar
Alan Moran committed
	 * @param identityKey The user key identifier
	 * @param request The HTTP request
	 * @return <code>Response</code> object. The operation status (success or fail)
	 */
	@DELETE
	@Path("{identityKey}")
	@Operation(summary = "Delete an user from the system", description = "Delete an user from the system")
	@ApiResponse(responseCode = "200", description = "The user is removed from the group")
	@ApiResponse(responseCode = "401", description = "he roles of the authenticated user are not sufficient")
	@ApiResponse(responseCode = "404", description = "The identity not found")
	@ApiResponse(responseCode = "500", description = "Unknown problem while deleting, see olat.log")
Alan Moran's avatar
Alan Moran committed
	public Response delete(@PathParam("identityKey") Long identityKey, @Context HttpServletRequest request) {
		if(actingIdentity == null || !isUserManagerOf(identityKey, request)) {
Alan Moran's avatar
Alan Moran committed
			return Response.serverError().status(Status.UNAUTHORIZED).build();
		}
		
		Identity identity = securityManager.loadIdentityByKey(identityKey, false);
Alan Moran's avatar
Alan Moran committed
		if(identity == null) {
			return Response.serverError().status(Status.NOT_FOUND).build();
		}
		boolean success = userLifecycleManager.deleteIdentity(identity, actingIdentity);
		if (success) {
			return Response.ok().build();			
		} else {
			return Response.serverError().status(Status.INTERNAL_SERVER_ERROR).build();
		}
Alan Moran's avatar
Alan Moran committed
	}
	
	private boolean isUserManager(HttpServletRequest request) {
		Roles managerRoles = getRoles(request);
		return managerRoles.isUserManager() || managerRoles.isRolesManager() || managerRoles.isAdministrator();
	}
	
	private boolean isUserManagerOf(Long identityKey, HttpServletRequest request) {
		if(identityKey == null) return false;
		
		Roles managerRoles = getRoles(request);
		if(!managerRoles.isUserManager() && !managerRoles.isRolesManager() && !managerRoles.isAdministrator()) {
			return false;