/* * ==================== * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2008-2009 Sun Microsystems, Inc. All rights reserved. * * The contents of this file are subject to the terms of the Common Development * and Distribution License("CDDL") (the "License"). You may not use this file * except in compliance with the License. * * You can obtain a copy of the License at * http://opensource.org/licenses/cddl1.php * See the License for the specific language governing permissions and limitations * under the License. * * When distributing the Covered Code, include this CDDL Header Notice in each file * and include the License file at http://opensource.org/licenses/cddl1.php. * If applicable, add the following below this CDDL Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * ==================== */ package org.identityconnectors.solaris.operation.nis; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import org.identityconnectors.common.CollectionUtil; import org.identityconnectors.common.StringUtil; import org.identityconnectors.common.logging.Log; import org.identityconnectors.common.security.GuardedString; import org.identityconnectors.framework.common.exceptions.ConnectorException; import org.identityconnectors.solaris.SolarisConfiguration; import org.identityconnectors.solaris.SolarisConnection; import org.identityconnectors.solaris.attr.NativeAttribute; import org.identityconnectors.solaris.operation.search.SolarisEntry; public class CreateNISUser extends AbstractNISOp { private static final Log logger = Log.getLog(CreateNISUser.class); private final static Set<String> CHSH_REJECTS = CollectionUtil.newSet("password:", "passwd:"); private static final String DEFAULTS_FILE = "/usr/sadm/defadduser"; private static final String NO_DEFAULT_PRIMARY_GROUP = "No default primary group"; private static final String NO_DEFAULT_HOME_DIR = "No default home directory"; private static final String NO_DEFAULT_LOGIN_SHELL = "No default login shell"; private static final String UID_NOT_UNIQUE = "uid is not unique."; /** initialize reject messages for the password cleanup command. */ private final static Set<String> PASSWD_CLEANUP_REJECT = CollectionUtil.newSet( NO_DEFAULT_PRIMARY_GROUP, NO_DEFAULT_HOME_DIR, NO_DEFAULT_LOGIN_SHELL, UID_NOT_UNIQUE); private static final String INVALID_SHELL = "unacceptable as a new shell"; private final static Set<String> SHELL_REJECTS = CollectionUtil.newSet(INVALID_SHELL); public static void performNIS(SolarisEntry entry, GuardedString password, SolarisConnection connection) { final SolarisConfiguration config = connection.getConfiguration(); final String accountId = entry.getName(); String shell = null; String uid = null; String gid = null; String gecos = null; String homedir = null; final String pwdDir = connection.getConfiguration().getNisPwdDir(); String pwdfile = pwdDir + "/passwd"; String shadowfile = pwdDir + "/shadow"; String salt = ""; StringBuilder passwordRecord; String shadowOwner = ""; String shadowRecord = ""; boolean shadow = config.isNisShadowPasswordSupport(); String cpCmd; String chownCmd; String diffCmd; String removeTmpFilesScript = AbstractNISOp.getRemovePwdTmpFiles(connection); String basedir = config.getHomeBaseDirectory(); if (StringUtil.isNotBlank(basedir)) { StringBuffer homedirBuffer = new StringBuffer(basedir); if (!basedir.endsWith("/")) { homedirBuffer.append("/"); } homedirBuffer.append(accountId); homedir = homedirBuffer.toString(); logger.ok("{0) got {1} from Configuration attribute 'homeBaseDir'", accountId, homedir); } String loginGroup = config.getDefaultPrimaryGroup(); if (StringUtil.isNotBlank(loginGroup)) { gid = loginGroup; logger.ok("{0} got {1} from Configuration attribute 'defaultPrimaryGroup'", accountId, loginGroup); } String loginShell = config.getLoginShell(); if (StringUtil.isNotBlank(loginShell)) { shell = loginShell; logger.ok("{0} got {1} from Configuration attribute 'loginShell'", accountId, loginShell); } // Get specified user attributes, which can override above resource // attributes Map<NativeAttribute, List<Object>> attributes = AbstractNISOp.constructNISUserAttributeParameters(entry, ALLOWED_NIS_ATTRIBUTES); for (Map.Entry<NativeAttribute, List<Object>> it : attributes.entrySet()) { NativeAttribute key = it.getKey(); String value = (String) it.getValue().get(0); boolean matched = true; switch (key) { case SHELL: shell = value; break; case GROUP_PRIM: gid = value; break; case DIR: homedir = value; break; case COMMENT: gecos = value; break; case ID: uid = value; break; default: matched = false; break; } if (matched) { logger.ok("{0} attribute '{1}' got value '{2}'", entry.getName(), key.toString(), value); } } cpCmd = connection.buildCommand(false, "cp"); chownCmd = connection.buildCommand(false, "chown"); diffCmd = connection.buildCommand(false, "diff"); // Seed the password field accordingly on whether or not // a shadow file is used if (shadow) { salt = "x"; // @formatter:off shadowOwner = "OWNER=`ls -l " + shadowfile + " | awk '{ print $3 }'`; " + "GOWNER=`ls -l " + shadowfile + " | awk '{ print $4 }'`"; shadowRecord = cpCmd + "-p " + shadowfile + " " + TMP_PWDFILE_1 + "; " + cpCmd + "-p " + shadowfile + " " + TMP_PWDFILE_2 + "; " + chownCmd + "$WHOIAM " + TMP_PWDFILE_2 + "\n " + "echo \"" + accountId + "::::::::\" >> " + TMP_PWDFILE_2 + "; " + diffCmd + shadowfile + " " + TMP_PWDFILE_1 + " 2>&1 >/dev/null; " + "RC=$?; " + "if [ $RC -eq 0 ]; then\n" + cpCmd + "-f " + TMP_PWDFILE_2 + " " + shadowfile + "; " + chownCmd + "$OWNER:$GOWNER " + shadowfile + "; " + "else " + "GRPERRMSG=\""+ ERROR_MODIFYING + shadowfile + ", for entry " + accountId + ".\"; " + "fi"; // @formatter:on } // Create script for adding password file entry // Test for existence and readability of defaults file before trying to // load it // @formatter:off passwordRecord = new StringBuilder( // The connection to the resource is pooled. Clear the environment // variables that will be used. "unset defgroup; unset defgname; unset defhome; unset defparent; " + "unset defshell; unset gecos; unset newuid; unset dupuid; " + "unset GRPERRMSG; unset PARERRMSG; unset SHLERRMSG; unset DUPUIDERRMSG; " + "if [ -r " + DEFAULTS_FILE + " ]; then\n" + ". " + DEFAULTS_FILE + "; " + "fi; " ); // If resource attributes are available, override results from defadduser // At the moment, we are using defgroup, defshell and defparent // Override for defgroup (RA_DEFAULT_PRIMARY_GROUP or USER_GROUP) has already been loaded above. if (StringUtil.isNotBlank(gid)) { passwordRecord.append("defgroup=`ypmatch " + gid + " group | cut -d: -f3`; "); passwordRecord.append("if [ -z \"$defgroup\" ]; then\n"); passwordRecord.append("GRPERRMSG=\"" + NO_DEFAULT_PRIMARY_GROUP + " matches in ypmatch " + gid + " group.\"; "); passwordRecord.append("fi; "); } else { passwordRecord.append("if [ -z \"$defgname\" ]; then\n"); passwordRecord.append("GRPERRMSG=\"" + NO_DEFAULT_PRIMARY_GROUP + " found in resource or account attributes, or " + DEFAULTS_FILE + "\"; "); passwordRecord.append("else\n"); passwordRecord.append(" defgroup=`ypmatch \"$defgname\" group | cut -d: -f3`; "); passwordRecord.append(" if [ -z \"$defgroup\" ]; then\n"); passwordRecord.append(" GRPERRMSG=\"" + NO_DEFAULT_PRIMARY_GROUP + " found in resource or account attributes, or " + DEFAULTS_FILE + "\"; "); passwordRecord.append(" fi; "); passwordRecord.append("fi; "); } // Override for defparent (homeBasedir or userDir) has already been loaded above. if (StringUtil.isNotBlank(homedir)) { passwordRecord.append("defhome=" + homedir +"; "); } else { passwordRecord.append("if [ -z \"$defparent\" ]; then\n"); passwordRecord.append("PARERRMSG=\"" + NO_DEFAULT_HOME_DIR + " found in resource or account attributes, or " + DEFAULTS_FILE + "\"; "); // error message reject on script execution will throw exception before defhome is needed, no need to set it here passwordRecord.append("else\n"); passwordRecord.append("defhome=$defparent/" + accountId + "; "); passwordRecord.append("fi; "); } // Override for defshell (RA_LOGIN_SHELL or USER_SHELL) has already been loaded above. if (StringUtil.isNotBlank(shell)) { passwordRecord.append("defshell=" + shell + "; "); } else { passwordRecord.append("if [ -z \"$defshell\" ]; then\n"); passwordRecord.append("SHLERRMSG=\"" + NO_DEFAULT_LOGIN_SHELL + " found in resource or account attributes, or " + DEFAULTS_FILE + "\"; "); // error message reject on script execution will throw exception before defshell is needed, no need to set it here passwordRecord.append("fi; "); } // @formatter:on if (gecos != null) { passwordRecord.append("gecos=\"" + gecos + "\"; "); } else { passwordRecord.append("unset gecos; "); } if (uid != null) { passwordRecord.append("newuid=" + uid + "; \\\n"); // check whether newuid is duplicate or not. passwordRecord.append("dupuid=`ypmatch \"$newuid\" passwd.byuid | cut -d: -f3`; "); passwordRecord.append("if [ \"$dupuid\" ]; then\n"); passwordRecord.append("DUPUIDERRMSG=\"" + UID_NOT_UNIQUE + " change uid " + uid + " to some other unique value." + "\"; "); passwordRecord.append("fi; "); } // emit any errors so the reject processing can see them String passwordCleanup = "echo \"$GRPERRMSG\"; echo \"$PARERRMSG\"; echo \"$SHLERRMSG\"; echo \"$DUPUIDERRMSG\"; " + // The connection to the resource is pooled. Clear the // environment // variables that were used. "unset GRPERRMSG; unset PARERRMSG; unset SHLERRMSG; unset dupuid; unset DUPUIDERRMSG; "; String getOwner = initGetOwner(pwdfile); // @formatter:off String createRecord1 = cpCmd + "-p " + pwdfile + " " + TMP_PWDFILE_1 + "; " + cpCmd + "-p " + pwdfile + " " + TMP_PWDFILE_2 + "\n " + chownCmd + "$WHOIAM " + TMP_PWDFILE_2 + "; " + "echo \"" + accountId + ":" + salt + ":$newuid:$defgroup:$gecos:$defhome:\" >> " + TMP_PWDFILE_2; String createRecord2 = diffCmd + pwdfile + " " + TMP_PWDFILE_1 + " 2>&1 >/dev/null; " + "RC=$?; " + "if [ $RC -eq 0 ]; then\n" + cpCmd + "-f " + TMP_PWDFILE_2 + " " + pwdfile + "; " + chownCmd + "$OWNER:$GOWNER " + pwdfile + "; " + "else " + "GRPERRMSG=\""+ ERROR_MODIFYING + pwdfile + ", for entry " + accountId + ".\"; "+ "fi"; // @formatter:on try { connection.doSudoStart(); // get required password settings connection.executeCommand(AbstractNISOp.WHO_I_AM); connection.executeCommand(passwordRecord.toString()); } finally { // The reject below can throw an exception when the script is // executed, so reset sudo before that test connection.doSudoReset(); } try { connection.executeCommand(passwordCleanup, PASSWD_CLEANUP_REJECT); } catch (Exception ex) { throw ConnectorException.wrap(ex); } try { connection.doSudoStart(); try { // Acquire password file update mutex connection.executeMutexAcquireScript(PWD_MUTEX_FILE, TMP_PWD_MUTEX_FILE, PWD_PID_FILE); // Clear any leftover temporary files connection.executeCommand(removeTmpFilesScript); // Add the script to determine the user's id if not specified if (uid == null) { final String uidScript = getNISNewUidScript(); connection.executeCommand(uidScript); } // Add password file record connection.executeCommand(getOwner); connection.executeCommand(createRecord1); try { // second prompt due to chown nl (the first is hidden in // executeCommand impl.) connection.waitForRootShellPrompt(); } catch (Exception ex) { throw ConnectorException.wrap(ex); } connection.executeCommand(createRecord2); connection.executeCommand(removeTmpFilesScript); // Add shadow record if needed if (shadow) { connection.executeCommand(shadowOwner); connection.executeCommand(shadowRecord); // second prompt due to chown nl (the first is hidden in // executeCommand impl.) connection.waitForRootShellPrompt(); connection.executeCommand(removeTmpFilesScript); } // NIS database has to be updated before updates to shell or // password // If the option is to bypass the make, only issue a make if // there is a shell or // password to be set. AbstractNISOp.addNISMake("passwd", connection); if (shell != null) { addNISShellUpdateWithCleanup(accountId, shell, connection); } if (password != null) { addNISPasswordUpdate(accountId, password, connection); } AbstractNISOp.addNISMake("passwd", connection); } finally { // Release the "mutex" connection.executeMutexReleaseScript(PWD_MUTEX_FILE); } } finally { // The reject below can throw an exception when the script is // executed, so reset sudo before that test connection.doSudoReset(); } } private static void addNISPasswordUpdate(String accountId, GuardedString password, SolarisConnection connection) { String passwdCmd = connection.buildCommand(true, "yppasswd", accountId); try { connection.executeCommand(passwdCmd, Collections.<String> emptySet(), CollectionUtil .newSet("password:")); connection.sendPassword(password, Collections.<String> emptySet(), CollectionUtil .newSet("new password:")); connection.sendPassword(password, CollectionUtil.newSet(" denied"), Collections .<String> emptySet()); } catch (Exception ex) { throw ConnectorException.wrap(ex); } } /** * Updates Shell for the new user, if shell is valid value for NIS * resources. Otherwise, deletes the newly creating user as it is failure * for updating with invalid shell for that user. */ private static void addNISShellUpdateWithCleanup(String accountId, String shell, SolarisConnection connection) { final String passwdCmd = connection.buildCommand(true, "passwd"); final String passwordRecord = passwdCmd + "-r nis -e " + accountId + " 2>&1 | tee " + TMP_PWDFILE_3 + " ; "; try { connection.executeCommand(passwordRecord, CHSH_REJECTS, CollectionUtil .newSet("new shell:")); connection.executeCommand(shell); } catch (Exception ex) { throw ConnectorException.wrap(ex); } final String passwordCleanup = "unset INVALID_SHELL_ERRMSG; INVALID_SHELL_ERRMSG=`grep \"" + INVALID_SHELL + "\" " + TMP_PWDFILE_3 + "`;"; connection.executeCommand(passwordCleanup); final String pwddir = connection.getConfiguration().getNisPwdDir(); final String pwdFile = pwddir + "/passwd"; final String shadowFile = pwddir + "/shadow"; final String removeTmpFilesScript = AbstractNISOp.getRemovePwdTmpFiles(connection); // Add script to remove entry in passwd file if shell update fails String getOwner = initGetOwner(pwdFile); final String cleanUpScript = initPasswdShadowCleanUpScript(accountId, connection, pwdFile, shadowFile, getOwner); connection.executeCommand(cleanUpScript); connection.executeCommand(removeTmpFilesScript); // The user has to be removed from the NIS database, incase of invalid // shell failures AbstractNISOp.addNISMake("passwd", connection); final String invalidShellCheck = "echo $INVALID_SHELL_ERRMSG; unset INVALID_SHELL_ERRMSG;"; connection.executeCommand(invalidShellCheck, SHELL_REJECTS); } private static String initPasswdShadowCleanUpScript(String accountId, SolarisConnection connection, String pwdFile, String shadowFile, String getOwner) { String cpCmd = connection.buildCommand(false, "cp"); String mvCmd = connection.buildCommand(false, "mv"); String chownCmd = connection.buildCommand(true, "chown"); String grepCmd = connection.buildCommand(false, "grep"); StringBuilder workScript = new StringBuilder(); // @formatter:off String passwdEntryCleanup = "if [ \"$INVALID_SHELL_ERRMSG\" ]; then \n" + getOwner + " \n" + cpCmd + "-p " + pwdFile + " " + TMP_PWDFILE_1 + "; \n " + grepCmd + "-v \"^" + accountId + ":\" " + pwdFile + " > " + TMP_PWDFILE_2 + "; \n" + cpCmd + "-p " + TMP_PWDFILE_2 + " " + TMP_PWDFILE_1 + "; \n" + mvCmd + "-f " + TMP_PWDFILE_1 + " " + pwdFile + "; \n" + chownCmd + "$OWNER:$GOWNER " + pwdFile + "; \n"; // @formatter:on workScript.append(passwdEntryCleanup); String shadowEntryCleanup = ""; if (connection.getConfiguration().isNisShadowPasswordSupport()) { // Do the same thing we just did but for the shadow file String getShadowOwner = "OWNER=`ls -l " + shadowFile + " | awk '{ print $3 }'`; " + "GOWNER=`ls -l " + shadowFile + " | awk '{ print $4 }'`"; shadowEntryCleanup = shadowEntryCleanup + getShadowOwner + " \n" + cpCmd + "-p " + shadowFile + " " + TMP_PWDFILE_1 + "; \n" + grepCmd + "-v \"^" + accountId + ":\" " + shadowFile + " > " + TMP_PWDFILE_2 + "; \n" + cpCmd + "-p " + TMP_PWDFILE_2 + " " + TMP_PWDFILE_1 + "; \n" + mvCmd + "-f " + TMP_PWDFILE_1 + " " + shadowFile + "; \n" + chownCmd + "$OWNER:$GOWNER " + shadowFile + "; \n"; } workScript.append(shadowEntryCleanup); workScript.append("fi"); return workScript.toString(); } private static String getNISNewUidScript() { // @formatter:off String script = "minuid=100; " + "newuid=`ypcat passwd | sort -n -t: -k3 | tail -1 | cut -d: -f3`; " + // prevent -lt from failing when there are no users "if [ -z \"$newuid\" ]; then\n" + "newuid=$minuid; " + "fi; " + "newuid=`expr $newuid + 1`; " + "if [ $newuid -lt $minuid ]; then\n" + "newuid=$minuid; " + "fi"; // @formatter:on return script; } }