com.evolveum.polygon.connector.ldap.ad.AdLdapConnector.java Source code

Java tutorial

Introduction

Here is the source code for com.evolveum.polygon.connector.ldap.ad.AdLdapConnector.java

Source

/*
 * Copyright (c) 2015-2017 Evolveum
 *
 * 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 com.evolveum.polygon.connector.ldap.ad;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.net.ssl.HostnameVerifier;

import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.entry.Modification;
import org.apache.directory.api.ldap.model.entry.ModificationOperation;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.message.LdapResult;
import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.name.Rdn;
import org.apache.directory.api.ldap.model.schema.AttributeType;
import org.apache.directory.api.ldap.model.schema.LdapSyntax;
import org.apache.directory.api.ldap.model.schema.MatchingRule;
import org.apache.directory.api.ldap.model.schema.MutableAttributeType;
import org.apache.directory.api.ldap.model.schema.MutableMatchingRule;
import org.apache.directory.api.ldap.model.schema.ObjectClass;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.api.ldap.model.schema.SchemaObject;
import org.apache.directory.api.ldap.model.schema.normalizers.DeepTrimToLowerNormalizer;
import org.apache.directory.api.ldap.model.schema.registries.AttributeTypeRegistry;
import org.apache.directory.api.ldap.model.schema.registries.MatchingRuleRegistry;
import org.apache.directory.api.ldap.model.schema.registries.ObjectClassRegistry;
import org.apache.directory.api.ldap.model.schema.registries.Registries;
import org.apache.directory.api.ldap.model.schema.registries.SchemaObjectRegistry;
import org.apache.directory.api.ldap.model.schema.syntaxCheckers.DirectoryStringSyntaxChecker;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.http.client.config.AuthSchemes;
import org.identityconnectors.common.logging.Log;
import org.identityconnectors.common.security.GuardedString;
import org.identityconnectors.framework.common.exceptions.ConfigurationException;
import org.identityconnectors.framework.common.exceptions.ConnectionFailedException;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.framework.common.exceptions.ConnectorIOException;
import org.identityconnectors.framework.common.exceptions.ConnectorSecurityException;
import org.identityconnectors.framework.common.exceptions.UnknownUidException;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.AttributeBuilder;
import org.identityconnectors.framework.common.objects.OperationOptions;
import org.identityconnectors.framework.common.objects.OperationalAttributeInfos;
import org.identityconnectors.framework.common.objects.ResultsHandler;
import org.identityconnectors.framework.common.objects.ScriptContext;
import org.identityconnectors.framework.common.objects.Uid;
import org.identityconnectors.framework.spi.Configuration;
import org.identityconnectors.framework.spi.ConnectorClass;
import org.identityconnectors.framework.spi.operations.ScriptOnResourceOp;

import com.evolveum.polygon.common.GuardedStringAccessor;
import com.evolveum.polygon.common.SchemaUtil;
import com.evolveum.polygon.connector.ldap.AbstractLdapConfiguration;
import com.evolveum.polygon.connector.ldap.AbstractLdapConnector;
import com.evolveum.polygon.connector.ldap.LdapUtil;
import com.evolveum.polygon.connector.ldap.OperationLog;
import com.evolveum.polygon.connector.ldap.schema.LdapFilterTranslator;
import com.evolveum.polygon.connector.ldap.schema.AbstractSchemaTranslator;
import com.evolveum.polygon.connector.ldap.search.DefaultSearchStrategy;
import com.evolveum.polygon.connector.ldap.search.SearchStrategy;
import com.evolveum.powerhell.AbstractPowerHellImpl;
import com.evolveum.powerhell.AbstractPowerHellWinRmImpl;
import com.evolveum.powerhell.ArgumentStyle;
import com.evolveum.powerhell.PowerHell;
import com.evolveum.powerhell.PowerHellCommunicationException;
import com.evolveum.powerhell.PowerHellException;
import com.evolveum.powerhell.PowerHellExecutionException;
import com.evolveum.powerhell.PowerHellLocalExecImpl;
import com.evolveum.powerhell.PowerHellLocalExecPowerShellImpl;
import com.evolveum.powerhell.PowerHellLocalExecWinRsPowerShellImpl;
import com.evolveum.powerhell.PowerHellSecurityException;
import com.evolveum.powerhell.PowerHellWinRmExecImpl;
import com.evolveum.powerhell.PowerHellWinRmExecPowerShellImpl;
import com.evolveum.powerhell.PowerHellWinRmLoopImpl;

import io.cloudsoft.winrm4j.winrm.WinRmTool;
import io.cloudsoft.winrm4j.winrm.WinRmToolResponse;

import org.apache.cxf.Bus;
import org.apache.cxf.BusFactory;
import org.apache.cxf.transport.https.httpclient.DefaultHostnameVerifier;

@ConnectorClass(displayNameKey = "connector.ldap.ad.display", configurationClass = AdLdapConfiguration.class)
public class AdLdapConnector extends AbstractLdapConnector<AdLdapConfiguration> implements ScriptOnResourceOp {

    private static final Log LOG = Log.getLog(AdLdapConnector.class);

    private static final String PING_COMMAND = "hostname.exe";
    private static final String EXCHANGE_INIT_SCRIPT = "Add-PSSnapin *Exchange*";

    private GlobalCatalogConnectionManager globalCatalogConnectionManager;

    // SCRIPTING
    private String winRmUsername;
    private String winRmHost;
    private HostnameVerifier hostnameVerifier;
    private Map<String, PowerHell> powerHellMap = new HashMap<>(); // key: scripting language

    private boolean busInitialized = false;
    private boolean isWinRmInitialized;

    private static int busUsageCount = 0;

    @Override
    public void init(Configuration configuration) {
        super.init(configuration);
        globalCatalogConnectionManager = new GlobalCatalogConnectionManager(getConfiguration());
    }

    @Override
    public void dispose() {
        super.dispose();
        disposeScripting();
    }

    @Override
    protected void cleanupBeforeTest() {
        cleanupScriptingBeforeTest();
    }

    @Override
    protected void additionalConnectionTests() {
        if (isScriptingExplicitlyConfigured()) {
            pingScripting();
        }
    }

    @Override
    protected void reconnectAfterTest() {
    }

    @Override
    protected AbstractSchemaTranslator<AdLdapConfiguration> createSchemaTranslator() {
        return new AdSchemaTranslator(getSchemaManager(), getConfiguration());
    }

    @Override
    protected LdapFilterTranslator<AdLdapConfiguration> createLdapFilterTranslator(ObjectClass ldapObjectClass) {
        return new AdLdapFilterTranslator(getSchemaTranslator(), ldapObjectClass);
    }

    @Override
    protected AdSchemaTranslator getSchemaTranslator() {
        return (AdSchemaTranslator) super.getSchemaTranslator();
    }

    @Override
    protected boolean isLogSchemaErrors() {
        // There are too many built-in schema errors in AD that this only pollutes the logs
        return false;
    }

    @Override
    protected void preCreate(org.apache.directory.api.ldap.model.schema.ObjectClass ldapStructuralObjectClass,
            Entry entry) {
        super.preCreate(ldapStructuralObjectClass, entry);
        if (getSchemaTranslator().isUserObjectClass(ldapStructuralObjectClass.getName())
                && !getConfiguration().isRawUserAccountControlAttribute()) {
            if (entry.get(AdConstants.ATTRIBUTE_USER_ACCOUNT_CONTROL_NAME) == null) {
                try {
                    entry.add(AdConstants.ATTRIBUTE_USER_ACCOUNT_CONTROL_NAME,
                            Integer.toString(AdConstants.USER_ACCOUNT_CONTROL_NORMAL));
                } catch (LdapException e) {
                    throw new IllegalStateException("Error adding attribute "
                            + AdConstants.ATTRIBUTE_USER_ACCOUNT_CONTROL_NAME + " to entry");
                }
            }
        }
    }

    @Override
    protected void addAttributeModification(Dn dn, List<Modification> modifications,
            ObjectClass ldapStructuralObjectClass,
            org.identityconnectors.framework.common.objects.ObjectClass icfObjectClass, Attribute icfAttr,
            ModificationOperation modOp) {
        Rdn firstRdn = dn.getRdns().get(0);
        String firstRdnAttrName = firstRdn.getAva().getType();
        AttributeType modAttributeType = getSchemaTranslator().toLdapAttribute(ldapStructuralObjectClass,
                icfAttr.getName());
        if (firstRdnAttrName.equalsIgnoreCase(modAttributeType.getName())) {
            // Ignore this modification. It is already done by the rename operation.
            // Attempting to do it will result in an error.
            return;
        } else {
            super.addAttributeModification(dn, modifications, ldapStructuralObjectClass, icfObjectClass, icfAttr,
                    modOp);
        }
    }

    @Override
    protected SearchStrategy<AdLdapConfiguration> chooseSearchStrategy(
            org.identityconnectors.framework.common.objects.ObjectClass objectClass, ObjectClass ldapObjectClass,
            ResultsHandler handler, OperationOptions options) {
        SearchStrategy<AdLdapConfiguration> searchStrategy = super.chooseSearchStrategy(objectClass,
                ldapObjectClass, handler, options);
        searchStrategy.setAttributeHandler(new AdAttributeHandler(searchStrategy));
        return searchStrategy;
    }

    @Override
    protected SearchStrategy<AdLdapConfiguration> getDefaultSearchStrategy(
            org.identityconnectors.framework.common.objects.ObjectClass objectClass, ObjectClass ldapObjectClass,
            ResultsHandler handler, OperationOptions options) {
        SearchStrategy<AdLdapConfiguration> searchStrategy = super.getDefaultSearchStrategy(objectClass,
                ldapObjectClass, handler, options);
        searchStrategy.setAttributeHandler(new AdAttributeHandler(searchStrategy));
        return searchStrategy;

    }

    @Override
    protected SearchStrategy<AdLdapConfiguration> searchByUid(Uid uid,
            org.identityconnectors.framework.common.objects.ObjectClass objectClass, ObjectClass ldapObjectClass,
            final ResultsHandler handler, OperationOptions options) {
        final String uidValue = SchemaUtil.getSingleStringNonBlankValue(uid);

        // Trivial (but not really realistic) case: UID is DN

        if (LdapUtil.isDnAttribute(getConfiguration().getUidAttribute())) {

            return searchByDn(getSchemaTranslator().toDn(uidValue), objectClass, ldapObjectClass, handler, options);

        }

        if (uid.getNameHint() != null) {

            // First attempt: name hint, GUID search (last seen DN)

            // Name hint is the last DN that we have seen for this object. However, the object may have
            // been renamed or may have moved. Therefore use the name hint just to select the connection.
            // Once we have the connection then forget name hint and use GUID DN to get the entry.
            // This is the most efficient and still very reliable way to get the entry.

            Dn nameHintDn = getSchemaTranslator().toDn(uid.getNameHint());
            SearchStrategy<AdLdapConfiguration> searchStrategy = getDefaultSearchStrategy(objectClass,
                    ldapObjectClass, handler, options);
            LdapNetworkConnection connection = getConnectionManager().getConnection(nameHintDn);
            searchStrategy.setExplicitConnection(connection);

            Dn guidDn = getSchemaTranslator().getGuidDn(uidValue);
            String[] attributesToGet = getAttributesToGet(ldapObjectClass, options);
            try {
                searchStrategy.search(guidDn, null, SearchScope.OBJECT, attributesToGet);
            } catch (LdapException e) {
                throw LdapUtil.processLdapException("Error searching for DN '" + guidDn + "'", e);
            }

            if (searchStrategy.getNumberOfEntriesFound() > 0) {
                return searchStrategy;
            }
        }

        // Second attempt: global catalog

        if (AdLdapConfiguration.GLOBAL_CATALOG_STRATEGY_NONE
                .equals(getConfiguration().getGlobalCatalogStrategy())) {
            // Make search with <GUID=....> baseDn on default connection. Rely on referrals to point our head to
            // the correct domain controller in multi-domain environment.
            // We know that this can return at most one object. Therefore always use simple search.
            SearchStrategy<AdLdapConfiguration> searchStrategy = getDefaultSearchStrategy(objectClass,
                    ldapObjectClass, handler, options);
            String[] attributesToGet = getAttributesToGet(ldapObjectClass, options);
            Dn guidDn = getSchemaTranslator().getGuidDn(uidValue);
            try {
                searchStrategy.search(guidDn, LdapUtil.createAllSearchFilter(), SearchScope.OBJECT,
                        attributesToGet);
            } catch (LdapException e) {
                throw LdapUtil.processLdapException("Error searching for GUID '" + uidValue + "'", e);
            }

            if (searchStrategy.getNumberOfEntriesFound() > 0) {
                return searchStrategy;
            }

        } else if (AdLdapConfiguration.GLOBAL_CATALOG_STRATEGY_READ
                .equals(getConfiguration().getGlobalCatalogStrategy())) {
            // Make a search directly to the global catalog server. Present that as final result.
            // We know that this can return at most one object. Therefore always use simple search.
            SearchStrategy<AdLdapConfiguration> searchStrategy = new DefaultSearchStrategy<>(
                    globalCatalogConnectionManager, getConfiguration(), getSchemaTranslator(), objectClass,
                    ldapObjectClass, handler, options);
            String[] attributesToGet = getAttributesToGet(ldapObjectClass, options);
            Dn guidDn = getSchemaTranslator().getGuidDn(uidValue);
            try {
                searchStrategy.search(guidDn, LdapUtil.createAllSearchFilter(), SearchScope.OBJECT,
                        attributesToGet);
            } catch (LdapException e) {
                throw LdapUtil.processLdapException("Error searching for GUID '" + uidValue + "'", e);
            }

            if (searchStrategy.getNumberOfEntriesFound() > 0) {
                return searchStrategy;
            }

        } else if (AdLdapConfiguration.GLOBAL_CATALOG_STRATEGY_RESOLVE
                .equals(getConfiguration().getGlobalCatalogStrategy())) {
            Dn guidDn = getSchemaTranslator().getGuidDn(uidValue);
            Entry entry = searchSingleEntry(globalCatalogConnectionManager, guidDn,
                    LdapUtil.createAllSearchFilter(), SearchScope.OBJECT,
                    new String[] { AbstractLdapConfiguration.PSEUDO_ATTRIBUTE_DN_NAME },
                    "global catalog entry for GUID " + uidValue);
            if (entry == null) {
                throw new UnknownUidException("Entry for GUID " + uidValue + " was not found in global catalog");
            }
            LOG.ok("Resolved GUID {0} in glogbal catalog to DN {1}", uidValue, entry.getDn());
            Dn dn = entry.getDn();

            SearchStrategy<AdLdapConfiguration> searchStrategy = getDefaultSearchStrategy(objectClass,
                    ldapObjectClass, handler, options);
            // We need to force the use of explicit connection here. The search is still using the <GUID=..> dn
            // The search strategy cannot use that to select a connection. So we need to select a connection
            // based on the DN returned from global catalog explicitly.
            // We also cannot use the DN from the global catalog as the base DN for the search.
            // The global catalog may not be replicated yet and it may not have the correct DN
            // (e.g. the case of quick read after rename)
            LdapNetworkConnection connection = getConnectionManager().getConnection(dn);
            searchStrategy.setExplicitConnection(connection);

            String[] attributesToGet = getAttributesToGet(ldapObjectClass, options);
            try {
                searchStrategy.search(guidDn, null, SearchScope.OBJECT, attributesToGet);
            } catch (LdapException e) {
                throw LdapUtil.processLdapException("Error searching for DN '" + guidDn + "'", e);
            }

            if (searchStrategy.getNumberOfEntriesFound() > 0) {
                return searchStrategy;
            }

        } else {
            throw new IllegalStateException(
                    "Unknown global catalog strategy '" + getConfiguration().getGlobalCatalogStrategy() + "'");
        }

        // Third attempt: brutal search over all the servers

        if (getConfiguration().isAllowBruteForceSearch()) {
            LOG.ok("Cannot find object with GUID {0} by using name hint or global catalog. Resorting to brute-force search",
                    uidValue);
            Dn guidDn = getSchemaTranslator().getGuidDn(uidValue);
            String[] attributesToGet = getAttributesToGet(ldapObjectClass, options);
            for (LdapNetworkConnection connection : getConnectionManager().getAllConnections()) {
                SearchStrategy<AdLdapConfiguration> searchStrategy = getDefaultSearchStrategy(objectClass,
                        ldapObjectClass, handler, options);
                searchStrategy.setExplicitConnection(connection);

                try {
                    searchStrategy.search(guidDn, null, SearchScope.OBJECT, attributesToGet);
                } catch (LdapException e) {
                    throw LdapUtil.processLdapException("Error searching for DN '" + guidDn + "'", e);
                }

                if (searchStrategy.getNumberOfEntriesFound() > 0) {
                    return searchStrategy;
                }
            }

        } else {
            LOG.ok("Cannot find object with GUID {0} by using name hint or global catalog. Brute-force search is disabled. Found nothing.",
                    uidValue);
        }

        // Found nothing
        return null;

    }

    @Override
    protected Dn resolveDn(org.identityconnectors.framework.common.objects.ObjectClass objectClass, Uid uid,
            OperationOptions options) {

        String guid = uid.getUidValue();

        if (uid.getNameHint() != null) {
            // Try to use name hint to select the correct server, but still search by GUID. The entry might
            // have been renamed since we looked last time and the name hint may be out of date. But it is
            // likely that it is still OK for selecting correct server.
            // Global catalog updates are quite lazy. Looking at global catalog can get even worse results
            // than name hint.

            String dnHintString = uid.getNameHintValue();
            Dn dnHint = getSchemaTranslator().toDn(dnHintString);
            LOG.ok("Resolvig DN by using name hint {0} and guid", dnHint, guid);

            Dn guidDn = getSchemaTranslator().getGuidDn(guid);

            LOG.ok("Resolvig DN by search for {0} (no global catalog)", guidDn);
            Entry entry = searchSingleEntry(getConnectionManager(), guidDn, LdapUtil.createAllSearchFilter(),
                    SearchScope.OBJECT, new String[] { AbstractLdapConfiguration.PSEUDO_ATTRIBUTE_DN_NAME },
                    "LDAP entry for GUID " + guid, dnHint);

            if (entry != null) {
                return entry.getDn();
            } else {
                LOG.ok("Resolvig DN for name hint {0} returned no object", dnHintString);
            }
        }

        Dn guidDn = getSchemaTranslator().getGuidDn(guid);

        if (AdLdapConfiguration.GLOBAL_CATALOG_STRATEGY_NONE
                .equals(getConfiguration().getGlobalCatalogStrategy())) {
            LOG.ok("Resolvig DN by search for {0} (no global catalog)", guidDn);
            Entry entry = searchSingleEntry(getConnectionManager(), guidDn, LdapUtil.createAllSearchFilter(),
                    SearchScope.OBJECT, new String[] { AbstractLdapConfiguration.PSEUDO_ATTRIBUTE_DN_NAME },
                    "LDAP entry for GUID " + guid);
            if (entry == null) {
                throw new UnknownUidException("Entry for GUID " + guid + " was not found");
            }
            return entry.getDn();

        } else {
            LOG.ok("Resolvig DN by search for {0} (global catalog)", guidDn);
            Entry entry = searchSingleEntry(globalCatalogConnectionManager, guidDn,
                    LdapUtil.createAllSearchFilter(), SearchScope.OBJECT,
                    new String[] { AbstractLdapConfiguration.PSEUDO_ATTRIBUTE_DN_NAME },
                    "LDAP entry for GUID " + guid);
            if (entry == null) {
                throw new UnknownUidException("Entry for GUID " + guid + " was not found in global catalog");
            }
            LOG.ok("Resolved GUID {0} in glogbal catalog to DN {1}", guid, entry.getDn());
            return entry.getDn();
        }
    }

    @Override
    protected void postUpdate(org.identityconnectors.framework.common.objects.ObjectClass icfObjectClass, Uid uid,
            Set<Attribute> values, OperationOptions options, ModificationOperation modOp, Dn dn,
            org.apache.directory.api.ldap.model.schema.ObjectClass ldapStructuralObjectClass,
            List<Modification> modifications) {
        super.postUpdate(icfObjectClass, uid, values, options, modOp, dn, ldapStructuralObjectClass, modifications);

        if (getConfiguration().isForcePasswordChangeAtNextLogon()) {

            //if password is in modifications set pwdLastSet=0 ("must change password at next logon")
            if (getSchemaTranslator().isUserObjectClass(ldapStructuralObjectClass.getName())) {
                for (Attribute icfAttr : values) {

                    // coming from midpoint password is __PASSWORD__
                    // TODO: should we additionally ask for  icfAttr.getName().equals(getConfiguration().getPasswordAttribute()?
                    if (OperationalAttributeInfos.PASSWORD.is(icfAttr.getName())) {

                        List<Modification> modificationsPwdLastSet = new ArrayList<Modification>();
                        Attribute attrPwdLastSet = AttributeBuilder.build(AdConstants.ATTRIBUTE_PWD_LAST_SET_NAME,
                                "0");
                        addAttributeModification(dn, modificationsPwdLastSet, ldapStructuralObjectClass,
                                icfObjectClass, attrPwdLastSet, ModificationOperation.REPLACE_ATTRIBUTE);
                        modify(dn, modificationsPwdLastSet);
                        break;
                    }
                }

            }
        }
    }

    @Override
    protected void patchSchemaManager(SchemaManager schemaManager) {
        super.patchSchemaManager(schemaManager);
        if (!getConfiguration().isTweakSchema()) {
            return;
        }

        Registries registries = schemaManager.getRegistries();
        MatchingRuleRegistry matchingRuleRegistry = registries.getMatchingRuleRegistry();

        MatchingRule mrCaseIgnoreMatch = matchingRuleRegistry.get(SchemaConstants.CASE_IGNORE_MATCH_MR_OID);
        // Microsoft ignores matching rules. Completely. There is not even a single definition.
        if (mrCaseIgnoreMatch == null) {
            MutableMatchingRule correctMrCaseIgnoreMatch = new MutableMatchingRule(
                    SchemaConstants.CASE_IGNORE_MATCH_MR_OID);
            correctMrCaseIgnoreMatch.setSyntaxOid(SchemaConstants.DIRECTORY_STRING_SYNTAX);
            correctMrCaseIgnoreMatch
                    .setNormalizer(new DeepTrimToLowerNormalizer(SchemaConstants.CASE_IGNORE_MATCH_MR_OID));
            mrCaseIgnoreMatch = correctMrCaseIgnoreMatch;
            register(matchingRuleRegistry, correctMrCaseIgnoreMatch);
        }

        // Microsoft violates RFC4519
        fixAttribute(schemaManager, SchemaConstants.CN_AT_OID, SchemaConstants.CN_AT,
                createStringSyntax(SchemaConstants.DIRECTORY_STRING_SYNTAX), mrCaseIgnoreMatch);
        fixAttribute(schemaManager, SchemaConstants.DOMAIN_COMPONENT_AT_OID, SchemaConstants.DC_AT,
                createStringSyntax(SchemaConstants.DIRECTORY_STRING_SYNTAX), mrCaseIgnoreMatch);
        fixAttribute(schemaManager, SchemaConstants.OU_AT_OID, SchemaConstants.OU_AT,
                createStringSyntax(SchemaConstants.DIRECTORY_STRING_SYNTAX), mrCaseIgnoreMatch);
    }

    private LdapSyntax createStringSyntax(String syntaxOid) {
        LdapSyntax syntax = new LdapSyntax(syntaxOid);
        syntax.setHumanReadable(true);
        syntax.setSyntaxChecker(DirectoryStringSyntaxChecker.INSTANCE);
        return syntax;
    }

    private void fixAttribute(SchemaManager schemaManager, String attrOid, String attrName, LdapSyntax syntax,
            MatchingRule equalityMr) {
        Registries registries = schemaManager.getRegistries();
        AttributeTypeRegistry attributeTypeRegistry = registries.getAttributeTypeRegistry();
        ObjectClassRegistry objectClassRegistry = registries.getObjectClassRegistry();

        AttributeType attrDcType = attributeTypeRegistry.get(attrOid);
        if (attrDcType == null || attrDcType.getEquality() == null) {
            MutableAttributeType correctAttrDcType;
            if (attrDcType != null) {
                try {
                    attributeTypeRegistry.unregister(attrDcType);
                } catch (LdapException e) {
                    throw new IllegalStateException("Error unregistering " + attrDcType + ": " + e.getMessage(), e);
                }
                correctAttrDcType = new MutableAttributeType(attrDcType.getOid());
                correctAttrDcType.setNames(attrDcType.getNames());
            } else {
                correctAttrDcType = new MutableAttributeType(attrOid);
                correctAttrDcType.setNames(attrName);
            }

            correctAttrDcType.setSyntax(syntax);
            correctAttrDcType.setEquality(equalityMr);
            correctAttrDcType.setSingleValued(true);
            LOG.ok("Registering replacement attributeType: {0}", correctAttrDcType);
            register(attributeTypeRegistry, correctAttrDcType);
            fixObjectClasses(objectClassRegistry, attrDcType, correctAttrDcType);
        }

    }

    private void fixObjectClasses(ObjectClassRegistry objectClassRegistry, AttributeType oldAttributeType,
            AttributeType newAttributeType) {
        for (ObjectClass objectClass : objectClassRegistry) {
            fixOblectClassAttributes(objectClass.getMayAttributeTypes(), oldAttributeType, newAttributeType);
            fixOblectClassAttributes(objectClass.getMustAttributeTypes(), oldAttributeType, newAttributeType);
        }

    }

    private void fixOblectClassAttributes(List<AttributeType> attributeTypes, AttributeType oldAttributeType,
            AttributeType newAttributeType) {
        for (int i = 0; i < attributeTypes.size(); i++) {
            AttributeType current = attributeTypes.get(i);
            if (current.equals(oldAttributeType)) {
                attributeTypes.set(i, newAttributeType);
                break;
            }
        }
    }

    private <T extends SchemaObject> void register(SchemaObjectRegistry<T> registry, T object) {
        try {
            registry.register(object);
        } catch (LdapException e) {
            throw new IllegalStateException("Error registering " + object + ": " + e.getMessage(), e);
        }
    }

    protected RuntimeException processLdapResult(String message, LdapResult ldapResult) {
        if (ldapResult.getResultCode() == ResultCodeEnum.UNWILLING_TO_PERFORM) {
            WillNotPerform willNotPerform = WillNotPerform
                    .parseDiagnosticMessage(ldapResult.getDiagnosticMessage());
            if (willNotPerform == null) {
                return LdapUtil.processLdapResult(message, ldapResult);
            }
            try {
                Class<? extends RuntimeException> exceptionClass = willNotPerform.getExceptionClass();
                Constructor<? extends RuntimeException> exceptionConstructor;
                exceptionConstructor = exceptionClass.getConstructor(String.class);
                String exceptionMessage = LdapUtil.sanitizeString(ldapResult.getDiagnosticMessage()) + ": "
                        + willNotPerform.name() + ": " + willNotPerform.getMessage();
                RuntimeException exception = exceptionConstructor.newInstance(exceptionMessage);
                LdapUtil.logOperationError(message, ldapResult, exceptionMessage);
                throw exception;
            } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException
                    | IllegalArgumentException | InvocationTargetException e) {
                LOG.error("Error during LDAP error handling: {0}: {1}", e.getClass(), e.getMessage(), e);
                // fallback
                return LdapUtil.processLdapResult(message, ldapResult);
            }

        } else {
            return LdapUtil.processLdapResult(message, ldapResult);
        }
    }

    // SCRIPTING
    // All of this will eventually go to a separate connector

    @Override
    public Object runScriptOnResource(ScriptContext scriptCtx, OperationOptions options) {
        String scriptLanguage = scriptCtx.getScriptLanguage();
        PowerHell powerHell = getPowerHell(scriptLanguage);

        String command = scriptCtx.getScriptText();
        OperationLog.log("{0} Script REQ {1}: {2}", winRmHost, scriptLanguage, command);
        LOG.ok("Executing {0} script on {0} as {1} using {2}: {3}", scriptLanguage, winRmHost, winRmUsername,
                powerHell.getImplementationName(), command);

        String output;
        try {

            output = powerHell.runCommand(command, scriptCtx.getScriptArguments());

        } catch (PowerHellException e) {
            OperationLog.error("{0} Script ERR {1}", winRmHost, e.getMessage());
            throw new ConnectorException("Script execution failed: " + e.getMessage(), e);
        }

        OperationLog.log("{0} Script RES {1}", winRmHost,
                (output == null || output.isEmpty()) ? "no output" : ("output " + output.length() + " chars"));
        LOG.ok("Script returned output\n{0}", output);

        return output;
    }

    private PowerHell getPowerHell(String scriptLanguage) {
        if (scriptLanguage == null) {
            throw new IllegalArgumentException("Script language not specified");
        }
        PowerHell powerHell = powerHellMap.get(scriptLanguage);
        if (powerHell == null) {
            powerHell = createPowerHell(scriptLanguage);
            try {
                powerHell.connect();
            } catch (PowerHellExecutionException e) {
                throw new ConnectorException(
                        "Cannot connect PowerHell " + powerHell.getImplementationName() + ": " + e.getMessage(), e);
            } catch (PowerHellSecurityException e) {
                throw new ConnectorSecurityException(
                        "Cannot connect PowerHell " + powerHell.getImplementationName() + ": " + e.getMessage(), e);
            } catch (PowerHellCommunicationException e) {
                throw new ConnectorIOException(
                        "Cannot connect PowerHell " + powerHell.getImplementationName() + ": " + e.getMessage(), e);
            }
            powerHellMap.put(scriptLanguage, powerHell);
        }
        return powerHell;
    }

    private PowerHell createPowerHell(String scriptLanguage) {
        if (!isWinRmInitialized) {
            initWinRm();
        }
        PowerHell powerHell;
        switch (scriptLanguage) {
        case AdLdapConfiguration.SCRIPT_LANGUAGE_CMD:
            powerHell = createCmdPowerHell();
            break;
        case AdLdapConfiguration.SCRIPT_LANGUAGE_POWERSHELL:
            powerHell = createPowershellPowerHell();
            break;
        case AdLdapConfiguration.SCRIPT_LANGUAGE_EXCHANGE:
            powerHell = createLoopPowerHell(EXCHANGE_INIT_SCRIPT);
            break;
        case AdLdapConfiguration.SCRIPT_LANGUAGE_POWERHELL:
            powerHell = createLoopPowerHell(null);
            break;
        default:
            throw new IllegalArgumentException("Unknown script language " + scriptLanguage);
        }
        LOG.ok("Initialized PowerHell {0} ({1}) for language {2}", powerHell.getImplementationName(),
                powerHell.getClass().getSimpleName(), scriptLanguage);
        return powerHell;
    }

    private PowerHell createCmdPowerHell() {
        if (isScriptingWinRm()) {
            PowerHellWinRmExecImpl powerHell = new PowerHellWinRmExecImpl();
            setWinRmParameters(powerHell);
            return powerHell;
        } else if (isScriptingLocal()) {
            PowerHellLocalExecImpl powerHell = new PowerHellLocalExecImpl();
            setLocalParameters(powerHell);
            return powerHell;
        } else {
            throw new IllegalArgumentException(
                    "Unknown scripting execution mechanism " + getConfiguration().getScriptExecutionMechanism());
        }
    }

    private PowerHell createPowershellPowerHell() {
        if (isScriptingWinRm()) {
            PowerHellWinRmExecPowerShellImpl powerHell = new PowerHellWinRmExecPowerShellImpl();
            setWinRmParameters(powerHell);
            return powerHell;
        } else if (isScriptingLocal()) {
            PowerHellLocalExecPowerShellImpl powerHell = new PowerHellLocalExecPowerShellImpl();
            setLocalParameters(powerHell);
            return powerHell;
        } else {
            throw new IllegalArgumentException(
                    "Unknown scripting execution mechanism " + getConfiguration().getScriptExecutionMechanism());
        }
    }

    private PowerHell createLoopPowerHell(String initSctip) {
        if (isScriptingWinRm()) {
            PowerHellWinRmLoopImpl powerHell = new PowerHellWinRmLoopImpl();
            setWinRmParameters(powerHell);
            return powerHell;
        } else if (isScriptingLocal()) {
            throw new UnsupportedOperationException(
                    "PowerHell loop is not supported for local script execution mechanism");
        } else {
            throw new IllegalArgumentException(
                    "Unknown scripting execution mechanism " + getConfiguration().getScriptExecutionMechanism());
        }
    }

    private boolean isScriptingWinRm() {
        return getConfiguration().getScriptExecutionMechanism() == null
                || AdLdapConfiguration.SCRIPT_EXECUTION_MECHANISM_WINRM
                        .equals(getConfiguration().getScriptExecutionMechanism());
    }

    private boolean isScriptingLocal() {
        return AdLdapConfiguration.SCRIPT_EXECUTION_MECHANISM_LOCAL
                .equals(getConfiguration().getScriptExecutionMechanism());
    }

    private void setWinRmParameters(AbstractPowerHellWinRmImpl powerHell) {
        setCommonParameters(powerHell);
        String winRmDomain = getConfiguration().getWinRmDomain();
        powerHell.setDomainName(winRmDomain);
        powerHell.setEndpointUrl(getWinRmEndpointUrl());
        powerHell.setUserName(winRmUsername);
        powerHell.setPassword(getWinRmPassword());
        powerHell.setAuthenticationScheme(getAuthenticationScheme());
        powerHell.setHostnameVerifier(hostnameVerifier);
    }

    private void setLocalParameters(PowerHellLocalExecImpl powerHell) {
        setCommonParameters(powerHell);
    }

    private void setCommonParameters(AbstractPowerHellImpl powerHell) {
        powerHell.setArgumentStyle(getArgumentStyle());
    }

    private ArgumentStyle getArgumentStyle() {
        if (getConfiguration().getPowershellArgumentStyle() == null) {
            return ArgumentStyle.PARAMETERS_DASH;
        }
        switch (getConfiguration().getPowershellArgumentStyle()) {
        case AdLdapConfiguration.ARGUMENT_STYLE_DASHED:
            return ArgumentStyle.PARAMETERS_DASH;
        case AdLdapConfiguration.ARGUMENT_STYLE_SLASHED:
            return ArgumentStyle.PARAMETERS_SLASH;
        case AdLdapConfiguration.ARGUMENT_STYLE_VARIABLES:
            return ArgumentStyle.VARIABLES;
        default:
            throw new IllegalArgumentException(
                    "Unknown argument style " + getConfiguration().getPowershellArgumentStyle());
        }
    }

    private String getAuthenticationScheme() {
        if (getConfiguration().getWinRmAuthenticationScheme() == null) {
            return AuthSchemes.NTLM;
        }
        if (AdLdapConfiguration.WINDOWS_AUTHENTICATION_SCHEME_BASIC
                .equals(getConfiguration().getWinRmAuthenticationScheme())) {
            return AuthSchemes.BASIC;
        }
        if (AdLdapConfiguration.WINDOWS_AUTHENTICATION_SCHEME_NTLM
                .equals(getConfiguration().getWinRmAuthenticationScheme())) {
            return AuthSchemes.NTLM;
        }
        if (AdLdapConfiguration.WINDOWS_AUTHENTICATION_SCHEME_CREDSSP
                .equals(getConfiguration().getWinRmAuthenticationScheme())) {
            return AuthSchemes.CREDSSP;
        }
        throw new ConfigurationException(
                "Unknown authentication scheme: " + getConfiguration().getWinRmAuthenticationScheme());
    }

    private void initWinRm() {
        if (!busInitialized) {
            initBus();
            busInitialized = true;
        }
        winRmUsername = getWinRmUsername();
        winRmHost = getWinRmHost();
        hostnameVerifier = new DefaultHostnameVerifier(null);
        isWinRmInitialized = true;
    }

    private boolean isScriptingExplicitlyConfigured() {
        if (getConfiguration().getScriptExecutionMechanism() != null) {
            return true;
        }
        if (getConfiguration().getWinRmUsername() != null) {
            return true;
        }
        if (getConfiguration().getWinRmPassword() != null) {
            return true;
        }
        if (getConfiguration().getWinRmDomain() != null) {
            return true;
        }
        if (getConfiguration().getWinRmAuthenticationScheme() != null) {
            return true;
        }
        return false;
    }

    private void pingScripting() {
        String command = PING_COMMAND;
        PowerHell powerHell = getPowerHell(AdLdapConfiguration.SCRIPT_LANGUAGE_CMD);

        OperationLog.log("{0} Script REQ ping cmd: {1}", winRmHost, command);
        LOG.ok("Executing ping cmd script on {0} as {1}: {2}", winRmHost, winRmUsername, command);

        try {

            String output = powerHell.runCommand(PING_COMMAND, null);

            OperationLog.log("{0} Script RES ping: {1}", winRmHost, output);

        } catch (PowerHellExecutionException e) {
            OperationLog.error("{0} Script ERR ping status={1}: {2}", winRmHost, e.getExitCode(), e.getMessage());
            LOG.error("Script ping error, exit status = {0}\nOUT:\n{1}\nERR:\n{2}", e.getExitCode(), e.getStdout(),
                    e.getStderr());
            throw new ConnectorException(
                    "Ping script execution failed (status code " + e.getExitCode() + "): " + e.getMessage(), e);
        } catch (PowerHellSecurityException | PowerHellCommunicationException e) {
            OperationLog.error("{0} Script ERR ping: {2}", winRmHost, e.getMessage());
            throw new ConnectorException("Ping script execution failed: " + e.getMessage(), e);
        }
    }

    private void cleanupScriptingBeforeTest() {
        for (Map.Entry<String, PowerHell> entry : powerHellMap.entrySet()) {
            entry.getValue().disconnect();
        }
        powerHellMap.clear();
        winRmUsername = null;
        winRmHost = null;
        hostnameVerifier = null;
        isWinRmInitialized = false;
    }

    private void disposeScripting() {
        for (Map.Entry<String, PowerHell> entry : powerHellMap.entrySet()) {
            entry.getValue().disconnect();
        }
        if (busInitialized) {
            disposeBus();
            busInitialized = false;
        }
    }

    /*
     * Init and dispose methods for the CXF bus. These are based on static usage
     * counter and static default bus. Which means that the bus will be reused by
     * all the connector instances (even those that have different configuration).
     * But as  WinRmTool tool creates new WinRmClient for each invocation which 
     * in turn creates a new CXF service then this approach should be safe.
     * This is the best that we can do as ConnId does not provide any
     * connector context that we could use to store per-resource bus instance.
     */
    private static synchronized void initBus() {
        busUsageCount++;
        LOG.ok("bus init (usage count = {0})", busUsageCount);
        // make sure that the bus is created here while we are synchronized
        BusFactory.getDefaultBus(true);
    }

    private static synchronized void disposeBus() {
        busUsageCount--;
        LOG.ok("bus dispose (usage count = {0})", busUsageCount);
        if (busUsageCount == 0) {
            Bus bus = BusFactory.getDefaultBus(false);
            if (bus != null) {
                LOG.ok("Shutting down WinRm CXF bus {0}", bus);
                bus.shutdown(true);
                LOG.ok("Bus shut down");
            }
        }
    }

    private String getWinRmHost() {
        if (getConfiguration().getWinRmHost() != null) {
            return getConfiguration().getWinRmHost();
        }
        return getConfiguration().getHost();
    }

    private String getWinRmUsername() {
        if (getConfiguration().getWinRmUsername() != null) {
            return getConfiguration().getWinRmUsername();
        }
        return getConfiguration().getBindDn();
    }

    private String getWinRmPassword() {
        GuardedString winRmPassword = getConfiguration().getWinRmPassword();
        if (winRmPassword == null) {
            winRmPassword = getConfiguration().getBindPassword();
        }
        if (winRmPassword == null) {
            return null;
        }
        GuardedStringAccessor accessor = new GuardedStringAccessor();
        winRmPassword.access(accessor);
        return new String(accessor.getClearChars());
    }

    private String getWinRmEndpointUrl() {
        StringBuilder sb = new StringBuilder();
        if (getConfiguration().isWinRmUseHttps()) {
            sb.append("https://");
        } else {
            sb.append("http://");
        }
        sb.append(winRmHost).append(":").append(getConfiguration().getWinRmPort());
        sb.append("/wsman");
        return sb.toString();
    }

}