/* * ProActive Parallel Suite(TM): * The Open Source library for parallel and distributed * Workflows & Scheduling, Orchestration, Cloud Automation * and Big Data Analysis on Enterprise Grids & Clouds. * * Copyright (c) 2007 - 2017 ActiveEon * Contact: contact@activeeon.com * * This library is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License * as published by the Free Software Foundation: version 3 of * the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * If needed, contact us to obtain a release under GPL Version 2 or 3 * or a different license than the AGPL. */ package org.ow2.proactive.scheduler.authentication; import java.io.*; import java.security.KeyException; import java.security.PublicKey; import java.util.*; import org.apache.commons.cli.*; import org.apache.commons.io.IOUtils; import org.objectweb.proactive.utils.SecurityManagerConfigurator; import org.ow2.proactive.authentication.FileLoginModule; import org.ow2.proactive.authentication.crypto.CreateCredentials; import org.ow2.proactive.authentication.crypto.Credentials; import org.ow2.proactive.authentication.crypto.HybridEncryptionUtil; import org.ow2.proactive.scheduler.core.properties.PASchedulerProperties; import org.ow2.proactive.utils.Tools; import com.google.common.base.Strings; import com.google.common.collect.Multimap; import com.google.common.collect.TreeMultimap; /** * Manage user accounts in login.cfg and group.cfg files * * @author The ProActive Team * @see org.ow2.proactive.authentication.crypto.Credentials */ public class ManageUsers { private static final String newline = System.lineSeparator(); public static final String CREATE_OPTION = "C"; public static final String CREATE_OPTION_NAME = "create"; public static final String UPDATE_OPTION = "U"; public static final String UPDATE_OPTION_NAME = "update"; public static final String DELETE_OPTION = "D"; public static final String DELETE_OPTION_NAME = "delete"; public static final String HELP_OPTION = "h"; public static final String HELP_OPTION_NAME = "help"; public static final String LOGIN_OPTION = "l"; public static final String LOGIN_OPTION_NAME = "login"; public static final String PWD_OPTION = "p"; public static final String PWD_OPTION_NAME = "password"; public static final String GROUPS_OPTION = "g"; public static final String GROUPS_OPTION_NAME = "groups"; public static final String KEYFILE_OPTION = "kf"; public static final String KEYFILE_OPTION_NAME = "keyfile"; public static final String LOGINFILE_OPTION = "lf"; public static final String LOGINFILE_OPTION_NAME = "loginfile"; public static final String GROUPFILE_OPTION = "gf"; public static final String GROUPFILE_OPTION_NAME = "groupfile"; public static final String SOURCE_LOGINFILE_OPTION = "slf"; public static final String SOURCE_LOGINFILE_OPTION_NAME = "sourceloginfile"; public static final String SOURCE_GROUPFILE_OPTION = "sgf"; public static final String SOURCE_GROUPFILE_OPTION_NAME = "sourcegroupfile"; public static final String USER_HEADER = "USER "; public static final String ALREADY_EXISTS_IN_LOGIN_FILE = " already exists in login file : "; public static final String ALREADY_EXISTS_IN_GROUP_FILE = " already exists in group file : "; public static final String IS_EMPTY_SKIPPING = " is empty, skipping."; public static final String DOES_NOT_EXIST_IN_LOGIN_FILE = " does not exist in login file : "; public static final String UPDATING_THIS_USER_INFORMATION = ", updating this user information."; public static final String DOES_NOT_EXIST_IN_GROUP_FILE = " does not exist in group file : "; public static final String PROVIDED_USERNAME = "Provided username "; /** * Entry point * * @param args arguments, try '-h' for help * @throws IOException * @throws ParseException * @see org.ow2.proactive.authentication.crypto.Credentials */ public static void main(String[] args) { try { manageUsers(args); } catch (ManageUsersException e) { System.err.println("ERROR : " + e.getMessage()); if (e.getCause() != null) { System.err.println(e.getCause().getMessage()); } if (e.getAdditionalInfo() != null) { System.out.println(e.getAdditionalInfo()); } System.exit(1); } System.exit(0); } public static void manageUsers(String... args) throws ManageUsersException { SecurityManagerConfigurator.configureSecurityManager(CreateCredentials.class.getResource("/all-permissions.security.policy") .toString()); Console console = System.console(); /** * default values */ String pubKeyPath = null; PublicKey pubKey = null; UserInfo userInfo = new UserInfo(); String loginFilePath = getLoginFilePath(); String groupFilePath = getGroupFilePath(); String sourceLoginFilePath = null; String sourceGroupFilePath = null; Action action = null; Options options = new Options(); CommandLine cmd = getCommandLine(args, loginFilePath, groupFilePath, options); if (cmd.hasOption(HELP_OPTION_NAME) || cmd.getOptions().length == 0) { displayHelp(options); return; } action = Action.getAction(cmd); if (cmd.hasOption(LOGIN_OPTION_NAME)) { userInfo.setLogin(cmd.getOptionValue(LOGIN_OPTION_NAME)); } if (cmd.hasOption(PWD_OPTION_NAME)) { userInfo.setPassword(cmd.getOptionValue(PWD_OPTION_NAME)); } if (cmd.hasOption(GROUPS_OPTION_NAME)) { String groupString = cmd.getOptionValue(GROUPS_OPTION_NAME); userInfo.setGroups(Arrays.asList(groupString.split(","))); } if (cmd.hasOption(LOGINFILE_OPTION_NAME)) { loginFilePath = cmd.getOptionValue(LOGINFILE_OPTION_NAME); } if (cmd.hasOption(GROUPFILE_OPTION_NAME)) { groupFilePath = cmd.getOptionValue(GROUPFILE_OPTION_NAME); } if (cmd.hasOption(KEYFILE_OPTION_NAME)) { pubKeyPath = cmd.getOptionValue(KEYFILE_OPTION_NAME); } if (cmd.hasOption(SOURCE_LOGINFILE_OPTION_NAME)) { if (action == Action.DELETE) { exitWithErrorMessage("Cannot use action delete with source login file.", null, null); } if (!cmd.hasOption(SOURCE_GROUPFILE_OPTION_NAME) && action == Action.CREATE) { exitWithErrorMessage("Source group file must be provided when creating users with source login file.", null, null); } sourceLoginFilePath = cmd.getOptionValue(SOURCE_LOGINFILE_OPTION_NAME); userInfo = null; } if (cmd.hasOption(SOURCE_GROUPFILE_OPTION_NAME)) { if (action == Action.DELETE) { exitWithErrorMessage("Cannot use action delete with source group file.", null, null); } if (!cmd.hasOption(SOURCE_LOGINFILE_OPTION_NAME) && action == Action.CREATE) { exitWithErrorMessage("Source login file must be provided when creating users with source group file.", null, null); } sourceGroupFilePath = cmd.getOptionValue(SOURCE_GROUPFILE_OPTION_NAME); userInfo = null; } if (pubKeyPath == null) { pubKeyPath = getPublicKeyFilePath(); } try { pubKey = Credentials.getPublicKey(pubKeyPath); } catch (KeyException e) { exitWithErrorMessage("Could not retrieve public key from '" + pubKeyPath, null, e); } boolean nonInteractive = checkInteractivity(userInfo, sourceLoginFilePath, sourceGroupFilePath, action); if (!nonInteractive) { askInteractively(console, userInfo, action); } updateAccounts(pubKey, userInfo, loginFilePath, groupFilePath, action, sourceLoginFilePath, sourceGroupFilePath); } private static boolean checkInteractivity(UserInfo userInfo, String sourceLoginFilePath, String sourceGroupFilePath, Action action) { boolean nonInteractive = true; if (sourceLoginFilePath != null || sourceGroupFilePath != null) { nonInteractive = true; } else { boolean l = userInfo.isLoginSet(); boolean p = userInfo.isPasswordSet(); boolean g = userInfo.isGroupSet(); switch (action) { case CREATE: nonInteractive = l && p && g; break; case UPDATE: nonInteractive = (l && p) || (l && g); break; case DELETE: nonInteractive = l; break; } } return nonInteractive; } /** * Ask the user for parameters missing on the command line */ private static void askInteractively(Console console, UserInfo userInfo, Action action) { System.out.println(action); System.out.print("login: "); if (!userInfo.isLoginSet()) { userInfo.setLogin(console.readLine()); } else { System.out.println(userInfo.getLogin()); } System.out.print("password: "); if ((action.isCreate() && !userInfo.isPasswordSet()) || (action.isUpdate() && !userInfo.isPasswordSet()) && !userInfo.isGroupSet()) { userInfo.setPassword(new String(console.readPassword())); } else { System.out.println("*******"); } if (action.isCreate() && !userInfo.isGroupSet()) { System.out.print("groups for user " + userInfo.getLogin() + ":"); String groupString = console.readLine(); userInfo.setGroups(Arrays.asList(groupString.split(","))); } } /** * Update the accounts in the login and config files * * @throws ManageUsersException */ private static void updateAccounts(final PublicKey pubKey, final UserInfo userInfo, final String loginFilePath, final String groupFilePath, final Action action, final String sourceLoginFile, final String sourceGroupFile) throws ManageUsersException { try { Properties destinationLoginProps = new Properties(); try (InputStreamReader stream = new InputStreamReader(new FileInputStream(loginFilePath))) { destinationLoginProps.load(stream); } catch (Exception e) { exitWithErrorMessage("could not read login file : " + loginFilePath, null, e); } Multimap<String, String> destinationGroupsMap = loadGroups(groupFilePath); Properties sourceLoginProps = new Properties(); if (sourceLoginFile != null) { try (InputStreamReader stream = new InputStreamReader(new FileInputStream(sourceLoginFile))) { sourceLoginProps.load(stream); } catch (Exception e) { exitWithErrorMessage("could not read source login file : " + sourceLoginFile, null, e); } } else if (userInfo != null) { if (userInfo.getPassword() == null) { // password can be null in case of account deletion sourceLoginProps.put(userInfo.getLogin(), ""); } else { sourceLoginProps.put(userInfo.getLogin(), userInfo.getPassword()); } } Multimap<String, String> sourceGroupsMap = null; if (sourceGroupFile != null) { sourceGroupsMap = loadGroups(sourceGroupFile); } else { sourceGroupsMap = TreeMultimap.create(); if (userInfo != null && !userInfo.getGroups().isEmpty()) { for (String group : userInfo.getGroups()) { sourceGroupsMap.put(userInfo.getLogin(), group); } } } Collection<String> sourceLoginNames = sourceLoginProps.stringPropertyNames(); if (sourceLoginNames.isEmpty()) { sourceLoginNames = sourceGroupsMap.keySet(); } boolean bulkMode = sourceLoginNames.size() > 1; for (String user : sourceLoginNames) { UserInfo sourceUserInfo = new UserInfo(); sourceUserInfo.setLogin(user); sourceUserInfo.setPassword((String) sourceLoginProps.get(user)); if (sourceGroupsMap.containsKey(user)) { sourceUserInfo.setGroups(sourceGroupsMap.get(user)); } switch (action) { case CREATE: createAccount(pubKey, sourceUserInfo, loginFilePath, groupFilePath, destinationLoginProps, destinationGroupsMap); break; case UPDATE: updateAccount(pubKey, sourceUserInfo, loginFilePath, destinationLoginProps, destinationGroupsMap, bulkMode); break; case DELETE: deleteAccount(sourceUserInfo, loginFilePath, groupFilePath, destinationLoginProps, destinationGroupsMap); break; } } storeLoginFile(loginFilePath, destinationLoginProps); storeGroups(groupFilePath, destinationGroupsMap); } catch (Throwable t) { exitWithErrorMessage("Unexpected error", null, t); } } private static void deleteAccount(UserInfo userInfo, String loginFilePath, String groupFilePath, Properties props, Multimap<String, String> groupsMap) throws ManageUsersException { if (!userInfo.isLoginSet()) { warnWithMessage(PROVIDED_USERNAME + IS_EMPTY_SKIPPING); return; } if (!props.containsKey(userInfo.getLogin())) { warnWithMessage(USER_HEADER + userInfo.getLogin() + DOES_NOT_EXIST_IN_LOGIN_FILE + loginFilePath); } if (!groupsMap.containsKey(userInfo.getLogin())) { warnWithMessage(USER_HEADER + userInfo.getLogin() + DOES_NOT_EXIST_IN_GROUP_FILE + groupFilePath); } props.remove(userInfo.getLogin()); groupsMap.removeAll(userInfo.getLogin()); System.out.println("Deleted user " + userInfo.getLogin()); } private static void updateAccount(PublicKey pubKey, UserInfo userInfo, String loginFilePath, Properties props, Multimap<String, String> groupsMap, boolean bulkMode) throws ManageUsersException, KeyException { if (!userInfo.isLoginSet()) { warnWithMessage(PROVIDED_USERNAME + IS_EMPTY_SKIPPING); return; } if (!props.containsKey(userInfo.getLogin())) { String userDoesNotExistInLoginFileMessage = USER_HEADER + userInfo.getLogin() + DOES_NOT_EXIST_IN_LOGIN_FILE + loginFilePath; if (userInfo.isPasswordSet() && userInfo.isGroupSet()) { warnWithMessage(userDoesNotExistInLoginFileMessage + ", create this user."); } else { if (bulkMode) { warnWithMessage(userDoesNotExistInLoginFileMessage + " and not enough information were provided to create a new user, skipping."); return; } else { exitWithErrorMessage(userDoesNotExistInLoginFileMessage + " and not enough information were provided to create a new user.", null, null); } } } if (userInfo.isPasswordSet()) { updateUserPassword(pubKey, userInfo.getLogin(), userInfo.getPassword(), props); System.out.println("Changed password for user " + userInfo.getLogin()); } if (!userInfo.getGroups().isEmpty()) { updateUserGroups(userInfo.getLogin(), userInfo.getGroups(), groupsMap); } System.out.println("Updated user " + userInfo.getLogin()); } private static void createAccount(PublicKey pubKey, UserInfo userInfo, String loginFilePath, String groupFilePath, Properties props, Multimap<String, String> groupsMap) throws ManageUsersException, KeyException { if (!userInfo.isLoginSet()) { warnWithMessage(PROVIDED_USERNAME + IS_EMPTY_SKIPPING); return; } if (!userInfo.isPasswordSet()) { warnWithMessage("Provided password for user " + userInfo.getLogin() + IS_EMPTY_SKIPPING); return; } if (!userInfo.isGroupSet()) { warnWithMessage("Provided groups for user " + userInfo.getLogin() + IS_EMPTY_SKIPPING); return; } if (props.containsKey(userInfo.getLogin())) { warnWithMessage(USER_HEADER + userInfo.getLogin() + ALREADY_EXISTS_IN_LOGIN_FILE + loginFilePath + UPDATING_THIS_USER_INFORMATION); } if (groupsMap.containsKey(userInfo.getLogin())) { warnWithMessage(USER_HEADER + userInfo.getLogin() + ALREADY_EXISTS_IN_GROUP_FILE + groupFilePath + UPDATING_THIS_USER_INFORMATION); } updateUserPassword(pubKey, userInfo.getLogin(), userInfo.getPassword(), props); updateUserGroups(userInfo.getLogin(), userInfo.getGroups(), groupsMap); System.out.println("Created user " + userInfo.getLogin()); } /** * Build the command line options and parse */ private static CommandLine getCommandLine(String[] args, String loginFilePath, String groupFilePath, Options options) throws ManageUsersException { Option opt = new Option(HELP_OPTION, HELP_OPTION_NAME, false, "Display this help"); opt.setRequired(false); options.addOption(opt); OptionGroup optionGroup = new OptionGroup(); optionGroup.setRequired(false); opt = new Option(CREATE_OPTION, CREATE_OPTION_NAME, true, "Action to create a user"); opt.setArgName(CREATE_OPTION_NAME.toUpperCase()); opt.setArgs(0); opt.setRequired(false); optionGroup.addOption(opt); opt = new Option(UPDATE_OPTION, UPDATE_OPTION_NAME, true, "Action to update an existing user. Updating a user means to change the user's password or group membership."); opt.setArgName(UPDATE_OPTION_NAME.toUpperCase()); opt.setArgs(0); opt.setRequired(false); optionGroup.addOption(opt); opt = new Option(DELETE_OPTION, DELETE_OPTION_NAME, true, "Action to delete an existing user"); opt.setArgName(DELETE_OPTION_NAME.toUpperCase()); opt.setArgs(0); opt.setRequired(false); optionGroup.addOption(opt); options.addOptionGroup(optionGroup); opt = new Option(LOGIN_OPTION, LOGIN_OPTION_NAME, true, "Generate credentials for this specific user, will be asked interactively if not specified"); opt.setArgName(LOGIN_OPTION_NAME.toUpperCase()); opt.setArgs(1); opt.setRequired(false); options.addOption(opt); opt = new Option(PWD_OPTION, PWD_OPTION_NAME, true, "Password of the user, if the user is created or updated, will be asked interactively if not specified"); opt.setArgName(PWD_OPTION_NAME.toUpperCase()); opt.setArgs(1); opt.setRequired(false); options.addOption(opt); opt = new Option(GROUPS_OPTION, GROUPS_OPTION_NAME, true, "A comma-separated list of groups the user must be member of. Can be used when the user is created or updated"); opt.setArgName(GROUPS_OPTION_NAME.toUpperCase()); opt.setArgs(1); opt.setRequired(false); options.addOption(opt); optionGroup.setRequired(false); opt = new Option(KEYFILE_OPTION, KEYFILE_OPTION_NAME, true, "Public key path on the local filesystem [default:" + getPublicKeyFilePath() + "]"); opt.setArgName(KEYFILE_OPTION_NAME.toUpperCase()); opt.setArgs(1); opt.setRequired(false); options.addOption(opt); opt = new Option(LOGINFILE_OPTION, LOGINFILE_OPTION_NAME, true, "Path to the login file in use [default:" + loginFilePath + "]"); opt.setArgName(LOGINFILE_OPTION_NAME.toUpperCase()); opt.setArgs(1); opt.setRequired(false); options.addOption(opt); opt = new Option(GROUPFILE_OPTION, GROUPFILE_OPTION_NAME, true, "Path to the group file in use [default:" + groupFilePath + "]"); opt.setArgName(GROUPFILE_OPTION_NAME.toUpperCase()); opt.setArgs(1); opt.setRequired(false); options.addOption(opt); opt = new Option(SOURCE_LOGINFILE_OPTION, SOURCE_LOGINFILE_OPTION_NAME, true, "Path to a source login file, used for bulk creation or bulk update. The source login file must contain clear text passwords in the format username:password"); opt.setArgName(SOURCE_LOGINFILE_OPTION_NAME.toUpperCase()); opt.setArgs(1); opt.setRequired(false); options.addOption(opt); opt = new Option(SOURCE_GROUPFILE_OPTION, SOURCE_GROUPFILE_OPTION_NAME, true, "Path to a source group file, used for bulk creation or bulk update. The source group file must contain group assignements in the format username:group"); opt.setArgName(SOURCE_GROUPFILE_OPTION_NAME.toUpperCase()); opt.setArgs(1); opt.setRequired(false); options.addOption(opt); CommandLineParser parser = new DefaultParser(); CommandLine cmd = null; try { cmd = parser.parse(options, args); } catch (Exception e) { exitWithErrorMessage(newline + e.getMessage() + newline, "type -h or --help to display help screen", null); } return cmd; } private static void updateUserGroups(String login, Collection<String> groups, Multimap<String, String> groupsMap) { if (!groups.isEmpty()) { groupsMap.removeAll(login); for (String group : groups) { if (!group.isEmpty()) { groupsMap.put(login, group); System.out.println("Adding group " + group + " to user " + login); } } } } private static void updateUserPassword(PublicKey pubKey, String login, String password, Properties props) throws KeyException { String encodedPassword; encodedPassword = HybridEncryptionUtil.encryptStringToBase64(password, pubKey, FileLoginModule.ENCRYPTED_DATA_SEP); props.put(login, encodedPassword); } private static Multimap<String, String> loadGroups(String groupFilePath) throws ManageUsersException { Multimap<String, String> groupsMap = TreeMultimap.create(); String line = null; try (BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(groupFilePath)))) { while ((line = reader.readLine()) != null) { if (!line.trim().isEmpty()) { String[] u2g = line.split(":"); if (u2g.length == 2) { groupsMap.put(u2g[0].trim(), u2g[1].trim()); } } } } catch (IOException e) { exitWithErrorMessage("could not read group file : " + groupFilePath, null, e); } return groupsMap; } /** * Stores the logins into login.cfg */ private static void storeLoginFile(String loginFilePath, Properties props) throws IOException { try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(loginFilePath)))) { props.store(writer, null); } List<String> lines = null; try (FileInputStream stream = new FileInputStream(loginFilePath)) { lines = IOUtils.readLines(stream); } TreeMap<String, String> sortedUsers = new TreeMap<>(); for (String line : lines) { if (!(line.isEmpty() || line.startsWith("#"))) { String[] loginAndPwd = line.split("=", 2); sortedUsers.put(loginAndPwd[0], loginAndPwd[1]); } } List<String> modifiedLines = new ArrayList<>(sortedUsers.size()); for (Map.Entry entry : sortedUsers.entrySet()) { modifiedLines.add(entry.getKey() + ":" + entry.getValue()); } try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(loginFilePath)))) { IOUtils.writeLines(modifiedLines, System.getProperty("line.separator"), writer); } System.out.println("Stored login file in " + loginFilePath); } /** * Stores the groups in group.cfg */ private static void storeGroups(String groupFilePath, Multimap<String, String> groups) throws ManageUsersException { try { try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(groupFilePath)))) { for (Map.Entry<String, String> userEntry : groups.entries()) { writer.println(userEntry.getKey() + ":" + userEntry.getValue()); } } } catch (IOException e) { exitWithErrorMessage("could not write group file : " + groupFilePath, null, e); } System.out.println("Stored group file in " + groupFilePath); } private static void exitWithErrorMessage(String errorMessage, String infoMessage, Throwable e) throws ManageUsersException { throw new ManageUsersException(errorMessage, e, infoMessage); } private static void warnWithMessage(String warnMessage) { System.err.println("WARN : " + warnMessage); } private static String getLoginFilePath() { return getSchedulerFile(PASchedulerProperties.SCHEDULER_LOGIN_FILENAME.getValueAsString()); } private static String getGroupFilePath() { return getSchedulerFile(PASchedulerProperties.SCHEDULER_GROUP_FILENAME.getValueAsString()); } private static String getPublicKeyFilePath() { return getSchedulerFile(PASchedulerProperties.SCHEDULER_AUTH_PUBKEY_PATH.getValueAsString()); } private static String getSchedulerFile(String path) { String schedulerFile = path; if (!(new File(schedulerFile).isAbsolute())) { //file path is relative, so we complete the path with the prefix Scheduler_Home constant schedulerFile = PASchedulerProperties.SCHEDULER_HOME.getValueAsString() + File.separator + path; } return schedulerFile; } private static void displayHelp(Options options) { HelpFormatter hf = new HelpFormatter(); hf.setWidth(135); hf.printHelp("proactive-users" + Tools.shellExtension(), "", options, "", true); } enum Action { CREATE("Creating User."), UPDATE("Updating existing user"), DELETE("Deleting user"); final String message; Action(String message) { this.message = message; } public static Action getAction(CommandLine cmd) { if (cmd.hasOption(CREATE_OPTION_NAME)) { return Action.CREATE; } else if (cmd.hasOption(UPDATE_OPTION_NAME)) { return Action.UPDATE; } else if (cmd.hasOption(DELETE_OPTION_NAME)) { return Action.DELETE; } else { throw new IllegalArgumentException("Command line does not contain, create, update or delete action: " + cmd); } } public boolean isCreate() { return this == Action.CREATE; } public boolean isUpdate() { return this == Action.UPDATE; } public boolean isDelete() { return this == Action.DELETE; } @Override public String toString() { return message; } } static class UserInfo { private String login; private String password; private Collection<String> groups = Collections.emptyList(); public UserInfo() { } public boolean isLoginSet() { return !Strings.isNullOrEmpty(login); } public boolean isPasswordSet() { return !Strings.isNullOrEmpty(password); } public boolean isGroupSet() { return groups != null && !groups.isEmpty(); } public String getLogin() { return login; } public void setLogin(String login) { this.login = login; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public Collection<String> getGroups() { return groups; } public void setGroups(Collection<String> groups) { this.groups = groups; } } static class ManageUsersException extends Exception { public String getAdditionalInfo() { return additionalInfo; } private String additionalInfo = null; public ManageUsersException(String message) { super(message); } public ManageUsersException(String message, Throwable cause) { super(message, cause); } public ManageUsersException(String message, Throwable cause, String additionalInfo) { super(message, cause); this.additionalInfo = additionalInfo; } } }