/** * Copyright (c) 2012 Evolveum * * The contents of this file are subject to the terms * of the Common Development and Distribution License * (the License). You may not use this file except in * compliance with the License. * * You can obtain a copy of the License at * http://www.opensource.org/licenses/cddl1 or * CDDLv1.0.txt file in the source code distribution. * See the License for the specific language governing * permission and limitations under the License. * * If applicable, add the following below the CDDL Header, * with the fields enclosed by brackets [] replaced by * your own identifying information: * * Portions Copyrighted 2012 [name of copyright owner] * * Portions Copyrighted 2008-2009 Sun Microsystems, Inc. */ package org.identityconnectors.solaris.mode; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumMap; import java.util.EnumSet; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.identityconnectors.framework.common.exceptions.ConnectorException; import org.identityconnectors.framework.common.objects.Attribute; import org.identityconnectors.framework.common.objects.AttributeInfo; import org.identityconnectors.framework.common.objects.AttributeInfo.Flags; import org.identityconnectors.framework.common.objects.AttributeInfoBuilder; import org.identityconnectors.framework.common.objects.AttributeUtil; import org.identityconnectors.framework.common.objects.ObjectClass; import org.identityconnectors.framework.common.objects.ObjectClassInfo; import org.identityconnectors.framework.common.objects.ObjectClassInfoBuilder; import org.identityconnectors.framework.common.objects.OperationalAttributeInfos; import org.identityconnectors.framework.common.objects.Schema; import org.identityconnectors.framework.common.objects.SchemaBuilder; import org.identityconnectors.framework.spi.operations.AuthenticateOp; import org.identityconnectors.framework.spi.operations.CreateOp; import org.identityconnectors.framework.spi.operations.DeleteOp; import org.identityconnectors.framework.spi.operations.ResolveUsernameOp; import org.identityconnectors.framework.spi.operations.SchemaOp; import org.identityconnectors.framework.spi.operations.UpdateOp; import org.identityconnectors.solaris.SolarisConnection; import org.identityconnectors.solaris.SolarisConnector; import org.identityconnectors.solaris.attr.AccountAttribute; import org.identityconnectors.solaris.attr.AttrUtil; import org.identityconnectors.solaris.attr.GroupAttribute; import org.identityconnectors.solaris.attr.NativeAttribute; import org.identityconnectors.solaris.operation.CommandSwitches; import org.identityconnectors.solaris.operation.search.AuthsCommand; import org.identityconnectors.solaris.operation.search.LastCommand; import org.identityconnectors.solaris.operation.search.LoginsCommand; import org.identityconnectors.solaris.operation.search.ProfilesCommand; import org.identityconnectors.solaris.operation.search.RolesCommand; import org.identityconnectors.solaris.operation.search.SolarisEntry; import org.identityconnectors.solaris.operation.search.SolarisSearch; /** * Driver for solaris-specific user management commands. * * Partially copied from the original (hard-coded) Solaris connector code and * modified. * * @see UnixModeDriver * * @author David Adam * @author Radovan Semancik * */ public class SolarisModeDriver extends UnixModeDriver { public static final String MODE_NAME = "solaris"; private static final String TMPFILE = "/tmp/connloginsError.$$"; private static final String SHELL_CONT_CHARS = "> "; private static final int CHARS_PER_LINE = 160; private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MM/DD/yyyy"); public SolarisModeDriver(final SolarisConnection conn) { super(conn); } @Override public List<SolarisEntry> buildAccountEntries(List<String> blockUserNames, boolean isLast) { conn.doSudoStart(); String out = null; try { conn.executeCommand(conn.buildCommand(false, "rm -f", TMPFILE)); String getUsersScript = buildGetUserScript(blockUserNames, isLast); out = conn.executeCommand(getUsersScript, conn.getConfiguration() .getBlockFetchTimeout()); conn.executeCommand(conn.buildCommand(false, "rm -f", TMPFILE)); } finally { conn.doSudoReset(); } List<SolarisEntry> fetchedEntries = processOutput(out, blockUserNames, isLast); if (fetchedEntries.size() != blockUserNames.size()) { throw new RuntimeException("ERROR: expecting to return " + blockUserNames.size() + " instead of " + fetchedEntries.size()); // TODO possibly compare by content. } return fetchedEntries; } /** retrieve account info from the output. */ private List<SolarisEntry> processOutput(String out, List<String> blockUserNames, boolean isLast) { // SVIDRA# getUsersFromCaptureList(CaptureList captureList, ArrayList // users)() List<String> lines = Arrays.asList(out.split("\n")); Iterator<String> it = lines.iterator(); int captureIndex = 0; List<SolarisEntry> result = new ArrayList<SolarisEntry>(blockUserNames.size()); while (it.hasNext()) { final String currentAccount = blockUserNames.get(captureIndex); String line = it.next(); String lastLoginLine = null; // Weed out shell continuation chars if (line.startsWith(SHELL_CONT_CHARS)) { int index = line.lastIndexOf(SHELL_CONT_CHARS); line = line.substring(index + SHELL_CONT_CHARS.length()); } if (isLast) { if (!it.hasNext()) { throw new ConnectorException(String.format( "User '%s' is missing last login time.", currentAccount)); } lastLoginLine = ""; while (lastLoginLine.length() < 3) { lastLoginLine = it.next(); } } // if (isLast) SolarisEntry entry = buildUser(currentAccount, line, lastLoginLine); if (entry != null) { result.add(entry); } captureIndex++; } // while (it.hasNext()) return result; } /** * build user based on the content given. * * @param loginsLine * @param lastLoginLine * @return the build user. */ private SolarisEntry buildUser(String username, String loginsLine, String lastLoginLine) { if (lastLoginLine == null) { return LoginsCommand.getEntry(loginsLine, username); } else { SolarisEntry.Builder entryBuilder = new SolarisEntry.Builder(username).addAttr(NativeAttribute.NAME, username); // logins SolarisEntry entry = LoginsCommand.getEntry(loginsLine, username); entryBuilder.addAllAttributesFrom(entry); // last Attribute attribute = LastCommand.parseOutput(username, lastLoginLine); entryBuilder.addAttr(NativeAttribute.LAST_LOGIN, attribute.getValue()); return entryBuilder.build(); } } private String buildGetUserScript(List<String> blockUserNames, boolean isLast) { // make a list of users, separated by space. StringBuilder connUserList = new StringBuilder(); int charsThisLine = 0; for (String user : blockUserNames) { final int length = user.length(); // take care that line meets the limit on 160 chars per line if ((charsThisLine + length + 3) > CHARS_PER_LINE) { connUserList.append("\n"); charsThisLine = 0; } connUserList.append(user); connUserList.append(" "); charsThisLine += length + 1; } StringBuilder getUsersScript = new StringBuilder(); getUsersScript.append("WSUSERLIST=\""); getUsersScript.append(connUserList.toString() + "\n\";"); getUsersScript.append("for user in $WSUSERLIST; do "); String getScript = null; if (isLast) { // @formatter:off getScript = conn.buildCommand(true, "logins") + " -oxma -l $user 2>>" + TMPFILE + "; " + "LASTLOGIN=`" + conn.buildCommand(true, "last") + " -1 $user`; " + "if [ -z \"$LASTLOGIN\" ]; then " + "echo \"wtmp begins\" ; " + "else " + "echo $LASTLOGIN; " + "fi; "; // @formatter:on } else { getScript = conn.buildCommand(true, "logins") + " -oxma -l $user 2>>" + TMPFILE + "; "; } getUsersScript.append(getScript); getUsersScript.append("done"); return getUsersScript.toString(); } @Override public SolarisEntry buildAccountEntry(String username, Set<NativeAttribute> attrsToGet) { /** * bunch of boolean flags says if the command is needed to be launched * (based on attributes to get) */ boolean isLogins = LoginsCommand.isLoginsRequired(attrsToGet); boolean isProfiles = attrsToGet.contains(NativeAttribute.PROFILES); boolean isAuths = attrsToGet.contains(NativeAttribute.AUTHS); boolean isLast = attrsToGet.contains(NativeAttribute.LAST_LOGIN); boolean isRoles = attrsToGet.contains(NativeAttribute.ROLES); // if (conn.isNis()) { // return buildNISUser(username); // } SolarisEntry.Builder entryBuilder = new SolarisEntry.Builder(username).addAttr(NativeAttribute.NAME, username); // we need to execute Logins command always, to figure out if the user // exists at all. SolarisEntry loginsEntry = LoginsCommand.getAttributesFor(username, conn); // Null indicates that the user was not found. if (loginsEntry == null) { return null; } if (isLogins) { entryBuilder.addAllAttributesFrom(loginsEntry); } if (isProfiles) { final Attribute profiles = ProfilesCommand.getProfilesAttributeFor(username, conn); entryBuilder.addAttr(NativeAttribute.PROFILES, profiles.getValue()); } if (isAuths) { final Attribute auths = AuthsCommand.getAuthsAttributeFor(username, conn); entryBuilder.addAttr(NativeAttribute.AUTHS, auths.getValue()); } if (isLast) { final Attribute last = LastCommand.getLastAttributeFor(username, conn); entryBuilder.addAttr(NativeAttribute.LAST_LOGIN, last.getValue()); } if (isRoles) { final Attribute roles = RolesCommand.getRolesAttributeFor(username, conn); entryBuilder.addAttr(NativeAttribute.ROLES, roles.getValue()); } return entryBuilder.build(); } @Override public String buildPasswdCommand(String username) { return conn.buildCommand(true, "passwd -r files", username); } @Override public void configurePasswordProperties(SolarisEntry entry, SolarisConnection conn) { Map<NativeAttribute, String> passwdSwitches = buildPasswdSwitches(entry, conn); final String cmdSwitches = CommandSwitches.formatCommandSwitches(entry, conn, passwdSwitches); if (cmdSwitches.length() == 0) { return; // no password related attribute present in the entry. } try { final String command = conn.buildCommand(true, "passwd", cmdSwitches, entry.getName()); final String out = conn.executeCommand(command); final String loweredOut = out.toLowerCase(); if (loweredOut.contains("usage:") || loweredOut.contains("password aging is disabled") || loweredOut.contains("command not found")) { throw new ConnectorException( "Error during configuration of password related attributes. Buffer content: <" + out + ">"); } } catch (Exception ex) { throw ConnectorException.wrap(ex); } } private Map<NativeAttribute, String> buildPasswdSwitches(SolarisEntry entry, SolarisConnection conn) { Map<NativeAttribute, String> passwdSwitches = new EnumMap<NativeAttribute, String>(NativeAttribute.class); passwdSwitches.put(NativeAttribute.PWSTAT, "-f"); // passwdSwitches.put(NativeAttribute.PW_LAST_CHANGE, null); // this is // not used attribute (see LoginsCommand and its SVIDRA counterpart). // TODO erase this comment. passwdSwitches.put(NativeAttribute.MIN_DAYS_BETWEEN_CHNG, "-n"); passwdSwitches.put(NativeAttribute.MAX_DAYS_BETWEEN_CHNG, "-x"); passwdSwitches.put(NativeAttribute.DAYS_BEFORE_TO_WARN, "-w"); String lockFlag = null; Attribute lock = entry.searchForAttribute(NativeAttribute.LOCK); if (lock != null) { Object lockValue = AttributeUtil.getSingleValue(lock); if (lockValue == null) { throw new IllegalArgumentException("missing value for attribute LOCK"); } boolean isLock = (Boolean) lockValue; if (isLock) { lockFlag = "-l"; } else { // *unlocking* differs in Solaris 8,9 and in Solaris 10+: lockFlag = (conn.isVersionLT10()) ? "-df" : "-u"; passwdSwitches.put(NativeAttribute.LOCK, lockFlag); } } if (lockFlag != null) { passwdSwitches.put(NativeAttribute.LOCK, lockFlag); } return passwdSwitches; } @Override public Schema buildSchema() { final SchemaBuilder schemaBuilder = new SchemaBuilder(SolarisConnector.class); /* * GROUP */ Set<AttributeInfo> attributes = new HashSet<AttributeInfo>(); // attributes.add(Name.INFO); for (GroupAttribute attr : GroupAttribute.values()) { switch (attr) { case USERS: attributes.add(AttributeInfoBuilder.build(attr.getName(), String.class, EnumSet .of(Flags.MULTIVALUED))); break; case GROUPNAME: attributes.add(AttributeInfoBuilder.build(attr.getName(), String.class, EnumSet .of(Flags.REQUIRED))); break; case GID: attributes.add(AttributeInfoBuilder.build(attr.getName(), int.class, EnumSet .of(Flags.NOT_RETURNED_BY_DEFAULT))); break; default: attributes.add(AttributeInfoBuilder.build(attr.getName())); break; } } // GROUP supports no authentication: final ObjectClassInfo ociInfoGroup = new ObjectClassInfoBuilder().setType(ObjectClass.GROUP_NAME).addAllAttributeInfo( attributes).build(); schemaBuilder.defineObjectClass(ociInfoGroup); schemaBuilder.removeSupportedObjectClass(AuthenticateOp.class, ociInfoGroup); schemaBuilder.removeSupportedObjectClass(ResolveUsernameOp.class, ociInfoGroup); /* * ACCOUNT */ attributes = new HashSet<AttributeInfo>(); attributes.add(OperationalAttributeInfos.PASSWORD); for (AccountAttribute attr : AccountAttribute.values()) { String attrName = attr.getName(); AttributeInfo newAttr = null; switch (attr) { case NAME: newAttr = AttributeInfoBuilder.build(attrName, String.class, EnumSet .of(Flags.REQUIRED)); break; case MIN: case MAX: case WARN: case INACTIVE: case UID: newAttr = AttributeInfoBuilder.build(attrName, int.class, EnumSet .of(Flags.NOT_RETURNED_BY_DEFAULT)); break; case PASSWD_FORCE_CHANGE: case LOCK: newAttr = AttributeInfoBuilder.build(attrName, boolean.class, EnumSet .of(Flags.NOT_RETURNED_BY_DEFAULT)); break; case SECONDARY_GROUP: case ROLES: case AUTHORIZATION: case PROFILE: newAttr = AttributeInfoBuilder.build(attrName, String.class, EnumSet.of( Flags.MULTIVALUED, Flags.NOT_RETURNED_BY_DEFAULT)); break; case REGISTRY: case TIME_LAST_LOGIN: newAttr = AttributeInfoBuilder.build(attrName, String.class, EnumSet.of( Flags.NOT_UPDATEABLE, Flags.NOT_CREATABLE, Flags.NOT_RETURNED_BY_DEFAULT)); break; default: newAttr = AttributeInfoBuilder.build(attrName); break; } if (newAttr != null) { attributes.add(newAttr); } } tweakAccountActivationSchema(attributes); final ObjectClassInfo ociInfoAccount = new ObjectClassInfoBuilder().setType(ObjectClass.ACCOUNT_NAME).addAllAttributeInfo( attributes).build(); schemaBuilder.defineObjectClass(ociInfoAccount); /* * SHELL */ attributes = new HashSet<AttributeInfo>(); attributes.add(AttributeInfoBuilder.build(SolarisSearch.SHELL.getObjectClassValue(), String.class, EnumSet.of(Flags.MULTIVALUED, Flags.NOT_RETURNED_BY_DEFAULT, Flags.NOT_UPDATEABLE))); final ObjectClassInfo ociInfoShell = new ObjectClassInfoBuilder().addAllAttributeInfo(attributes).setType( SolarisSearch.SHELL.getObjectClassValue()).build(); schemaBuilder.defineObjectClass(ociInfoShell); schemaBuilder.removeSupportedObjectClass(AuthenticateOp.class, ociInfoShell); schemaBuilder.removeSupportedObjectClass(CreateOp.class, ociInfoShell); schemaBuilder.removeSupportedObjectClass(UpdateOp.class, ociInfoShell); schemaBuilder.removeSupportedObjectClass(DeleteOp.class, ociInfoShell); schemaBuilder.removeSupportedObjectClass(SchemaOp.class, ociInfoShell); schemaBuilder.removeSupportedObjectClass(ResolveUsernameOp.class, ociInfoShell); return schemaBuilder.build(); } @Override public String getSudoPasswordRegexp() { return "^[pP]assword[^:]*:"; } @Override public String getRenameDirScript(SolarisEntry entry, String newName) { // @formatter:off String renameDir = "NEWNAME=" + newName + "; " + "OLDNAME=" + entry.getName() + "; " + "OLDDIR=`" + conn.buildCommand(true, "logins") + " -ox -l $NEWNAME | cut -d: -f6`; " + "OLDBASE=`basename $OLDDIR`; " + "if [ \"$OLDNAME\" = \"$OLDBASE\" ]; then\n" + "PARENTDIR=`dirname $OLDDIR`; " + "NEWDIR=`echo $PARENTDIR/$NEWNAME`; " + "if [ ! -s $NEWDIR ]; then " + conn.buildCommand(true, "chown") + " $NEWNAME $OLDDIR; " + conn.buildCommand(true, "mv") + " -f $OLDDIR $NEWDIR; " + "if [ $? -eq 0 ]; then\n" + conn.buildCommand(true, "usermod") + " -d $NEWDIR $NEWNAME; " + "fi; " + "fi; " + "fi"; // @formatter:off return renameDir; } @Override public String formatDate(long daysSinceEpoch) { return DATE_FORMAT.format(daysSinceEpoch*(24*60*60*1000)); } }