org.connid.bundles.ad.sync.ADSyncStrategy Maven / Gradle / Ivy
The newest version!
/**
* Copyright (C) 2011 ConnId ([email protected])
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.connid.bundles.ad.sync;
import static org.connid.bundles.ad.ADConnector.OBJECTGUID;
import com.sun.jndi.ldap.ctl.DirSyncResponseControl;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.LdapContext;
import net.tirasa.adsddl.ntsd.data.SDFlagsControl;
import org.connid.bundles.ad.ADConfiguration;
import org.connid.bundles.ad.ADConnection;
import org.connid.bundles.ad.util.ADUtilities;
import org.connid.bundles.ad.util.DeletedControl;
import org.connid.bundles.ad.util.DirSyncControl;
import org.connid.bundles.ad.util.DirSyncUtils;
import org.connid.bundles.ldap.search.LdapInternalSearch;
import org.identityconnectors.common.StringUtil;
import org.identityconnectors.common.logging.Log;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.framework.common.objects.ObjectClass;
import org.identityconnectors.framework.common.objects.OperationOptions;
import org.identityconnectors.framework.common.objects.SyncDelta;
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;
/**
* An implementation of the sync operation based on the DirSync protocol, for Active Directory.
*/
public class ADSyncStrategy {
private static final Log LOG = Log.getLog(ADSyncStrategy.class);
private final transient ADConnection conn;
private transient SyncToken latestSyncToken;
private final ADUtilities utils;
public ADSyncStrategy(final ADConnection conn) {
this.conn = conn;
this.utils = new ADUtilities(conn);
}
private Set search(
final LdapContext ctx,
final String filter,
final SearchControls searchCtls,
final boolean updateLastSyncToken) {
final Set result = new HashSet();
for (String baseContextDn : conn.getConfiguration().getBaseContextsToSynchronize()) {
if (LOG.isOk()) {
LOG.ok("Searching from " + baseContextDn);
}
try {
final NamingEnumeration answer = ctx.search(baseContextDn, filter, searchCtls);
while (answer.hasMoreElements()) {
result.add(answer.nextElement());
}
if (updateLastSyncToken) {
final Control[] rspCtls = ctx.getResponseControls();
if (rspCtls != null) {
if (LOG.isOk()) {
LOG.ok("Response Controls: {0}", rspCtls.length);
}
for (Control rspCtl : rspCtls) {
if (rspCtl instanceof DirSyncResponseControl) {
DirSyncResponseControl dirSyncRspCtl = (DirSyncResponseControl) rspCtl;
latestSyncToken = new SyncToken(dirSyncRspCtl.getCookie());
}
}
if (LOG.isOk()) {
LOG.ok("Latest sync token set to {0}", latestSyncToken);
}
}
}
} catch (NamingException e) {
LOG.error(e, "While searching base context {0} with filter {1} and search controls {2}",
baseContextDn, filter, searchCtls);
}
}
return result;
}
public void sync(
final SyncToken token,
final SyncResultsHandler handler,
final OperationOptions options,
final ObjectClass oclass) {
// -----------------------------------
// Create basicLdapSearch control
// -----------------------------------
final SearchControls searchCtls = LdapInternalSearch.createDefaultSearchControls();
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
searchCtls.setReturningAttributes(null);
// -----------------------------------
// -----------------------------------
// Get Synchronization Context
// -----------------------------------
final LdapContext ctx;
try {
if (token == null
|| token.getValue() == null
|| !(token.getValue() instanceof byte[])
|| ((byte[]) token.getValue()).length == 0) {
if (LOG.isOk()) {
LOG.ok("Synchronization with empty token.");
}
ctx = conn.getSyncContext(new Control[] { new DirSyncControl() });
if (((ADConfiguration) conn.getConfiguration()).isStartSyncFromToday()) {
search(ctx, "(cn=__CONNID-NORES__)", searchCtls, true);
return;
}
} else {
if (LOG.isOk()) {
LOG.ok("Synchronization with token.");
}
ctx = conn.getSyncContext(new Control[] { new DirSyncControl((byte[]) token.getValue()) });
}
} catch (Exception e) {
throw new ConnectorException("Could not set DirSync request controls", e);
}
// -----------------------------------
// -----------------------------------
// Create basicLdapSearch filter
// -----------------------------------
final String filter = oclass.is(ObjectClass.ACCOUNT_NAME)
? // get user filter
DirSyncUtils.createDirSyncUFilter((ADConfiguration) conn.getConfiguration(), utils)
: // get group filter
DirSyncUtils.createDirSyncGFilter((ADConfiguration) conn.getConfiguration());
if (LOG.isOk()) {
LOG.ok("Search filter: " + filter);
}
// -----------------------------------
final String[] attrsToGetOption = options.getAttributesToGet();
final Set attrsToGet = utils.getAttributesToGet(attrsToGetOption, oclass);
final Set changes = search(ctx, filter, searchCtls, true);
if (oclass.is(ObjectClass.ACCOUNT_NAME)) {
for (SearchResult sr : changes) {
try {
handleSyncUDelta(ctx, sr, attrsToGet, handler);
} catch (NamingException e) {
LOG.error(e, "SyncDelta handling for '{0}' failed", sr.getName());
}
}
} else {
for (SearchResult sr : changes) {
try {
handleSyncGDelta(ctx, sr, attrsToGet, handler);
} catch (NamingException e) {
LOG.error(e, "SyncDelta handling for '{0}' failed", sr.getName());
}
}
}
}
public SyncToken getLatestSyncToken() {
// -----------------------------------
// Create basicLdapSearch control
// -----------------------------------
final SearchControls searchCtls = LdapInternalSearch.createDefaultSearchControls();
searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE);
searchCtls.setReturningAttributes(null);
// -----------------------------------
SyncToken token = null;
final String baseContextDn = conn.getConfiguration().getBaseContextsToSynchronize()[0];
final String filter = "(CN=__CONNID-NORES__)";
try {
final LdapContext ctx = conn.getSyncContext(new Control[] { new DirSyncControl() });
ctx.search(baseContextDn, filter, searchCtls);
final Control[] rspCtls = ctx.getResponseControls();
if (rspCtls != null) {
for (Control rspCtl : rspCtls) {
if (rspCtl instanceof DirSyncResponseControl) {
final DirSyncResponseControl dirSyncRspCtl = (DirSyncResponseControl) rspCtl;
token = new SyncToken(dirSyncRspCtl.getCookie());
}
}
if (LOG.isOk()) {
LOG.ok("Latest sync token set to {0}", token);
}
}
} catch (Exception e) {
LOG.error(e, "While searching for {0} with filter {1} and controls {2}", baseContextDn, filter, searchCtls);
}
return token;
}
@SuppressWarnings("unchecked")
private void handleSyncUDelta(
final LdapContext ctx,
final SearchResult result,
final Collection attrsToGet,
final SyncResultsHandler handler)
throws NamingException {
if (ctx == null || result == null) {
throw new ConnectorException("Invalid context or search result.");
}
try {
ctx.setRequestControls(new Control[] { new DeletedControl(), new SDFlagsControl(0x00000004) });
} catch (IOException e) {
LOG.error(e, "Error initializing context request controls");
throw new ConnectorException(e);
}
// Just used to retrieve object classes and to pass to getSyncDelta
Attributes profile = result.getAttributes();
if (LOG.isOk()) {
LOG.ok("Object profile: {0}", profile);
}
final String guid = DirSyncUtils.getGuidAsString((byte[]) profile.get("objectGUID").get());
boolean isDeleted = false;
try {
javax.naming.directory.Attribute attributeIsDeleted = profile.get("isDeleted");
isDeleted = attributeIsDeleted != null
&& attributeIsDeleted.get() != null
&& Boolean.parseBoolean(attributeIsDeleted.get().toString());
} catch (NoSuchElementException e) {
if (LOG.isOk()) {
LOG.ok("Cannot find the isDeleted element for user.");
}
} catch (Throwable t) {
LOG.error(t, "Error retrieving isDeleted attribute");
}
// We need for this beacause DirSync can return an uncomplete profile.
profile = ctx.getAttributes("");
final Attribute objectClasses = profile.get("objectClass");
final ADConfiguration conf = (ADConfiguration) conn.getConfiguration();
final javax.naming.directory.Attribute member11;
final javax.naming.directory.Attribute member00;
if (objectClasses.contains("group")) {
// basicLdapSearch for users in adn users out
if (LOG.isOk()) {
LOG.ok("Modified group {0}", result.getNameInNamespace());
}
member11 = result.getAttributes().get("member;range=1-1");
member00 = result.getAttributes().get("member;range=0-0");
ctx.setRequestControls(null);
if (member11 != null) {
if (LOG.isOk()) {
LOG.ok("Found users 'IN' ...");
}
handleInOutEntries(
ctx,
ObjectClass.ACCOUNT,
(NamingEnumeration) member11.getAll(),
DirSyncUtils.getUserFilter(conf),
handler,
conf,
attrsToGet);
}
if (member00 != null && conf.isRetrieveDeletedUser()) {
// users to be removed
if (LOG.isOk()) {
LOG.ok("Found users 'OUT' ...");
}
handleInOutEntries(
ctx,
ObjectClass.ACCOUNT,
(NamingEnumeration) member00.getAll(),
DirSyncUtils.getUserFilter(conf),
handler,
conf,
attrsToGet);
}
} else if (objectClasses.contains("user")) {
if (LOG.isOk()) {
LOG.ok("Created/Updated/Deleted user {0}",
result.getNameInNamespace());
}
if (isDeleted) {
if (LOG.isOk()) {
LOG.ok("Deleted user {0}", result.getNameInNamespace());
}
if (conf.isRetrieveDeletedUser()) {
handler.handle(getSyncDelta(
ObjectClass.ACCOUNT,
result.getNameInNamespace(),
SyncDeltaType.DELETE,
profile,
attrsToGet));
}
} else {
// user to be created/updated
if (LOG.isOk()) {
LOG.ok("Created/Updated user {0}", result.getNameInNamespace());
}
handleEntry(
ctx,
ObjectClass.ACCOUNT,
result.getNameInNamespace(),
DirSyncUtils.getUserFilter(conf),
handler,
conf,
attrsToGet);
}
} else {
if (LOG.isInfo()) {
LOG.info("Invalid object type {0}", objectClasses);
}
}
}
@SuppressWarnings("unchecked")
private void handleSyncGDelta(
final LdapContext ctx,
final SearchResult sr,
final Collection attrsToGet,
final SyncResultsHandler handler)
throws NamingException {
if (ctx == null || sr == null) {
throw new ConnectorException("Invalid context or search result.");
}
ctx.setRequestControls(new Control[] { new DeletedControl() });
// Just used to retrieve object classes and to pass to getSyncDelta
Attributes profile = sr.getAttributes();
if (LOG.isOk()) {
LOG.ok("Object profile: {0}", profile);
}
String guid = DirSyncUtils.getGuidAsString((byte[]) profile.get(OBJECTGUID).get());
boolean isDeleted = false;
try {
javax.naming.directory.Attribute attributeIsDeleted = profile.get("isDeleted");
isDeleted = attributeIsDeleted != null
&& attributeIsDeleted.get() != null
&& Boolean.parseBoolean(
attributeIsDeleted.get().toString());
} catch (NoSuchElementException e) {
if (LOG.isOk()) {
LOG.ok("Cannot find the isDeleted element for group.");
}
} catch (Throwable t) {
LOG.error(t, "Error retrieving isDeleted attribute");
}
// We need for this beacause DirSync can return an uncomplete profile.
profile = ctx.getAttributes("");
final Attribute objectClasses = profile.get("objectClass");
if (objectClasses.contains("group")) {
final ADConfiguration conf = (ADConfiguration) conn.getConfiguration();
if (LOG.isOk()) {
LOG.ok("Created/Updated/Deleted group {0}", sr.getNameInNamespace());
}
if (isDeleted) {
if (LOG.isOk()) {
LOG.ok("Deleted group {0}", sr.getNameInNamespace());
}
if (conf.isRetrieveDeletedGroup()) {
handler.handle(getSyncDelta(
ObjectClass.GROUP,
sr.getNameInNamespace(),
SyncDeltaType.DELETE,
profile,
attrsToGet));
}
} else {
// user to be created/updated
if (LOG.isOk()) {
LOG.ok("Created/Updated group {0}", sr.getNameInNamespace());
}
String userDN = sr.getNameInNamespace();
handleEntry(ctx, ObjectClass.GROUP, userDN, conf.getGroupSearchFilter(), handler, conf, attrsToGet);
final javax.naming.directory.Attribute member11 = sr.getAttributes().get("member;range=1-1");
final javax.naming.directory.Attribute member00 = sr.getAttributes().get("member;range=0-0");
ctx.setRequestControls(null);
if (member11 != null) {
if (LOG.isOk()) {
LOG.ok("Found entry 'IN' ...");
}
handleInOutEntries(
ctx,
ObjectClass.GROUP,
(NamingEnumeration) member11.getAll(),
"(&(objectclass=group)" + conf.getGroupSearchFilter() + ")",
handler,
conf,
attrsToGet);
}
if (member00 != null) {
// users to be removed
if (LOG.isOk()) {
LOG.ok("Found entry 'OUT' ...");
}
handleInOutEntries(
ctx,
ObjectClass.GROUP,
(NamingEnumeration) member00.getAll(),
"(&(objectclass=group)" + conf.getGroupSearchFilter() + ")",
handler,
conf,
attrsToGet);
}
}
} else {
if (LOG.isInfo()) {
LOG.info("Invalid object type {0}", objectClasses);
}
}
}
private SyncDelta getSyncDelta(
final ObjectClass oclass,
final String entryDN,
final SyncDeltaType syncDeltaType,
final Attributes profile,
final Collection attrsToGet)
throws NamingException {
final SyncDeltaBuilder sdb = new SyncDeltaBuilder();
// Set token
sdb.setToken(latestSyncToken);
// Set Delta Type
sdb.setDeltaType(syncDeltaType);
javax.naming.directory.Attribute uidAttribute;
Uid uid = null;
if (StringUtil.isNotBlank(conn.getConfiguration().getUidAttribute())) {
uidAttribute = profile.get(conn.getConfiguration().getUidAttribute());
if (uidAttribute != null) {
uid = new Uid(uidAttribute.get().toString());
}
}
if (uid == null) {
throw new ConnectorException("UID attribute not found");
}
// Set UID
sdb.setUid(uid);
// Set Connector Object
sdb.setObject(utils.createConnectorObject(entryDN, profile, attrsToGet, oclass));
return sdb.build();
}
private void handleInOutEntries(
final LdapContext ctx,
final ObjectClass oclass,
final NamingEnumeration dns,
final String filter,
final SyncResultsHandler handler,
final ADConfiguration conf,
final Collection attrsToGet)
throws NamingException {
while (dns.hasMoreElements()) {
// for each new user "in" we must verify custom ldap filter
handleEntry(ctx, oclass, dns.next(), filter, handler, conf, attrsToGet);
}
}
private void handleEntry(
final LdapContext ctx,
final ObjectClass oclass,
final String dn,
final String filter,
final SyncResultsHandler handler,
final ADConfiguration conf,
final Collection attrsToGet)
throws NamingException {
final Attributes profile = ctx.getAttributes(dn);
final Attribute objectClasses = profile.get("objectClass");
if (oclass.is(ObjectClass.ACCOUNT_NAME) && !objectClasses.contains("user")
|| oclass.is(ObjectClass.GROUP_NAME) && !objectClasses.contains("group")) {
LOG.info("Invalid type: skip object {0}", dn);
return;
}
final SyncDeltaType deltaType;
if (DirSyncUtils.verifyFilter(ctx, dn, filter)) {
if (LOG.isOk()) {
LOG.ok("Entry {0} - update", dn);
}
deltaType = SyncDeltaType.CREATE_OR_UPDATE;
} else {
// new memberOf could invalidate the custom filter
if (LOG.isOk()) {
LOG.ok("Entry {0} - delete", dn);
}
deltaType = SyncDeltaType.DELETE;
}
if (deltaType != SyncDeltaType.DELETE
|| (oclass.is(ObjectClass.GROUP_NAME) && conf.isRetrieveDeletedGroup())
|| (oclass.is(ObjectClass.ACCOUNT_NAME) && conf.isRetrieveDeletedUser())) {
handler.handle(getSyncDelta(
oclass,
dn,
deltaType,
profile,
attrsToGet));
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy