package org.gbif.ipt.service.admin.impl; import org.gbif.ipt.config.AppConfig; import org.gbif.ipt.config.DataDir; import org.gbif.ipt.model.Resource; import org.gbif.ipt.model.User; import org.gbif.ipt.model.User.Role; import org.gbif.ipt.model.converter.PasswordConverter; import org.gbif.ipt.service.AlreadyExistingException; import org.gbif.ipt.service.BaseManager; import org.gbif.ipt.service.DeletionNotAllowedException; import org.gbif.ipt.service.DeletionNotAllowedException.Reason; import org.gbif.ipt.service.InvalidConfigException; import org.gbif.ipt.service.InvalidConfigException.TYPE; import org.gbif.ipt.service.admin.UserAccountManager; import org.gbif.ipt.service.manage.ResourceManager; import org.gbif.ipt.utils.FileUtils; import java.io.EOFException; import java.io.FileNotFoundException; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Reader; import java.io.Writer; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import com.google.common.collect.Sets; import com.google.inject.Inject; import com.google.inject.Singleton; import com.thoughtworks.xstream.XStream; /** * Reads user accounts from a simple XStream managed xml file. */ @Singleton public class UserAccountManagerImpl extends BaseManager implements UserAccountManager { public static final String PERSISTENCE_FILE = "users.xml"; private Map<String, User> users = new LinkedHashMap<String, User>(); private boolean allowSimplifiedAdminLogin = true; private String onlyAdminEmail; private final XStream xstream = new XStream(); private ResourceManager resourceManager; private User setupUser; @Inject public UserAccountManagerImpl(AppConfig cfg, DataDir dataDir, ResourceManager resourceManager, PasswordConverter passwordConverter) { super(cfg, dataDir); this.resourceManager = resourceManager; defineXstreamMapping(passwordConverter); } private User addUser(User user) { if (user != null) { if (user.getRole() == Role.Admin) { log.debug("Adding admin " + user.getEmail()); if (allowSimplifiedAdminLogin) { if (onlyAdminEmail == null) { // first admin - keep its email address available for simplified login // without email address but keyword "admin" onlyAdminEmail = user.getEmail(); } else { // at least 2 admins exist - disable simplified admin login onlyAdminEmail = null; allowSimplifiedAdminLogin = false; } } } else { log.debug("Adding user " + user.getEmail()); } users.put(user.getEmail().toLowerCase(), user); } return user; } public User authenticate(String email, String password) { if (allowSimplifiedAdminLogin && email != null && email.equalsIgnoreCase("admin")) { // lookup the admins email address# email = onlyAdminEmail; } User agent = get(email); if (agent != null && agent.getPassword() != null && agent.getPassword().equals(password)) { return agent; } return null; } public void create(User user) throws AlreadyExistingException, IOException { if (user != null) { if (get(user.getEmail()) != null) { throw new AlreadyExistingException(); } addUser(user); save(); } } private void defineXstreamMapping(PasswordConverter passwordConverter) { xstream.alias("user", User.class); xstream.useAttributeFor(User.class, "email"); xstream.useAttributeFor(User.class, "password"); xstream.useAttributeFor(User.class, "firstname"); xstream.useAttributeFor(User.class, "lastname"); xstream.useAttributeFor(User.class, "role"); xstream.useAttributeFor(User.class, "lastLogin"); // encrypt passwords xstream.registerConverter(passwordConverter); } public User delete(String email) throws DeletionNotAllowedException, IOException { if (email != null) { User remUser = get(email); if (remUser != null) { // when deleting an admin, ensure another admin still exists if (remUser.getRole() == Role.Admin) { boolean lastAdmin = true; for (User u : users.values()) { if (u.getRole() == Role.Admin && !u.equals(remUser)) { lastAdmin = false; break; } } if (lastAdmin) { log.warn("Last admin cannot be deleted"); throw new DeletionNotAllowedException(Reason.LAST_ADMIN); } } Set<String> resourcesCreatedByUser = new HashSet<String>(); for (Resource r : resourceManager.list()) { User creator = get(r.getCreator().getEmail()); if (creator != null && creator.equals(remUser)) { resourcesCreatedByUser.add(r.getShortname()); } } Set<String> resourcesManagedOnlyByUser = new HashSet<String>(); for (Resource r : resourceManager.list(remUser)) { Set<User> managers = Sets.newHashSet(); // add creator to list of managers, but only if creator has manager rights! User creator = get(r.getCreator().getEmail()); if (creator != null && creator.hasManagerRights()) { managers.add(creator); } for (User m : r.getManagers()) { User manager = get(m.getEmail()); if (manager != null && !managers.contains(manager)) { managers.add(manager); } } // lastly, exclude user to be deleted, then check if at least one user with manager rights remains for resource managers.remove(remUser); if (managers.isEmpty()) { resourcesManagedOnlyByUser.add(r.getShortname()); } } if (!resourcesManagedOnlyByUser.isEmpty()) { // Check #1, is user the only manager that exists for or more resources? If yes, prevent deletion! throw new DeletionNotAllowedException(Reason.LAST_RESOURCE_MANAGER, resourcesManagedOnlyByUser.toString()); } else if (!resourcesCreatedByUser.isEmpty()) { // Check #2, is user the creator of one or more resources? If yes, prevent deletion! throw new DeletionNotAllowedException(Reason.IS_RESOURCE_CREATOR, resourcesCreatedByUser.toString()); } else if (remove(email)) { // and remove user from each resource's list of managers for (Resource r : resourceManager.list(remUser)) { r.getManagers().remove(remUser); resourceManager.save(r); } save(); // persist changes to users.xml return remUser; } } } return null; } /** * Retrieve user from internal hash. * * @param email email of user account to retrieve * * @return User if found, null otherwise */ public User get(String email) { if (email != null && users.containsKey(email.toLowerCase())) { return users.get(email.toLowerCase()); } return null; } /** * Remove user from internal hash. * * @param email email of user account to remove * * @return true if user was removed, false otherwise */ public boolean remove(String email) { if (email != null && users.containsKey(email.toLowerCase())) { return users.remove(email.toLowerCase()) != null; } return false; } public User getSetupUser() { return setupUser; } public List<User> list() { ArrayList<User> userList = new ArrayList<User>(users.values()); Collections.sort(userList, new Comparator<User>() { public int compare(User o1, User o2) { return (o1.getFirstname() + " " + o1.getLastname()).compareTo(o2.getFirstname() + " " + o2.getLastname()); } }); return userList; } public List<User> list(Role role) { List<User> matchingUsers = new ArrayList<User>(); for (User u : users.values()) { if (u.getRole() == role) { matchingUsers.add(u); } } return matchingUsers; } public void load() throws InvalidConfigException { Reader userReader; ObjectInputStream in = null; try { userReader = FileUtils.getUtf8Reader(dataDir.configFile(PERSISTENCE_FILE)); in = xstream.createObjectInputStream(userReader); users.clear(); while (true) { try { User u = (User) in.readObject(); addUser(u); } catch (EOFException e) { // end of file, expected exception! break; } catch (ClassNotFoundException e) { log.error(e.getMessage(), e); } } } catch (FileNotFoundException e) { log.warn("User accounts not existing, " + PERSISTENCE_FILE + " file missing (This is normal when first setting up a new datadir)"); } catch (IOException e) { log.error(e.getMessage(), e); throw new InvalidConfigException(TYPE.USER_CONFIG, "Couldnt read user accounts: " + e.getMessage()); } finally { if (in != null) { try { in.close(); } catch (IOException e) { } } } } public synchronized void save() throws IOException { log.debug("Saving all " + users.size() + " user accounts..."); Writer userWriter = FileUtils.startNewUtf8File(dataDir.configFile(PERSISTENCE_FILE)); ObjectOutputStream out = xstream.createObjectOutputStream(userWriter, "users"); for (Entry<String, User> entry : users.entrySet()) { out.writeObject(entry.getValue()); } out.close(); } /* * (non-Javadoc) * @see org.gbif.ipt.service.admin.UserAccountManager#save(org.gbif.ipt.model.User) */ public void save(User user) throws IOException { addUser(user); save(); } public void setSetupUser(User setupUser) { this.setupUser = setupUser; } }