mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Yubao Liu
24.10.2022 ba0527f3c3b1c639944d262951d59a21bbf59aa9
support AD attributes userAccountControl, msDS-UserAccountDisabled and pwdLastSet (#233)

It's a pity LDAP doesn't have standard attribute to represent disabling
an user account, Redhat's Keycloak supports an AD mapper to read and write
attribute `userAccountControl`, and an AD LDS mapper to read and write
attribute `msDS-UserAccountDisabled`, both mappers support attribute
`pwdLastSet` too.

With this patch, these three attributes basically work like AD and AD LDS:

* AD: if (userAccountControl & 2L) != 0, then the user account is disabled for binding.
* AD LDS: if msDS-UserAccountDisabled is TRUE, then the user account is disabled for binding.
* Both AD and AD LDS:
* new user added: if pwdLastSet != 0, it's automatically set to current time.
* user password modified: if new pwdLastSet != 0, it's automatically set to current time.
* pwdLastSet changed: if new pwdLastSet != 0, it's automatically set to current time.
* pwdLastSet deleted: pwdLastSet is automatically set to current time.

References:
* https://docs.microsoft.com/en-us/windows/win32/adschema/a-useraccountcontrol
* https://docs.microsoft.com/en-us/windows/win32/adschema/a-msds-useraccountdisabled
* https://docs.microsoft.com/en-us/windows/win32/adschema/a-pwdlastset
10 files added
1 files modified
417 ■■■■■ changed files
opendj-server-legacy/resource/schema/99-msad.ldif 11 ●●●●● patch | view | raw | blame | history
opendj-server-msad-plugin/README.msad.plugin 40 ●●●●● patch | view | raw | blame | history
opendj-server-msad-plugin/pom.xml 69 ●●●●● patch | view | raw | blame | history
opendj-server-msad-plugin/src/main/assembly/config/msad-plugin.ldif 10 ●●●●● patch | view | raw | blame | history
opendj-server-msad-plugin/src/main/assembly/config/schema/99-msad-plugin.ldif 7 ●●●●● patch | view | raw | blame | history
opendj-server-msad-plugin/src/main/assembly/descriptor.xml 30 ●●●●● patch | view | raw | blame | history
opendj-server-msad-plugin/src/main/java/opendj/MsadPlugin.java 221 ●●●●● patch | view | raw | blame | history
opendj-server-msad-plugin/src/main/java/opendj/MsadPluginConfiguration.xml 21 ●●●●● patch | view | raw | blame | history
opendj-server-msad-plugin/src/main/java/opendj/Package.xml 6 ●●●●● patch | view | raw | blame | history
opendj-server-msad-plugin/src/main/java/opendj/package-info.java 1 ●●●● patch | view | raw | blame | history
pom.xml 1 ●●●● patch | view | raw | blame | history
opendj-server-legacy/resource/schema/99-msad.ldif
New file
@@ -0,0 +1,11 @@
dn: cn=schema
objectClass: top
objectClass: ldapSubentry
objectClass: subschema
cn: schema
attributeTypes: ( 1.2.840.113556.1.4.1853 NAME ( 'msDS-UserAccountDisabled' 'ms-DS-User-Account-Disabled' ) DESC 'https://docs.microsoft.com/en-us/windows/win32/adschema/a-msds-useraccountdisabled' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE USAGE userApplications X-SCHEMA-FILE '99-msad.ldif' X-ORIGIN 'msad' )
attributeTypes: ( 1.2.840.113556.1.4.96 NAME ( 'pwdLastSet' 'Pwd-Last-Set' ) DESC 'https://docs.microsoft.com/en-us/windows/win32/adschema/a-pwdlastset' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE USAGE userApplications X-SCHEMA-FILE '99-msad.ldif' X-ORIGIN 'msad' )
attributeTypes: ( 1.2.840.113556.1.4.8 NAME ( 'userAccountControl' 'User-Account-Control' ) DESC 'https://docs.microsoft.com/en-us/windows/win32/adschema/a-useraccountcontrol' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE USAGE userApplications X-SCHEMA-FILE '99-msad.ldif' X-ORIGIN 'msad' )
objectClasses: ( 1.2.840.113556.1.5.244 NAME ( 'msDS-BindableObject' 'ms-DS-Bindable-Object' ) DESC 'https://docs.microsoft.com/en-us/windows/win32/adschema/c-msds-bindableobject' SUP top AUXILIARY MAY ( msDS-UserAccountDisabled $ pwdLastSet ) X-SCHEMA-FILE '99-msad.ldif' X-ORIGIN 'msad' )
objectClasses: ( 1.2.840.113556.1.5.9 NAME 'user' DESC 'https://docs.microsoft.com/en-us/windows/win32/adschema/c-user' SUP inetOrgPerson STRUCTURAL MAY ( userAccountControl $ pwdLastSet ) X-SCHEMA-FILE '99-msad.ldif' X-ORIGIN 'msad' )
opendj-server-msad-plugin/README.msad.plugin
New file
@@ -0,0 +1,40 @@
In order to build and use this plugin, perform the following steps while the server is stopped:
# 1. ensure OpenDJ is stopped:
     bin/stop-ds
# 2. Go into the msad-plugin source folder:
     cd opendj-server-msad-plugin
# 3. Build the plugin (this requires Maven version 3):
     mvn clean install
# 4. Unzip the built msad-plugin zip
     unzip target/opendj-server-msad-plugin.zip -d target
# 5. Copy the msad-plugin's content into the parent OpenDJ installation:
     cp -r target/opendj-server-msad-plugin/* ..
# 6. This will copy the following files:
#     lib/extensions/opendj-server-msad-plugin.jar
#     config/msad-plugin.ldif
#     config/schema/99-msad-plugin.ldif
# 7. Add the plugin's config to the server configuration.
     cd ..
     bin/start-ds
     bin/dsconfig -h localhost -p 4444 -D "cn=Directory Manager" -w password \
      create-plugin --plugin-name "MSAD Plugin" --type msad \
      --set enabled:true --set plugin-type:preoperationbind \
      --set plugin-type:preoperationadd --set plugin-type:preoperationmodify -X -n
# 8. Restart the server:
     bin/stop-ds --restart
opendj-server-msad-plugin/pom.xml
New file
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.openidentityplatform.opendj</groupId>
        <artifactId>opendj-parent</artifactId>
        <version>4.4.15-SNAPSHOT</version>
    </parent>
    <artifactId>opendj-server-msad-plugin</artifactId>
    <name>OpenDJ Server Microsoft Active Directory Plugin</name>
    <description>
        An OpenDJ server plugin to handle AD specific attributes "userAccountControl", "msDS-UserAccountDisabled" and "pwdLastSet".
    </description>
    <packaging>jar</packaging>
    <dependencies>
        <dependency>
            <groupId>org.openidentityplatform.opendj</groupId>
            <artifactId>opendj-server-legacy</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>
    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.openidentityplatform.opendj</groupId>
                <artifactId>opendj-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>generate-config</id>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>generate-config</goal>
                        </goals>
                        <configuration>
                            <packageName>opendj</packageName>
                            <isExtension>true</isExtension>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                        <configuration>
                            <appendAssemblyId>false</appendAssemblyId>
                            <descriptors>
                                <descriptor>src/main/assembly/descriptor.xml</descriptor>
                            </descriptors>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
opendj-server-msad-plugin/src/main/assembly/config/msad-plugin.ldif
New file
@@ -0,0 +1,10 @@
dn: cn=MSAD Plugin,cn=Plugins,cn=config
objectClass: top
objectClass: ds-cfg-plugin
objectClass: ds-cfg-msad-plugin
cn: MSAD Plugin
ds-cfg-enabled: true
ds-cfg-java-class: opendj.MsadPlugin
ds-cfg-plugin-type: preoperationbind
ds-cfg-plugin-type: preoperationadd
ds-cfg-plugin-type: preoperationmodify
opendj-server-msad-plugin/src/main/assembly/config/schema/99-msad-plugin.ldif
New file
@@ -0,0 +1,7 @@
dn: cn=schema
objectClass: top
objectClass: ldapSubentry
objectClass: subschema
objectClasses: ( ds-cfg-msad-plugin-oid NAME 'ds-cfg-msad-plugin'
  SUP ds-cfg-plugin STRUCTURAL
  X-ORIGIN 'msad' )
opendj-server-msad-plugin/src/main/assembly/descriptor.xml
New file
@@ -0,0 +1,30 @@
<?xml version="1.0"?>
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2
                      http://maven.apache.org/xsd/assembly-1.1.2.xsd">
    <id>opendj-server-msad-plugin</id>
    <formats>
        <format>zip</format>
    </formats>
    <fileSets>
        <fileSet>
            <directory>src/main/assembly/config</directory>
            <outputDirectory>config</outputDirectory>
            <directoryMode>0755</directoryMode>
            <fileMode>0644</fileMode>
            <lineEnding>unix</lineEnding>
        </fileSet>
        <fileSet>
            <directory>${project.build.directory}</directory>
            <outputDirectory>lib/extensions</outputDirectory>
            <includes>
                <include>*.jar</include>
            </includes>
            <excludes>
                <exclude>*-javadoc.jar</exclude>
                <exclude>*-sources.jar</exclude>
            </excludes>
        </fileSet>
    </fileSets>
</assembly>
opendj-server-msad-plugin/src/main/java/opendj/MsadPlugin.java
New file
@@ -0,0 +1,221 @@
package opendj;
import java.util.List;
import java.util.Set;
import opendj.server.MsadPluginCfg;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.config.server.ConfigurationChangeListener;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.ModificationType;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.schema.AttributeType;
import org.forgerock.opendj.ldap.schema.CoreSchema;
import org.forgerock.opendj.ldap.schema.ObjectClass;
import org.forgerock.opendj.ldap.schema.Schema;
import org.opends.messages.CoreMessages;
import org.opends.messages.PluginMessages;
import org.opends.server.api.AuthenticationPolicy;
import org.opends.server.api.LocalBackend;
import org.opends.server.api.plugin.DirectoryServerPlugin;
import org.opends.server.api.plugin.PluginResult.PreOperation;
import org.opends.server.api.plugin.PluginType;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.PasswordPolicy;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeParser;
import org.opends.server.types.Attributes;
import org.opends.server.types.AuthenticationType;
import org.opends.server.types.CanceledOperationException;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.InitializationException;
import org.opends.server.types.Modification;
import org.opends.server.types.operation.PreOperationAddOperation;
import org.opends.server.types.operation.PreOperationBindOperation;
import org.opends.server.types.operation.PreOperationModifyOperation;
public class MsadPlugin extends DirectoryServerPlugin<MsadPluginCfg>
        implements ConfigurationChangeListener<MsadPluginCfg> {
    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
    private static final String USER_ACCOUNT_CONTROL_OID = "1.2.840.113556.1.4.8";
    private static final String MS_DS_USER_ACCOUNT_DISABLED_OID = "1.2.840.113556.1.4.1853";
    private static final String PWD_LAST_SET_OID = "1.2.840.113556.1.4.96";
    private static final String USER_OID = "1.2.840.113556.1.5.9";
    private static final String MS_DS_BINDABLE_OBJECT_OID = "1.2.840.113556.1.5.244";
    private AttributeType userAccountControlAT;
    private AttributeType msDSUserAccountDisabledAT;
    private AttributeType pwdLastSetAT;
    private ObjectClass userOC;
    private ObjectClass msDSBindableObjectOC;
    private MsadPluginCfg config;
    public MsadPlugin() {
        super();
        logger.info(LocalizableMessage.raw("created MSAD plugin"));
    }
    @Override
    public void initializePlugin(Set<PluginType> pluginTypes, MsadPluginCfg config)
            throws ConfigException, InitializationException {
        this.config = config;
        config.addMsadChangeListener(this);
        Schema schema = getServerContext().getSchema();
        userAccountControlAT = schema.getAttributeType(USER_ACCOUNT_CONTROL_OID);
        msDSUserAccountDisabledAT = schema.getAttributeType(MS_DS_USER_ACCOUNT_DISABLED_OID);
        pwdLastSetAT = schema.getAttributeType(PWD_LAST_SET_OID);
        userOC = schema.getObjectClass(USER_OID);
        msDSBindableObjectOC = schema.getObjectClass(MS_DS_BINDABLE_OBJECT_OID);
        for (PluginType t : pluginTypes) {
            switch (t) {
                case PRE_OPERATION_BIND:
                    break;
                case PRE_OPERATION_ADD:
                    break;
                case PRE_OPERATION_MODIFY:
                    break;
                default:
                    throw new InitializationException(
                            PluginMessages.ERR_PLUGIN_TYPE_NOT_SUPPORTED.get(this.getPluginEntryDN(), t));
            }
        }
        logger.info(LocalizableMessage.raw("initialized MSAD plugin"));
    }
    @Override
    public PreOperation doPreOperation(PreOperationBindOperation bindOperation) {
        DN bindDN = bindOperation.getBindDN();
        Entry userEntry;
        try {
            if (bindOperation.getAuthenticationType().equals(AuthenticationType.SIMPLE)) {
                DN dn = DirectoryServer.getActualRootBindDN(bindDN);
                if (dn != null) {
                    bindDN = dn;
                }
            }
            userEntry = getUserEntry(bindDN);
        } catch (DirectoryException e) {
            logger.traceException(e);
            return PreOperation.stopProcessing(e.getResultCode(), e.getMessageObject());
        }
        if (userEntry == null) {
            return PreOperation.stopProcessing(ResultCode.INVALID_CREDENTIALS, CoreMessages.ERR_BIND_OPERATION_UNKNOWN_USER.get());
        }
        if (parseAttribute(userEntry, msDSUserAccountDisabledAT).asBoolean(false)
                || (parseAttribute(userEntry, userAccountControlAT).asLong(0L) & 2L) != 0L) {
            return PreOperation.stopProcessing(ResultCode.INVALID_CREDENTIALS, CoreMessages.ERR_BIND_OPERATION_ACCOUNT_DISABLED.get());
        }
        return PreOperation.continueOperationProcessing();
    }
    @Override
    public PreOperation doPreOperation(PreOperationAddOperation addOperation) throws CanceledOperationException {
        if (addOperation.isSynchronizationOperation()) {
            return PreOperation.continueOperationProcessing();
        }
        Entry entry = addOperation.getEntryToAdd();
        if (!isActiveDirectoryUser(entry)) {
            return PreOperation.continueOperationProcessing();
        }
        if (parseAttribute(entry, pwdLastSetAT).asLong(-1L) != 0L) {
            entry.replaceAttribute(Attributes.create(pwdLastSetAT, currentTimeInActiveDirectory()));
        }
        return PreOperation.continueOperationProcessing();
    }
    @Override
    public PreOperation doPreOperation(PreOperationModifyOperation modifyOperation) throws CanceledOperationException {
        if (modifyOperation.isSynchronizationOperation()) {
            return PreOperation.continueOperationProcessing();
        }
        Entry entry = modifyOperation.getModifiedEntry();
        if (!isActiveDirectoryUser(entry)) {
            return PreOperation.continueOperationProcessing();
        }
        try {
            AttributeType userPasswordAT = CoreSchema.getUserPasswordAttributeType();
            AuthenticationPolicy policy = AuthenticationPolicy.forUser(entry, true);
            if (policy.isPasswordPolicy()) {
                userPasswordAT = ((PasswordPolicy) policy).getPasswordAttribute();
            }
            boolean needUpdatePwdLastSet = false;
            boolean nonZeroPwdLastSet = true;
            List<Modification> mods = modifyOperation.getModifications();
            for (Modification mod : mods) {
                Attribute attr = mod.getAttribute();
                AttributeType type = attr.getAttributeDescription().getAttributeType();
                if (nonZeroPwdLastSet && userPasswordAT.equals(type)) {
                    needUpdatePwdLastSet = true;
                } else if (pwdLastSetAT.equals(type)) {
                    nonZeroPwdLastSet = AttributeParser.parseAttribute(attr).asLong(-1L) != 0L;
                    needUpdatePwdLastSet = nonZeroPwdLastSet;
                }
            }
            if (needUpdatePwdLastSet) {
                Modification mod = new Modification(ModificationType.REPLACE,
                        Attributes.create(pwdLastSetAT, currentTimeInActiveDirectory()));
                modifyOperation.addModification(mod);
            }
        } catch (DirectoryException e) {
            logger.traceException(e);
            return PreOperation.stopProcessing(e.getResultCode(), e.getMessageObject());
        }
        return PreOperation.continueOperationProcessing();
    }
    @Override
    public ConfigChangeResult applyConfigurationChange(MsadPluginCfg config) {
        logger.info(LocalizableMessage.raw("changed MSAD plugin configuration"));
        this.config = config;
        return new ConfigChangeResult();
    }
    @Override
    public boolean isConfigurationChangeAcceptable(MsadPluginCfg config, List<LocalizableMessage> messages) {
        return true;
    }
    private Entry getUserEntry(DN dn) throws DirectoryException {
        LocalBackend<?> backend = getServerContext().getBackendConfigManager().findLocalBackendForEntry(dn);
        return backend != null ? backend.getEntry(dn) : null;
    }
    private AttributeParser parseAttribute(Entry entry, AttributeType type) {
        List<Attribute> attributes = entry.getAllAttributes(type);
        return AttributeParser.parseAttribute(attributes == null || attributes.isEmpty()
                ? null : attributes.get(0));
    }
    private boolean isActiveDirectoryUser(Entry entry) {
        return (userOC != null && entry.hasObjectClass(userOC))
                || (msDSBindableObjectOC != null && entry.hasObjectClass(msDSBindableObjectOC));
    }
    // https://github.com/apache/directory-studio/blob/2.0.0.v20200411-M15/plugins/valueeditors/src/main/java/org/apache/directory/studio/valueeditors/adtime/ActiveDirectoryTimeUtils.java#L54
    private static String currentTimeInActiveDirectory() {
        return Long.toUnsignedString(System.currentTimeMillis() * 10000L + 116444736000000000L);
    }
}
opendj-server-msad-plugin/src/main/java/opendj/MsadPluginConfiguration.xml
New file
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<adm:managed-object name="msad-plugin" plural-name="msad-plugins"
  package="opendj" extends="plugin"
  parent-package="org.forgerock.opendj.server.config"
  xmlns:adm="http://opendj.forgerock.org/admin"
  xmlns:ldap="http://opendj.forgerock.org/admin-ldap">
  <adm:synopsis>Microsoft Active Directory plugin.</adm:synopsis>
  <adm:profile name="ldap">
    <ldap:object-class>
      <ldap:name>ds-cfg-msad-plugin</ldap:name>
      <ldap:superior>ds-cfg-plugin</ldap:superior>
    </ldap:object-class>
  </adm:profile>
  <adm:property-override name="java-class">
    <adm:default-behavior>
      <adm:defined>
        <adm:value>opendj.MsadPlugin</adm:value>
      </adm:defined>
    </adm:default-behavior>
  </adm:property-override>
</adm:managed-object>
opendj-server-msad-plugin/src/main/java/opendj/Package.xml
New file
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<adm:package name="opendj"
  xmlns:adm="http://opendj.forgerock.org/admin"
  xmlns:ldap="http://opendj.forgerock.org/admin-ldap">
  <adm:synopsis>Microsoft Active Directory plugin.</adm:synopsis>
</adm:package>
opendj-server-msad-plugin/src/main/java/opendj/package-info.java
New file
@@ -0,0 +1 @@
package opendj;
pom.xml
@@ -267,6 +267,7 @@
        <module>opendj-rest2ldap-servlet</module>
        <module>opendj-server</module>
        <module>opendj-server-example-plugin</module>
        <module>opendj-server-msad-plugin</module>
        <module>opendj-legacy</module>
        <module>opendj-server-legacy</module>
        <module>opendj-dsml-servlet</module>