From ba0527f3c3b1c639944d262951d59a21bbf59aa9 Mon Sep 17 00:00:00 2001
From: Yubao Liu <yubao.liu@gmail.com>
Date: Tue, 24 May 2022 08:10:33 +0000
Subject: [PATCH] support AD attributes userAccountControl, msDS-UserAccountDisabled and pwdLastSet (#233)

---
 opendj-server-msad-plugin/src/main/assembly/descriptor.xml                    |   30 ++++
 opendj-server-legacy/resource/schema/99-msad.ldif                             |   11 +
 opendj-server-msad-plugin/README.msad.plugin                                  |   40 +++++
 opendj-server-msad-plugin/src/main/assembly/config/schema/99-msad-plugin.ldif |    7 +
 opendj-server-msad-plugin/src/main/assembly/config/msad-plugin.ldif           |   10 +
 opendj-server-msad-plugin/src/main/java/opendj/MsadPluginConfiguration.xml    |   21 +++
 opendj-server-msad-plugin/pom.xml                                             |   69 +++++++++
 opendj-server-msad-plugin/src/main/java/opendj/MsadPlugin.java                |  221 +++++++++++++++++++++++++++++++
 opendj-server-msad-plugin/src/main/java/opendj/package-info.java              |    1 
 opendj-server-msad-plugin/src/main/java/opendj/Package.xml                    |    6 
 pom.xml                                                                       |    1 
 11 files changed, 417 insertions(+), 0 deletions(-)

diff --git a/opendj-server-legacy/resource/schema/99-msad.ldif b/opendj-server-legacy/resource/schema/99-msad.ldif
new file mode 100644
index 0000000..782d973
--- /dev/null
+++ b/opendj-server-legacy/resource/schema/99-msad.ldif
@@ -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' )
+
diff --git a/opendj-server-msad-plugin/README.msad.plugin b/opendj-server-msad-plugin/README.msad.plugin
new file mode 100755
index 0000000..29c9bd1
--- /dev/null
+++ b/opendj-server-msad-plugin/README.msad.plugin
@@ -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
diff --git a/opendj-server-msad-plugin/pom.xml b/opendj-server-msad-plugin/pom.xml
new file mode 100644
index 0000000..04a733c
--- /dev/null
+++ b/opendj-server-msad-plugin/pom.xml
@@ -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>
diff --git a/opendj-server-msad-plugin/src/main/assembly/config/msad-plugin.ldif b/opendj-server-msad-plugin/src/main/assembly/config/msad-plugin.ldif
new file mode 100644
index 0000000..a50b16a
--- /dev/null
+++ b/opendj-server-msad-plugin/src/main/assembly/config/msad-plugin.ldif
@@ -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
diff --git a/opendj-server-msad-plugin/src/main/assembly/config/schema/99-msad-plugin.ldif b/opendj-server-msad-plugin/src/main/assembly/config/schema/99-msad-plugin.ldif
new file mode 100644
index 0000000..de6b6a5
--- /dev/null
+++ b/opendj-server-msad-plugin/src/main/assembly/config/schema/99-msad-plugin.ldif
@@ -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' )
diff --git a/opendj-server-msad-plugin/src/main/assembly/descriptor.xml b/opendj-server-msad-plugin/src/main/assembly/descriptor.xml
new file mode 100644
index 0000000..f00472e
--- /dev/null
+++ b/opendj-server-msad-plugin/src/main/assembly/descriptor.xml
@@ -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>
diff --git a/opendj-server-msad-plugin/src/main/java/opendj/MsadPlugin.java b/opendj-server-msad-plugin/src/main/java/opendj/MsadPlugin.java
new file mode 100644
index 0000000..d6264ae
--- /dev/null
+++ b/opendj-server-msad-plugin/src/main/java/opendj/MsadPlugin.java
@@ -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);
+    }
+}
diff --git a/opendj-server-msad-plugin/src/main/java/opendj/MsadPluginConfiguration.xml b/opendj-server-msad-plugin/src/main/java/opendj/MsadPluginConfiguration.xml
new file mode 100644
index 0000000..550534e
--- /dev/null
+++ b/opendj-server-msad-plugin/src/main/java/opendj/MsadPluginConfiguration.xml
@@ -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>
diff --git a/opendj-server-msad-plugin/src/main/java/opendj/Package.xml b/opendj-server-msad-plugin/src/main/java/opendj/Package.xml
new file mode 100644
index 0000000..da543c4
--- /dev/null
+++ b/opendj-server-msad-plugin/src/main/java/opendj/Package.xml
@@ -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>
diff --git a/opendj-server-msad-plugin/src/main/java/opendj/package-info.java b/opendj-server-msad-plugin/src/main/java/opendj/package-info.java
new file mode 100644
index 0000000..a50187c
--- /dev/null
+++ b/opendj-server-msad-plugin/src/main/java/opendj/package-info.java
@@ -0,0 +1 @@
+package opendj;
diff --git a/pom.xml b/pom.xml
index b0faf50..01cff4c 100644
--- a/pom.xml
+++ b/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>

--
Gitblit v1.10.0