/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 2013-2014 ForgeRock AS. All Rights Reserved
*
* 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://forgerock.org/license/CDDLv1.0.html
* See the License for the specific language governing
* permission and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* at http://forgerock.org/license/CDDLv1.0.html
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*/
package org.identityconnectors.ldap.sync.timestamps;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.TimeZone;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.PartialResultException;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.PagedResultsControl;
import org.identityconnectors.common.logging.Log;
import org.identityconnectors.common.security.GuardedString;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.AttributeBuilder;
import org.identityconnectors.framework.common.objects.ConnectorObjectBuilder;
import org.identityconnectors.framework.common.objects.ObjectClass;
import org.identityconnectors.framework.common.objects.OperationOptions;
import org.identityconnectors.framework.common.objects.OperationalAttributes;
import org.identityconnectors.framework.common.objects.SyncDeltaBuilder;
import org.identityconnectors.framework.common.objects.SyncDeltaType;
import org.identityconnectors.framework.common.objects.SyncResultsHandler;
import org.identityconnectors.framework.common.objects.SyncToken;
import org.identityconnectors.framework.common.objects.Uid;
import org.identityconnectors.framework.spi.SyncTokenResultsHandler;
import org.identityconnectors.ldap.ADLdapUtil;
import org.identityconnectors.ldap.LdapEntry;
import static org.identityconnectors.ldap.ADLdapUtil.fetchGroupMembersByRange;
import static org.identityconnectors.ldap.ADLdapUtil.getADLdapDatefromJavaDate;
import static org.identityconnectors.ldap.ADLdapUtil.getJavaDateFromADTime;
import static org.identityconnectors.ldap.ADLdapUtil.objectGUIDtoString;
import org.identityconnectors.ldap.LdapConnection;
import static org.identityconnectors.ldap.LdapUtil.buildMemberIdAttribute;
import org.identityconnectors.ldap.ADUserAccountControl;
import org.identityconnectors.ldap.LdapConnection.ServerType;
import org.identityconnectors.ldap.LdapConstants;
import static org.identityconnectors.ldap.LdapConstants.OBJECTCLASS_ATTR;
import static org.identityconnectors.ldap.LdapUtil.getObjectClassFilter;
import static org.identityconnectors.ldap.LdapUtil.guessObjectClass;
import org.identityconnectors.ldap.search.DefaultSearchStrategy;
import org.identityconnectors.ldap.search.LdapInternalSearch;
import org.identityconnectors.ldap.search.LdapSearchStrategy;
import org.identityconnectors.ldap.search.LdapSearchResultsHandler;
import org.identityconnectors.ldap.search.SimplePagedSearchStrategy;
import org.identityconnectors.ldap.sync.LdapSyncStrategy;
/**
*
* @author Gael Allioux <gael.allioux@forgerock.com>
*/
/**
* An implementation of the sync operation based on the generic timestamps
* attribute present in any LDAP directory.
*/
public class TimestampsSyncStrategy implements LdapSyncStrategy {
private final String createTimestamp = "createTimestamp";
private final String modifyTimestamp = "modifyTimestamp";
private final LdapConnection conn;
private final ObjectClass oclass;
private final ServerType server;
private static final Log logger = Log.getLog(TimestampsSyncStrategy.class);
public TimestampsSyncStrategy(LdapConnection conn, ObjectClass oclass) {
this.conn = conn;
this.oclass = oclass;
this.server = conn.getServerType();
}
public SyncToken getLatestSyncToken() {
return new SyncToken(getNowTime());
}
public void sync(SyncToken token, final SyncResultsHandler handler, final OperationOptions options) {
// ldapsearch -h host -p 389 -b "dc=example,dc=com" -D "cn=administrator,cn=users,dc=example,dc=com" -w xxx "whenchanged>=20130214130642.0Z"
// on AD
// ldapsearch -h host -p 389 -b 'dc=example,dc=com' -S modifytimestamp -D 'cn=directory manager' -w xxx "createTimestamp>=20120424080554Z"
// on other directories
final String now = getNowTime();
LdapSearchStrategy strategy;
SearchControls controls = LdapInternalSearch.createDefaultSearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
controls.setDerefLinkFlag(false);
controls.setReturningAttributes(new String[]{"*", createTimestamp, modifyTimestamp,conn.getConfiguration().getUidAttribute()});
if (conn.getConfiguration().getUseBlocks() && conn.supportsControl(PagedResultsControl.OID)) {
strategy = new SimplePagedSearchStrategy(conn.getConfiguration().getBlockSize());
} else {
strategy = new DefaultSearchStrategy(false);
}
LdapInternalSearch search = new LdapInternalSearch(conn,
generateFilter(oclass, token),
Arrays.asList(conn.getConfiguration().getBaseContextsToSynchronize()),
strategy, controls);
try {
search.execute(new LdapSearchResultsHandler() {
public boolean handle(String baseDN, SearchResult result) throws NamingException {
Attributes attrs = result.getAttributes();
String uidAttribute = conn.getConfiguration().getUidAttribute();
Uid uid;
if (LdapEntry.isDNAttribute(uidAttribute)) {
uid = new Uid(result.getNameInNamespace());
} else {
uid = conn.getSchemaMapping().createUid(uidAttribute, attrs);
}
// build the object first
ConnectorObjectBuilder cob = new ConnectorObjectBuilder();
cob.setUid(uid);
if (ObjectClass.ALL.equals(oclass)) {
cob.setObjectClass(guessObjectClass(conn, attrs.get(OBJECTCLASS_ATTR)));
} else {
cob.setObjectClass(oclass);
}
cob.setName(result.getNameInNamespace());
// Let's process AD specifics...
if (ServerType.MSAD_GC.equals(server) || ServerType.MSAD.equals(server) || ServerType.MSAD_LDS.equals(server)) {
if (ObjectClass.ACCOUNT.equals(oclass)) {
if (ServerType.MSAD_LDS.equals(server)) {
if (attrs.get(LdapConstants.MS_DS_USER_ACCOUNT_DISABLED) != null) {
cob.addAttribute(AttributeBuilder.buildEnabled(!Boolean.parseBoolean(attrs.get(LdapConstants.MS_DS_USER_ACCOUNT_DISABLED).get().toString())));
} else if (attrs.get(LdapConstants.MS_DS_USER_PASSWORD_EXPIRED) != null) {
cob.addAttribute(AttributeBuilder.buildPasswordExpired(Boolean.parseBoolean(attrs.get(LdapConstants.MS_DS_USER_PASSWORD_EXPIRED).get().toString())));
} else if (attrs.get(LdapConstants.MS_DS_USER_ACCOUNT_AUTOLOCKED) != null) {
cob.addAttribute(AttributeBuilder.buildLockOut(Boolean.parseBoolean(attrs.get(LdapConstants.MS_DS_USER_ACCOUNT_AUTOLOCKED).get().toString())));
}
} else {
javax.naming.directory.Attribute uac = attrs.get(ADUserAccountControl.MS_USR_ACCT_CTRL_ATTR);
if (uac != null) {
String controls = uac.get().toString();
cob.addAttribute(AttributeBuilder.buildEnabled(!ADUserAccountControl.isAccountDisabled(controls)));
cob.addAttribute(AttributeBuilder.buildLockOut(ADUserAccountControl.isAccountLockOut(controls)));
cob.addAttribute(AttributeBuilder.buildPasswordExpired(ADUserAccountControl.isPasswordExpired(controls)));
}
}
if (attrs.get(ADLdapUtil.ACCOUNT_EXPIRES) != null) {
String value = (String) attrs.get(ADLdapUtil.ACCOUNT_EXPIRES).get();
if ("0".equalsIgnoreCase(value) || ADLdapUtil.ACCOUNT_NEVER_EXPIRES.equalsIgnoreCase(value)) {
// Let's set it to zero - this is equivalent: it means Never
cob.addAttribute(AttributeBuilder.build(ADLdapUtil.ACCOUNT_EXPIRES, "0"));
} else {
Date date = getJavaDateFromADTime(value);
cob.addAttribute(AttributeBuilder.build(ADLdapUtil.ACCOUNT_EXPIRES, getADLdapDatefromJavaDate(date)));
}
attrs.remove(ADLdapUtil.ACCOUNT_EXPIRES);
}
if (attrs.get(ADLdapUtil.PWD_LAST_SET) != null) {
String value = (String) attrs.get(ADLdapUtil.PWD_LAST_SET).get();
if ("0".equalsIgnoreCase(value)) {
cob.addAttribute(AttributeBuilder.build(ADLdapUtil.PWD_LAST_SET, "0"));
} else {
Date date = getJavaDateFromADTime(value);
cob.addAttribute(AttributeBuilder.build(ADLdapUtil.PWD_LAST_SET, getADLdapDatefromJavaDate(date)));
}
attrs.remove(ADLdapUtil.PWD_LAST_SET);
}
}
if (ObjectClass.GROUP.equals(oclass)) {
// Make sure we're not hitting AD large group issue
// see: http://msdn.microsoft.com/en-us/library/ms817827.aspx
if (attrs.get("member;range=0-1499") != null) {
// we're in the limitation
Attribute range = AttributeBuilder.build("member", fetchGroupMembersByRange(conn, result));
cob.addAttribute(range);
if (conn.getConfiguration().isGetGroupMemberId()){
cob.addAttribute(buildMemberIdAttribute(conn, range));
}
attrs.remove("member;range=0-1499");
attrs.remove("member");
}
}
javax.naming.directory.Attribute guid = attrs.get(LdapConstants.MS_GUID_ATTR);
if (guid != null) {
cob.addAttribute(AttributeBuilder.build(LdapConstants.MS_GUID_ATTR, objectGUIDtoString(guid)));
attrs.remove(LdapConstants.MS_GUID_ATTR);
}
}
// Set all Attributes
NamingEnumeration<? extends javax.naming.directory.Attribute> attrsEnum = attrs.getAll();
while (attrsEnum.hasMore()) {
javax.naming.directory.Attribute attr = attrsEnum.next();
String id = attr.getID();
NamingEnumeration vals = attr.getAll();
ArrayList values = new ArrayList();
while (vals.hasMore()) {
Object val = vals.next();
if ("userPassword".equals(id)) {
byte[] passBytes = (byte[])val;
try {
val = new GuardedString((new String(passBytes, "UTF-8")).toCharArray());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e.getMessage(),e);
}
}
values.add(val);
}
if (conn.getConfiguration().isGetGroupMemberId() && ObjectClass.GROUP.equals(oclass)
&& id.equalsIgnoreCase(conn.getConfiguration().getGroupMemberAttribute())) {
cob.addAttribute(buildMemberIdAttribute(conn, attr));
}
if ("userPassword".equals(id)) {
id = OperationalAttributes.PASSWORD_NAME;
}
cob.addAttribute(AttributeBuilder.build(id, values));
}
SyncDeltaBuilder syncDeltaBuilder = new SyncDeltaBuilder();
syncDeltaBuilder.setToken(new SyncToken(now));
syncDeltaBuilder.setDeltaType(SyncDeltaType.CREATE_OR_UPDATE);
syncDeltaBuilder.setUid(uid);
syncDeltaBuilder.setObject(cob.build());
return handler.handle(syncDeltaBuilder.build());
}
});
// ICF 1.4 now allows us to send the Token even if no entries were actually processed
((SyncTokenResultsHandler)handler).handleResult(new SyncToken(now));
} catch (ConnectorException e) {
if (e.getCause() instanceof PartialResultException) {
logger.warn("PartialResultException has been caught");
} else {
throw e;
}
}
}
@SuppressWarnings("fallthrough")
private String getNowTime() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
switch (server) {
case MSAD_GC:
case MSAD:
case MSAD_LDS:
return sdf.format(new Date()) + ".0Z";
default:
return sdf.format(new Date()) + "Z";
}
}
private String generateFilter(ObjectClass oc, SyncToken token) {
StringBuilder filter;
filter = new StringBuilder();
if (token == null) {
token = this.getLatestSyncToken();
}
if (ObjectClass.ACCOUNT.equals(oc)) {
filter.append(getObjectClassFilter(conn.getConfiguration().getAccountObjectClasses()));
if (conn.getConfiguration().getAccountSynchronizationFilter() != null){
filter.append(conn.getConfiguration().getAccountSynchronizationFilter());
}
} else if (ObjectClass.GROUP.equals(oc)) {
filter.append(getObjectClassFilter(conn.getConfiguration().getGroupObjectClasses()));
if (conn.getConfiguration().getGroupSynchronizationFilter() != null){
filter.append(conn.getConfiguration().getGroupSynchronizationFilter());
}
} else if (ObjectClass.ALL.equals(oc)) {
filter.append(getObjectClassFilter(conn.getConfiguration().getObjectClassesToSynchronize()));
} else { // we use the ObjectClass value as the filter...
filter.append("(objectClass=");
filter.append(oc.getObjectClassValue());
filter.append(")");
}
filter.append("(|(");
filter.append(modifyTimestamp);
filter.append(">=");
filter.append(token.getValue().toString());
filter.append(")(");
filter.append(createTimestamp);
filter.append(">=");
filter.append(token.getValue().toString());
filter.append("))");
filter.insert(0, "(&");
filter.append(")");
logger.info("Using timestamp filter {0}",filter.toString());
return filter.toString();
}
}