/** * $Id: UserEntityProvider.java 130611 2013-10-18 14:17:53Z azeckoski@unicon.net $ * $URL: https://source.sakaiproject.org/svn/entitybroker/trunk/core-providers/src/java/org/sakaiproject/entitybroker/providers/UserEntityProvider.java $ * UserEntityProvider.java - entity-broker - Jun 28, 2008 2:59:57 PM - azeckoski ************************************************************************** * Copyright (c) 2008, 2009 The Sakai Foundation * * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.entitybroker.providers; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.azeckoski.reflectutils.FieldUtils; import org.azeckoski.reflectutils.ReflectUtils; import org.sakaiproject.component.api.ServerConfigurationService; import org.sakaiproject.entity.api.ResourcePropertiesEdit; import org.sakaiproject.entitybroker.DeveloperHelperService; import org.sakaiproject.entitybroker.EntityReference; import org.sakaiproject.entitybroker.EntityView; import org.sakaiproject.entitybroker.entityprovider.CoreEntityProvider; import org.sakaiproject.entitybroker.entityprovider.annotations.EntityCustomAction; import org.sakaiproject.entitybroker.entityprovider.capabilities.Describeable; import org.sakaiproject.entitybroker.entityprovider.capabilities.RESTful; import org.sakaiproject.entitybroker.entityprovider.extension.ActionReturn; import org.sakaiproject.entitybroker.entityprovider.extension.Formats; import org.sakaiproject.entitybroker.entityprovider.search.Restriction; import org.sakaiproject.entitybroker.entityprovider.search.Search; import org.sakaiproject.entitybroker.providers.model.EntityUser; import org.sakaiproject.entitybroker.util.AbstractEntityProvider; import org.sakaiproject.user.api.User; import org.sakaiproject.user.api.UserAlreadyDefinedException; import org.sakaiproject.user.api.UserDirectoryService; import org.sakaiproject.user.api.UserDirectoryService.PasswordRating; import org.sakaiproject.user.api.UserEdit; import org.sakaiproject.user.api.UserIdInvalidException; import org.sakaiproject.user.api.UserLockedException; import org.sakaiproject.user.api.UserNotDefinedException; import org.sakaiproject.user.api.UserPermissionException; /** * Entity Provider for users * * @author Aaron Zeckoski (azeckoski @ gmail.com) */ public class UserEntityProvider extends AbstractEntityProvider implements CoreEntityProvider, RESTful, Describeable { private static Log log = LogFactory.getLog(UserEntityProvider.class); private static final String ID_PREFIX = "id="; private UserDirectoryService userDirectoryService; public void setUserDirectoryService(UserDirectoryService userDirectoryService) { this.userDirectoryService = userDirectoryService; } private DeveloperHelperService developerHelperService; public void setDeveloperHelperService( DeveloperHelperService developerHelperService) { this.developerHelperService = developerHelperService; } private ServerConfigurationService serverConfigurationService; public void setServerConfigurationService( ServerConfigurationService serverConfigurationService) { this.serverConfigurationService = serverConfigurationService; } public static String PREFIX = "user"; public String getEntityPrefix() { return PREFIX; } @EntityCustomAction(action="current",viewKey=EntityView.VIEW_LIST) public EntityUser getCurrentUser(EntityView view) { EntityUser eu = new EntityUser(userDirectoryService.getCurrentUser()); return eu; } @EntityCustomAction(action="exists", viewKey=EntityView.VIEW_SHOW) public boolean checkUserExists(EntityView view) { String userId = view.getEntityReference().getId(); userId = findAndCheckUserId(userId, null); boolean exists = (userId != null); return exists; } @EntityCustomAction(action="validatePassword", viewKey=EntityView.VIEW_NEW) public ActionReturn validatePassword(EntityView view, Map<String, Object> params) { PasswordRating rating = PasswordRating.PASSED_DEFAULT; if (!params.containsKey("password")) { throw new IllegalArgumentException("Must include a 'password' to validate"); } String password = (String) params.get("password"); User user = null; if (params.containsKey("username")) { String username = (String) params.get("username"); user = new EntityUser(username, null, null, null, username, password, null); } rating = userDirectoryService.validatePassword(password, user); return new ActionReturn(rating.name()); } public boolean entityExists(String id) { if (id == null) { return false; } if ("".equals(id)) { return true; } String userId = findAndCheckUserId(id, null); if (userId != null) { return true; } return false; } public String createEntity(EntityReference ref, Object entity, Map<String, Object> params) { String userId = null; if (ref.getId() != null && ref.getId().length() > 0) { userId = ref.getId(); } if (entity.getClass().isAssignableFrom(User.class)) { // if someone passes in a user or useredit User user = (User) entity; if (userId == null && user.getId() != null) { userId = user.getId(); } //check if this user can add an account of this type if (!canAddAccountType(user.getType())) { throw new SecurityException("User can't add an account of type: " + user.getType()); } // NOTE: must assign empty password if user is created this way.... it sucks -AZ try { User newUser = userDirectoryService.addUser(userId, user.getEid(), user.getFirstName(), user.getLastName(), user.getEmail(), "", user.getType(), user.getProperties()); userId = newUser.getId(); } catch (UserIdInvalidException e) { throw new IllegalArgumentException("User ID is invalid, id=" + user.getId() + ", eid="+user.getEid(), e); } catch (UserAlreadyDefinedException e) { throw new IllegalArgumentException("Cannot create user, user already exists: " + ref, e); } catch (UserPermissionException e) { throw new SecurityException("Could not create user, permission denied: " + ref, e); } } else if (entity.getClass().isAssignableFrom(EntityUser.class)) { // if they instead pass in the EntityUser object EntityUser user = (EntityUser) entity; if (userId == null && user.getId() != null) { userId = user.getId(); } //check if this user can add an account of this type if (!canAddAccountType(user.getType())) { throw new SecurityException("User can't add an account of type: " + user.getType()); } try { UserEdit edit = userDirectoryService.addUser(userId, user.getEid()); edit.setEmail(user.getEmail()); edit.setFirstName(user.getFirstName()); edit.setLastName(user.getLastName()); edit.setPassword(user.getPassword()); edit.setType(user.getType()); // put in properties ResourcePropertiesEdit rpe = edit.getPropertiesEdit(); for (String key : user.getProps().keySet()) { String value = user.getProps().get(key); rpe.addProperty(key, value); } userDirectoryService.commitEdit(edit); userId = edit.getId(); } catch (UserIdInvalidException e) { throw new IllegalArgumentException("User ID is invalid: " + user.getId(), e); } catch (UserAlreadyDefinedException e) { throw new IllegalArgumentException("Cannot create user, user already exists: " + ref, e); } catch (UserPermissionException e) { throw new SecurityException("Could not create user, permission denied: " + ref, e); } } else { throw new IllegalArgumentException("Invalid entity for creation, must be User or EntityUser object"); } return userId; } public Object getSampleEntity() { return new EntityUser(); } public void updateEntity(EntityReference ref, Object entity, Map<String, Object> params) { String userId = ref.getId(); if (userId == null || "".equals(userId)) { throw new IllegalArgumentException("Cannot update, No userId in provided reference: " + ref); } User user = getUserByIdEid(userId); UserEdit edit = null; try { edit = userDirectoryService.editUser(user.getId()); } catch (UserNotDefinedException e) { throw new IllegalArgumentException("Invalid user: " + ref + ":" + e.getMessage()); } catch (UserPermissionException e) { throw new SecurityException("Permission denied: User cannot be updated: " + ref); } catch (UserLockedException e) { throw new RuntimeException("Something strange has failed with Sakai: " + e.getMessage()); } if (entity.getClass().isAssignableFrom(User.class)) { // if someone passes in a user or useredit User u = (User) entity; edit.setEmail(u.getEmail()); edit.setFirstName(u.getFirstName()); edit.setLastName(u.getLastName()); edit.setType(u.getType()); // put in properties ResourcePropertiesEdit rpe = edit.getPropertiesEdit(); rpe.set(u.getProperties()); } else if (entity.getClass().isAssignableFrom(EntityUser.class)) { // if they instead pass in the myuser object EntityUser u = (EntityUser) entity; edit.setEmail(u.getEmail()); edit.setFirstName(u.getFirstName()); edit.setLastName(u.getLastName()); edit.setPassword(u.getPassword()); edit.setType(u.getType()); // put in properties ResourcePropertiesEdit rpe = edit.getPropertiesEdit(); for (String key : u.getProps().keySet()) { String value = u.getProps().get(key); rpe.addProperty(key, value); } } else { throw new IllegalArgumentException("Invalid entity for update, must be User or EntityUser object"); } try { userDirectoryService.commitEdit(edit); } catch (UserAlreadyDefinedException e) { throw new RuntimeException(ref + ": This exception should not be possible: " + e.getMessage(), e); } } public void deleteEntity(EntityReference ref, Map<String, Object> params) { String userId = ref.getId(); if (userId == null || "".equals(userId)) { throw new IllegalArgumentException("Cannot delete, No userId in provided reference: " + ref); } User user = getUserByIdEid(userId); if (user != null) { try { UserEdit edit = userDirectoryService.editUser(user.getId()); userDirectoryService.removeUser(edit); } catch (UserNotDefinedException e) { throw new IllegalArgumentException("Invalid user: " + ref + ":" + e.getMessage()); } catch (UserPermissionException e) { throw new SecurityException("Permission denied: User cannot be removed: " + ref); } catch (UserLockedException e) { throw new RuntimeException("Something strange has failed with Sakai: " + e.getMessage()); } } } public Object getEntity(EntityReference ref) { if (ref.getId() == null) { return new EntityUser(); } String userId = ref.getId(); User user = getUserByIdEid(userId); if (developerHelperService.isEntityRequestInternal(ref.toString())) { // internal lookups are allowed to get everything } else { // external lookups require auth boolean allowed = false; String currentUserRef = developerHelperService.getCurrentUserReference(); if (currentUserRef != null) { String currentUserId = developerHelperService.getUserIdFromRef(currentUserRef); if (developerHelperService.isUserAdmin(currentUserId) || currentUserId.equals(user.getId())) { // allowed to access the user data allowed = true; } } if (! allowed) { throw new SecurityException("Current user ("+currentUserRef+") cannot access information about user: " + ref); } } // convert EntityUser eu = convertUser(user); return eu; } /** * WARNING: The search results may be drawn from different populations depending on the * search parameters specified. A straight listing with no filtering, or a search on "search" * or "criteria", will only retrieve matches from the Sakai-maintained user records. A search * on "email" may also check the records maintained by the user directory provider. */ public List<?> getEntities(EntityReference ref, Search search) { Collection<User> users = new ArrayList<User>(); if (developerHelperService.getConfigurationSetting("entity.users.viewall", false)) { // setting bypasses all checks } else if (developerHelperService.isEntityRequestInternal(ref.toString())) { // internal lookups are allowed to get everything } else { // external lookups require auth boolean allowed = false; String currentUserRef = developerHelperService.getCurrentUserReference(); if (currentUserRef != null) { String currentUserId = developerHelperService.getUserIdFromRef(currentUserRef); if ( developerHelperService.isUserAdmin(currentUserId) ) { // allowed to access the user data allowed = true; } } if (! allowed) { throw new SecurityException("Only admin can access multiple users, current user ("+currentUserRef+") cannot access ref: " + ref); } } // fix up the search limits if (search.getLimit() > 50 || search.getLimit() == 0) { search.setLimit(50); } if (search.getStart() == 0 || search.getStart() > 49) { search.setStart(1); } // get the search restrictions out Restriction restrict = search.getRestrictionByProperty("email"); if (restrict != null) { // search users by email users = userDirectoryService.findUsersByEmail(restrict.value.toString()); } if (restrict == null) { restrict = search.getRestrictionByProperty("eid"); if (restrict == null) { restrict = search.getRestrictionByProperty("search"); } if (restrict == null) { restrict = search.getRestrictionByProperty("criteria"); } if (restrict != null) { // search users but match users = userDirectoryService.searchUsers(restrict.value + "", (int) search.getStart(), (int) search.getLimit()); } } if (restrict == null) { users = userDirectoryService.getUsers((int) search.getStart(), (int) search.getLimit()); } // convert these into EntityUser objects List<EntityUser> entityUsers = new ArrayList<EntityUser>(); for (User user : users) { entityUsers.add( convertUser(user) ); } return entityUsers; } public String[] getHandledInputFormats() { return new String[] { Formats.HTML, Formats.XML, Formats.JSON }; } public String[] getHandledOutputFormats() { return new String[] { Formats.XML, Formats.JSON, Formats.FORM }; } /** * Allows for easy retrieval of the user object * @param userId a user ID (must be internal ID only and not EID) * @return the user object * @throws IllegalArgumentException if the user Id is invalid */ public EntityUser getUserById(String userId) { userId = findAndCheckUserId(userId, null); // we could have been passed a Id that no longer refers to a user if (userId == null) { return null; } /* Switched this to ID only lookup without failover to EID lookup - SAK-21654 EntityReference ref = new EntityReference("user", userId); EntityUser eu = (EntityUser) getEntity(ref); */ // ID only lookup so prefix with "id=" User user = getUserByIdEid(ID_PREFIX+userId); // convert EntityUser eu = convertUser(user); return eu; } /* * This ugliness is needed because of the edge case where people are using identical ID/EIDs, * this is a really really bad hack to attempt to get the server to tell us if the eid==id for users */ private Boolean usesSeparateIdEid = null; private boolean isUsingSameIdEid() { if (usesSeparateIdEid == null) { String config = developerHelperService.getConfigurationSetting("separateIdEid@org.sakaiproject.user.api.UserDirectoryService", (String)null); if (config != null) { try { usesSeparateIdEid = ReflectUtils.getInstance().convert(config, Boolean.class); } catch (UnsupportedOperationException e) { // oh well usesSeparateIdEid = null; } } if (usesSeparateIdEid == null) { // could not get the stupid setting so attempt to check the service itself try { usesSeparateIdEid = FieldUtils.getInstance().getFieldValue(userDirectoryService, "m_separateIdEid", Boolean.class); } catch (RuntimeException e) { // no luck here usesSeparateIdEid = null; } } if (usesSeparateIdEid == null) usesSeparateIdEid = Boolean.FALSE; } return ! usesSeparateIdEid.booleanValue(); } /** * Will check that a userId/eid is valid and will produce a valid userId from the check * @param currentUserId user id (can be eid) * @param currentUserEid user eid (can be id) * @return a valid user id OR null if not valid */ public String findAndCheckUserId(String currentUserId, String currentUserEid) { if (currentUserId == null && currentUserEid == null) { throw new IllegalArgumentException("Cannot get user from a null userId and eid, ensure at least userId or userEid are set"); } String userId = null; // can use the efficient methods to check if the user Id is valid if (currentUserId == null) { // We should assume we will resolve by EID if (log.isDebugEnabled()) log.debug("currentUserId is null, currentUserEid=" + currentUserEid, new Exception()); // try to get userId from eid if (currentUserEid.startsWith("/user/")) { // assume the form of "/user/userId" (the UDS method is protected) currentUserEid = new EntityReference(currentUserEid).getId(); } if (isUsingSameIdEid()) { // have to actually fetch the user User u; try { u = getUserByIdEid(currentUserEid); if (u != null) { userId = u.getId(); } } catch (IllegalArgumentException e) { userId = null; } } else { if (userIdExplicitOnly()) { // only check ID or EID if (currentUserEid.length() > ID_PREFIX.length() && currentUserEid.startsWith(ID_PREFIX) ) { // strip the id marker out currentUserEid = currentUserEid.substring(ID_PREFIX.length()); // check ID, do not attempt to check by EID as well try { userId = userDirectoryService.getUserId(currentUserEid); } catch (UserNotDefinedException e2) { userId = null; } } else { // check by EID try { userDirectoryService.getUserEid(currentUserEid); // simply here to throw an exception or not userId = currentUserEid; } catch (UserNotDefinedException e2) { userId = null; } } } else { // check for EID and then ID try { userId = userDirectoryService.getUserId(currentUserEid); } catch (UserNotDefinedException e) { try { userDirectoryService.getUserEid(currentUserEid); // simply here to throw an exception or not userId = currentUserEid; } catch (UserNotDefinedException e2) { userId = null; } } } } } else { // Assume we will resolve by ID // get the id out of a ref if (currentUserId.startsWith("/user/")) { // assume the form of "/user/userId" (the UDS method is protected) currentUserId = new EntityReference(currentUserId).getId(); } // verify the userId is valid if (isUsingSameIdEid()) { // have to actually fetch the user try { User u = getUserByIdEid(currentUserId); if (u != null) { userId = u.getId(); } } catch (IllegalArgumentException e) { userId = null; } } else { if (userIdExplicitOnly()) { if (currentUserId.length() > ID_PREFIX.length() && currentUserId.startsWith(ID_PREFIX) ) { // strip the id marker out currentUserId = currentUserId.substring(ID_PREFIX.length()); } // check ID, do not attempt to check by EID as well try { userDirectoryService.getUserEid(currentUserId); // simply here to throw an exception or not userId = currentUserId; } catch (UserNotDefinedException e2) { userId = null; } } else { // check for ID and then EID try { userDirectoryService.getUserEid(currentUserId); // simply here to throw an exception or not userId = currentUserId; } catch (UserNotDefinedException e) { try { userId = userDirectoryService.getUserId(currentUserId); } catch (UserNotDefinedException e2) { userId = null; } } } } } return userId; } /** * @param userSearchValue either a user ID, a user EID, or a user email address * @return the first matching user, or null if no search method worked */ public EntityUser findUserFromSearchValue(String userSearchValue) { EntityUser entityUser; User user; try { user = userDirectoryService.getUser(userSearchValue); } catch (UserNotDefinedException e) { try { user = userDirectoryService.getUserByEid(userSearchValue); } catch (UserNotDefinedException e1) { user = null; } } if (user == null) { Collection<User> users = userDirectoryService.findUsersByEmail(userSearchValue); if ((users != null) && (users.size() > 0)) { user = users.iterator().next(); if (users.size() > 1) { if (log.isWarnEnabled()) log.warn("Found multiple users with email " + userSearchValue); } } } if (user != null) { entityUser = convertUser(user); } else { entityUser = null; } return entityUser; } public EntityUser convertUser(User user) { EntityUser eu = new EntityUser(user); return eu; } /** * Attempt to get a user by EID or ID (if that fails) * * NOTE: can force this to only attempt the ID lookups if prefixed with "id=" using "user.explicit.id.only=true" * * @param userEid the user EID (could also be the ID) * @return the populated User object */ private User getUserByIdEid(String userEid) { User user = null; if (userEid != null) { boolean doCheckForId = false; boolean doCheckForEid = true; String userId = userEid; // check if the incoming param says this is explicitly an id if (userId.length() > ID_PREFIX.length() && userId.startsWith(ID_PREFIX) ) { // strip the id marker out userId = userEid.substring(ID_PREFIX.length()); doCheckForEid = false; // skip the EID check entirely doCheckForId = true; } // attempt checking both with failover by default (override by property "user.id.failover.check=false") if (doCheckForEid) { try { user = userDirectoryService.getUserByEid(userEid); } catch (UserNotDefinedException e) { user = null; //String msg = "Could not find user with eid="+userEid; if (!userIdExplicitOnly()) { //msg += " (attempting check using user id="+userId+")"; doCheckForId = true; } // SAK-22690 removed this log warning //msg += " :: " + e.getMessage(); //log.warn(msg); } } if (doCheckForId) { try { user = userDirectoryService.getUser(userId); } catch (UserNotDefinedException e) { user = null; // SAK-22690 removed this log warning //String msg = "Could not find user with id="+userId+" :: " + e.getMessage(); //log.warn(msg); } } if (user == null) { throw new IllegalArgumentException("Could not find user with eid="+userEid+" or id="+userId); } } return user; } /** * Can the current user add an account of this type see KNL-357 * @param type * @return */ private boolean canAddAccountType(String type) { log.debug("canAddAccountType(" + type + ")"); //admin can always add users if (developerHelperService.isUserAdmin(developerHelperService.getCurrentUserReference())) { log.debug("Admin user is allowed!"); return true; } String currentSessionUserId = developerHelperService.getCurrentUserId(); log.debug("checking if " + currentSessionUserId + " can add account of type: " + type); //this may be an anonymous session registering if (currentSessionUserId == null) { String regAccountTypes = serverConfigurationService.getString("user.registrationTypes", "registered"); List<String> regTypes = Arrays.asList(regAccountTypes.split(",")); if (! regTypes.contains(type)) { log.warn("Anonamous user can't create an account of type: " + type + ", allowed types: " + regAccountTypes); return false; } } else { //this is a authenticated non-admin user String newAccountTypes = serverConfigurationService.getString("user.nonAdminTypes", "guest"); List<String> newTypes = Arrays.asList(newAccountTypes.split(",")); if (! newTypes.contains(type)) { log.warn("User " + currentSessionUserId + " can't create an account of type: " + type +" with eid , allowed types: " + newAccountTypes); return false; } } return true; } /** * Checks the sakai.properties setting for: "user.explicit.id.only", * set this to true to disable id/eid failover checks (this means lookups will only attempt to use * id or eid as per the exact params which are passed or as per the endpoint API) * * In other words, the user id must be prefixed with "id=", otherwise it will be treated like an eid * * @return true if user ID must be passed explicitly (no id/eid failover checks are allowed), default: false */ private boolean userIdExplicitOnly() { boolean allowed = developerHelperService.getConfigurationSetting("user.explicit.id.only", false); return allowed; } }