/**
* 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
*/
package org.identityconnectors.solaris.mode;
import java.text.DateFormat;
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.common.logging.Log;
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.GetentPasswordCommand;
import org.identityconnectors.solaris.operation.search.GetentShadowCommand;
import org.identityconnectors.solaris.operation.search.IdCommand;
import org.identityconnectors.solaris.operation.search.PasswdCommand;
import org.identityconnectors.solaris.operation.search.SolarisEntry;
import org.identityconnectors.solaris.operation.search.SolarisSearch;
/**
* Driver for linux-specific user management commands.
*
* Partially copied from the original (hard-coded) Solaris connector code and
* modified for the linux-specific commands. It is using combination of
* 'getent', 'id' and 'passwd' commands instead the solaris-specific 'logins'
* command.
*
* The code is not ideal. But the goal was to do minimal modifications to
* original Solaris connector.
*
* @see UnixModeDriver
*
* @author Radovan Semancik
*
*/
public class LinuxModeDriver extends UnixModeDriver {
public static final String MODE_NAME = "linux";
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 Log logger = Log.getLog(LinuxModeDriver.class);
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-DD");
public LinuxModeDriver(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);
logger.info("Script: {0}", getUsersScript);
out =
conn.executeCommand(getUsersScript, conn.getConfiguration()
.getBlockFetchTimeout());
logger.info("OUT: {0}", out);
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;
}
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 scriptBuilder = new StringBuilder();
scriptBuilder.append("WSUSERLIST=\"");
scriptBuilder.append(connUserList.toString() + "\n\";");
scriptBuilder.append("for user in $WSUSERLIST; do ");
scriptBuilder.append(buildGetUserScriptLine(true, "getent", "passwd $user"));
scriptBuilder.append(buildGetUserScriptLine(true, "id", "-Grn $user"));
scriptBuilder.append(buildGetUserScriptLine(true, "passwd", "-S $user"));
if (isLast) {
// TODO
}
scriptBuilder.append("done");
return scriptBuilder.toString();
}
private String buildGetUserScriptLine(boolean needSudo, String command, String args) {
return conn.buildCommand(needSudo, command) + (args == null ? "" : " " + args) + " 2>>"
+ TMPFILE + "; ";
}
/** Retrieve account info from the output. */
private List<SolarisEntry> processOutput(String out, List<String> blockUserNames, boolean isLast) {
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 accountUsername = blockUserNames.get(captureIndex);
String linePwent = readLine(it, accountUsername, "pwent");
String lineId = readLine(it, accountUsername, "id");
String linePasswd = readLine(it, accountUsername, "passwd");
String lineLastLogin = null;
if (isLast) {
lineLastLogin = readLine(it, accountUsername, "last login");
} // if (isLast)
SolarisEntry.Builder entryBuilder = new SolarisEntry.Builder(accountUsername);
SolarisEntry getentEntry = GetentPasswordCommand.getEntry(linePwent, accountUsername);
entryBuilder.addAllAttributesFrom(getentEntry);
SolarisEntry idEntry = IdCommand.getEntry(lineId, accountUsername);
entryBuilder.addAllAttributesFrom(idEntry);
SolarisEntry passwdEntry = PasswdCommand.getEntry(linePasswd, accountUsername);
entryBuilder.addAllAttributesFrom(passwdEntry);
SolarisEntry entry = entryBuilder.build();
if (entry != null) {
result.add(entry);
}
captureIndex++;
} // while (it.hasNext())
return result;
}
private String readLine(Iterator<String> lineIterator, String username, String lineType) {
if (!lineIterator.hasNext()) {
throw new ConnectorException(String.format("User '%s' is missing %s time.", username,
lineType));
}
String line = lineIterator.next();
String noContLine = weedOutShellContChars(line);
return weedOutControlChars(noContLine);
}
private String weedOutShellContChars(String line) {
// 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());
}
return line;
}
private String weedOutControlChars(String line) {
if (line.endsWith("\r")) {
return line.substring(0, line.length() - 1);
} else {
return line;
}
}
@Override
public SolarisEntry buildAccountEntry(String username, Set<NativeAttribute> attrsToGet) {
// TODO: this has a lot of common code with the buildAccountEntries.
// Refactor.
boolean isLast = attrsToGet.contains(NativeAttribute.LAST_LOGIN);
conn.doSudoStart();
String out = null;
try {
conn.executeCommand(conn.buildCommand(false, "rm -f", TMPFILE));
StringBuilder scriptBuilder = new StringBuilder();
scriptBuilder.append(buildGetUserScriptLine(true, "getent", "passwd " + username));
scriptBuilder.append(buildGetUserScriptLine(true, "id", "-Grn " + username));
scriptBuilder.append(buildGetUserScriptLine(true, "passwd", "-S " + username));
scriptBuilder.append(buildGetUserScriptLine(true, "getent", "shadow " + username));
if (isLast) {
// TODO
}
String script = scriptBuilder.toString();
logger.info("Script: {0}", script);
out = conn.executeCommand(script, conn.getConfiguration().getBlockFetchTimeout());
logger.info("OUT: {0}", out);
conn.executeCommand(conn.buildCommand(false, "rm -f", TMPFILE));
} finally {
conn.doSudoReset();
}
List<String> lines = Arrays.asList(out.split("\n"));
Iterator<String> it = lines.iterator();
String linePwent = readLine(it, username, "pwent");
String lineId = readLine(it, username, "id");
String linePasswd = readLine(it, username, "passwd");
String lineShadow = readLine(it, username, "shadow");
SolarisEntry.Builder entryBuilder = new SolarisEntry.Builder(username);
SolarisEntry getentEntry = GetentPasswordCommand.getEntry(linePwent, username);
entryBuilder.addAllAttributesFrom(getentEntry);
SolarisEntry idEntry = IdCommand.getEntry(lineId, username);
entryBuilder.addAllAttributesFrom(idEntry);
SolarisEntry passwdEntry = PasswdCommand.getEntry(linePasswd, username);
entryBuilder.addAllAttributesFrom(passwdEntry);
SolarisEntry shadowEntry = GetentShadowCommand.getEntry(lineShadow, username);
entryBuilder.addAllAttributesFrom(shadowEntry);
SolarisEntry entry = entryBuilder.build();
return entry;
}
@Override
public String buildPasswdCommand(String username) {
return conn.buildCommand(true, "passwd", username);
}
@Override
public void configurePasswordProperties(SolarisEntry entry, SolarisConnection conn) {
// Linux has to handle lock/unlock in a separate command
Map<NativeAttribute, String> passwdSwitches = buildPasswdSwitchesLock(entry, conn);
String cmdSwitches = CommandSwitches.formatCommandSwitches(entry, conn, passwdSwitches);
if (!cmdSwitches.isEmpty()) {
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);
}
}
passwdSwitches = buildPasswdSwitches(entry, conn);
cmdSwitches = CommandSwitches.formatCommandSwitches(entry, conn, passwdSwitches);
if (!cmdSwitches.isEmpty()) {
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);
// Immediately expire password. This forces password change.
passwdSwitches.put(NativeAttribute.PWSTAT, "-e");
passwdSwitches.put(NativeAttribute.MIN_DAYS_BETWEEN_CHNG, "-n");
passwdSwitches.put(NativeAttribute.MAX_DAYS_BETWEEN_CHNG, "-x");
passwdSwitches.put(NativeAttribute.DAYS_BEFORE_TO_WARN, "-w");
return passwdSwitches;
}
private Map<NativeAttribute, String> buildPasswdSwitchesLock(SolarisEntry entry,
SolarisConnection conn) {
Map<NativeAttribute, String> passwdSwitches =
new EnumMap<NativeAttribute, String>(NativeAttribute.class);
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 {
lockFlag = "-u";
}
}
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;
default:
attributes.add(AttributeInfoBuilder.build(attr.getName()));
break;
} // switch
} // for
// 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 EXPIRE:
case UID:
newAttr = AttributeInfoBuilder.build(attrName, long.class);
break;
case PASSWD_FORCE_CHANGE:
newAttr =
AttributeInfoBuilder.build(attrName, boolean.class, EnumSet
.of(Flags.NOT_RETURNED_BY_DEFAULT));
break;
case SECONDARY_GROUP:
newAttr =
AttributeInfoBuilder.build(attrName, String.class, EnumSet
.of(Flags.MULTIVALUED));
break;
case ROLES:
case AUTHORIZATION:
case PROFILE:
newAttr = null;
break;
case REGISTRY:
case TIME_LAST_LOGIN:
newAttr =
AttributeInfoBuilder.build(attrName, String.class, EnumSet.of(
Flags.NOT_UPDATEABLE, 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 "\\[sudo\\].*[pP]assword[^:]*:";
}
@Override
public String getRenameDirScript(SolarisEntry entry, String newName) {
// @formatter:off
String renameDir =
"NEWNAME=" + newName + "; " +
"OLDNAME=" + entry.getName() + "; " +
"OLDDIR=`" + conn.buildCommand(true, "getent") + " passwd $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));
}
}