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