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 implements ConfigurationChangeListener { 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 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 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 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 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); } }