From b94a5ec04283b57e0128bc7508ce4a7a1abc3cb2 Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Sat, 19 Sep 2026 11:52:45 +0300 Subject: [PATCH] Fix genuine bugs found while cleaning up java/unused-parameter alerts - ADUserAccountControl's private constructor assigned uac to both fields, so isAccountLockOut()/isPasswordExpired() never read the real msDSUac value from AD's ms-DS-User-Account-Control-Computed attribute. - JavaScriptExecutorFactory accepted a ClassLoader but never applied it, unlike its Groovy sibling; JS scripts always ran under the ambient thread context classloader instead of the one requested by the caller. - OpenICFWebSocketCreator.unauthorized() dropped the specific reason it was passed and always sent the same generic explanation. Also removes dead OperationOptions/typeName parameters from private helpers (ActiveDirectoryChangeLogSyncStrategy.handleEvents, SchemaApiOpTests.getTestPropertyOrFail, CSVFileConnector findAccount/doDelete/doUpdate) and updates their call sites. --- .../openicf/csvfile/CSVFileConnector.java | 24 ++-- .../contract/test/SchemaApiOpTests.java | 12 +- .../javascript/JavaScriptExecutorFactory.java | 22 +++- .../JavaScriptExecutorFactoryTests.java | 74 +++++++++++++ .../server/jetty/OpenICFWebSocketCreator.java | 3 +- .../jetty/UnauthorizedResponseTest.java | 104 ++++++++++++++++++ .../ldap/ADUserAccountControl.java | 6 +- .../ActiveDirectoryChangeLogSyncStrategy.java | 6 +- .../ldap/ADUserAccountControlTests.java | 54 +++++++++ 9 files changed, 281 insertions(+), 24 deletions(-) create mode 100644 OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/common/script/javascript/JavaScriptExecutorFactoryTests.java create mode 100644 OpenICF-java-framework/connector-server-jetty/src/test/java/org/forgerock/openicf/framework/server/jetty/UnauthorizedResponseTest.java create mode 100644 OpenICF-ldap-connector/src/test/java/org/identityconnectors/ldap/ADUserAccountControlTests.java diff --git a/OpenICF-csvfile-connector/src/main/java/org/forgerock/openicf/csvfile/CSVFileConnector.java b/OpenICF-csvfile-connector/src/main/java/org/forgerock/openicf/csvfile/CSVFileConnector.java index f0a2b0a2..2f1f7d71 100644 --- a/OpenICF-csvfile-connector/src/main/java/org/forgerock/openicf/csvfile/CSVFileConnector.java +++ b/OpenICF-csvfile-connector/src/main/java/org/forgerock/openicf/csvfile/CSVFileConnector.java @@ -14,6 +14,7 @@ * Copyright 2015-2016 ForgeRock AS * Portions Copyright 2011 Viliam Repan * Portions Copyright 2011 Radovan Semancik + * Portions Copyright 2026 3A Systems, LLC. */ package org.forgerock.openicf.csvfile; @@ -264,7 +265,7 @@ private Uid testCredentials(String name, GuardedString password, OperationOption if (name == null) { throw new InvalidCredentialException("Name cannot be null."); } - Uid uid = findAccount(new Uid(name), password, options); + Uid uid = findAccount(new Uid(name), password); if (uid == null) { throw new InvalidCredentialException(String.format("Account %s does not exist.", name)); } @@ -285,7 +286,7 @@ public Uid create(final ObjectClass objectClass, final Set createAttr */ public void delete(final ObjectClass objectClass, final Uid uid, final OperationOptions options) { isAccount(objectClass); - doDelete(uid, options); + doDelete(uid); } /** @@ -613,7 +614,7 @@ public void test() { */ public Uid update(ObjectClass objectClass, Uid uid, Set attributes, OperationOptions options) { isAccount(objectClass); - return doUpdate(UpdateType.UPDATE, uid, attributes, options); + return doUpdate(UpdateType.UPDATE, uid, attributes); } /** @@ -622,7 +623,7 @@ public Uid update(ObjectClass objectClass, Uid uid, Set attributes, O public Uid addAttributeValues(ObjectClass objectClass, Uid uid, Set attributes, OperationOptions options) { isAccount(objectClass); - return doUpdate(UpdateType.ADDVALUES, uid, attributes, options); + return doUpdate(UpdateType.ADDVALUES, uid, attributes); } /** @@ -631,7 +632,7 @@ public Uid addAttributeValues(ObjectClass objectClass, Uid uid, Set a public Uid removeAttributeValues(ObjectClass objectClass, Uid uid, Set attributes, OperationOptions options) { isAccount(objectClass); - return doUpdate(UpdateType.REMOVEVALUES, uid, attributes, options); + return doUpdate(UpdateType.REMOVEVALUES, uid, attributes); } /** @@ -673,12 +674,12 @@ public Uid execute(CreateBatchTask task) { } public BatchEmptyResult execute(DeleteBatchTask task) { - doDelete(task.getUid(), task.getOptions()); + doDelete(task.getUid()); return null; } public Uid execute(UpdateBatchTask task) { - return doUpdate(task.getUpdateType(), task.getUid(), task.getAttributes(), task.getOptions()); + return doUpdate(task.getUpdateType(), task.getUid(), task.getAttributes()); } } @@ -699,8 +700,7 @@ private void isAccount(ObjectClass objectClass) { } } - private Uid findAccount(final Uid uid, final GuardedString password, - final OperationOptions options) { + private Uid findAccount(final Uid uid, final GuardedString password) { if ((password != null && config.getHeaderPassword() == null) || (uid == null && config.getHeaderUid() == null)) { return null; @@ -975,7 +975,7 @@ private Uid doCreate(Set attributes, OperationOptions options) { if (uid == null) { uid = new Uid(UUID.randomUUID().toString()); - } else if (findAccount(uid, null, options) != null) { + } else if (findAccount(uid, null) != null) { throw new AlreadyExistsException(String.format("Account %s already exists.", uid.getUidValue())); } @@ -1016,7 +1016,7 @@ private Uid doCreate(Set attributes, OperationOptions options) { return uid; } - private void doDelete(Uid uid, OperationOptions options) { + private void doDelete(Uid uid) { if (uid == null) { throw new IllegalArgumentException("Uid cannot be null"); } @@ -1085,7 +1085,7 @@ private void doDelete(Uid uid, OperationOptions options) { } } - private Uid doUpdate(UpdateType type, Uid uid, Set attributes, OperationOptions options) { + private Uid doUpdate(UpdateType type, Uid uid, Set attributes) { Uid updated = null; if (uid == null) { throw new IllegalArgumentException("Uid may not be null"); diff --git a/OpenICF-java-framework/connector-framework-contract/src/main/java/org/identityconnectors/contract/test/SchemaApiOpTests.java b/OpenICF-java-framework/connector-framework-contract/src/main/java/org/identityconnectors/contract/test/SchemaApiOpTests.java index 189b54d3..507b86e7 100644 --- a/OpenICF-java-framework/connector-framework-contract/src/main/java/org/identityconnectors/contract/test/SchemaApiOpTests.java +++ b/OpenICF-java-framework/connector-framework-contract/src/main/java/org/identityconnectors/contract/test/SchemaApiOpTests.java @@ -19,6 +19,8 @@ * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * ==================== + * + * Portions Copyrighted 2026 3A Systems LLC. */ package org.identityconnectors.contract.test; @@ -163,7 +165,7 @@ public void testSchemaExpected() { // list of expected object classes @SuppressWarnings("unchecked") - List expOClasses = (List) getTestPropertyOrFail(List.class.getName(), + List expOClasses = (List) getTestPropertyOrFail( SUPPORTED_OBJECT_CLASSES_PROPERTY_PREFIX, true); List testedOClasses = new ArrayList(); @@ -185,7 +187,7 @@ public void testSchemaExpected() { // list of expected attributes for the object class @SuppressWarnings("unchecked") - List expAttrs = (List) getTestPropertyOrFail(List.class.getName(), + List expAttrs = (List) getTestPropertyOrFail( "attributes." + ocInfo.getType() + "." + SUPPORTED_OBJECT_CLASSES_PROPERTY_PREFIX, strictCheck); @@ -200,7 +202,7 @@ public void testSchemaExpected() { // expected attribute values @SuppressWarnings("unchecked") Map expAttrValues = (Map) getTestPropertyOrFail( - Map.class.getName(), attr.getName() + ".attribute." + ocInfo.getType() + attr.getName() + ".attribute." + ocInfo.getType() + "." + SUPPORTED_OBJECT_CLASSES_PROPERTY_PREFIX, strictCheck); // check attribute's values in case the test is strict or property is provided @@ -230,7 +232,7 @@ public void testSchemaExpected() { // expected object classes supported by operations @SuppressWarnings("unchecked") Map> expOperations = (Map>) getTestPropertyOrFail( - Map.class.getName(), SUPPORTED_OPERATIONS_PROPERTY_PREFIX, true); + SUPPORTED_OPERATIONS_PROPERTY_PREFIX, true); Map, Set> supportedOperations = schema .getSupportedObjectClassesByOperation(); @@ -340,7 +342,7 @@ private Boolean getStrictCheckProperty() { /** * Returns property value or fails test if property is not defined. */ - private Object getTestPropertyOrFail(String typeName, String propName, boolean failOnError) { + private Object getTestPropertyOrFail(String propName, boolean failOnError) { Object propValue = null; try { diff --git a/OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/common/script/javascript/JavaScriptExecutorFactory.java b/OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/common/script/javascript/JavaScriptExecutorFactory.java index df61fac1..e6b6dfc2 100644 --- a/OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/common/script/javascript/JavaScriptExecutorFactory.java +++ b/OpenICF-java-framework/connector-framework-internal/src/main/java/org/identityconnectors/common/script/javascript/JavaScriptExecutorFactory.java @@ -91,9 +91,11 @@ public ScriptExecutor newScriptExecutor(ClassLoader loader, String script, boole } private static class CompiledJavaScriptExecutor implements ScriptExecutor { + private final ClassLoader loader; private final CompiledScript compiled; public CompiledJavaScriptExecutor(ClassLoader loader, CompiledScript compiled) { + this.loader = loader; this.compiled = compiled; } @@ -104,15 +106,24 @@ public Object execute(Map arguments) throws Exception { for (Map.Entry entry : args.entrySet()) { engineScope.put(entry.getKey(), entry.getValue()); } - return compiled.eval(newContext); + Thread currentThread = Thread.currentThread(); + ClassLoader previousLoader = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(loader); + try { + return compiled.eval(newContext); + } finally { + currentThread.setContextClassLoader(previousLoader); + } } } private class JavaScriptExecutor implements ScriptExecutor { + private final ClassLoader loader; private final String script; public JavaScriptExecutor(ClassLoader loader, String script) { + this.loader = loader; this.script = script; } @@ -123,7 +134,14 @@ public Object execute(Map arguments) throws Exception { for (Map.Entry entry : args.entrySet()) { engine.put(entry.getKey(), entry.getValue()); } - return engine.eval(script); + Thread currentThread = Thread.currentThread(); + ClassLoader previousLoader = currentThread.getContextClassLoader(); + currentThread.setContextClassLoader(loader); + try { + return engine.eval(script); + } finally { + currentThread.setContextClassLoader(previousLoader); + } } } diff --git a/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/common/script/javascript/JavaScriptExecutorFactoryTests.java b/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/common/script/javascript/JavaScriptExecutorFactoryTests.java new file mode 100644 index 00000000..477df015 --- /dev/null +++ b/OpenICF-java-framework/connector-framework-internal/src/test/java/org/identityconnectors/common/script/javascript/JavaScriptExecutorFactoryTests.java @@ -0,0 +1,74 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 3A Systems, LLC. + */ + +package org.identityconnectors.common.script.javascript; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertSame; + +import java.net.URLClassLoader; + +import org.identityconnectors.common.script.ScriptExecutor; +import org.identityconnectors.common.script.ScriptExecutorFactory; +import org.testng.annotations.Test; + +public class JavaScriptExecutorFactoryTests { + + @Test + public void testValidScript() throws Exception { + ScriptExecutor ex = getScriptExecutor("1 + 1;", false); + assertEquals(((Number) ex.execute(null)).intValue(), 2); + } + + @Test + public void testCompiledScript() throws Exception { + ScriptExecutor ex = getScriptExecutor("1 + 1;", true); + assertEquals(((Number) ex.execute(null)).intValue(), 2); + } + + @Test + public void testUncompiledScriptRunsWithProvidedClassLoader() throws Exception { + ClassLoader custom = new URLClassLoader(new java.net.URL[0], getClass().getClassLoader()); + ScriptExecutor ex = ScriptExecutorFactory.newInstance("JavaScript").newScriptExecutor(custom, + "java.lang.Thread.currentThread().getContextClassLoader();", false); + assertSame(ex.execute(null), custom); + } + + @Test + public void testCompiledScriptRunsWithProvidedClassLoader() throws Exception { + ClassLoader custom = new URLClassLoader(new java.net.URL[0], getClass().getClassLoader()); + ScriptExecutor ex = ScriptExecutorFactory.newInstance("JavaScript").newScriptExecutor(custom, + "java.lang.Thread.currentThread().getContextClassLoader();", true); + assertSame(ex.execute(null), custom); + } + + @Test + public void testScriptDoesNotLeakClassLoaderAfterExecution() throws Exception { + ClassLoader before = Thread.currentThread().getContextClassLoader(); + ClassLoader custom = new URLClassLoader(new java.net.URL[0], getClass().getClassLoader()); + ScriptExecutor ex = getScriptExecutor("1;", custom, false); + ex.execute(null); + assertSame(Thread.currentThread().getContextClassLoader(), before); + } + + private ScriptExecutor getScriptExecutor(String script, boolean compile) { + return getScriptExecutor(script, getClass().getClassLoader(), compile); + } + + private ScriptExecutor getScriptExecutor(String script, ClassLoader loader, boolean compile) { + return ScriptExecutorFactory.newInstance("JavaScript").newScriptExecutor(loader, script, compile); + } +} diff --git a/OpenICF-java-framework/connector-server-jetty/src/main/java/org/forgerock/openicf/framework/server/jetty/OpenICFWebSocketCreator.java b/OpenICF-java-framework/connector-server-jetty/src/main/java/org/forgerock/openicf/framework/server/jetty/OpenICFWebSocketCreator.java index 2e4c5d82..e78dcfc3 100644 --- a/OpenICF-java-framework/connector-server-jetty/src/main/java/org/forgerock/openicf/framework/server/jetty/OpenICFWebSocketCreator.java +++ b/OpenICF-java-framework/connector-server-jetty/src/main/java/org/forgerock/openicf/framework/server/jetty/OpenICFWebSocketCreator.java @@ -167,7 +167,8 @@ protected void unauthorized(JettyServerUpgradeResponse response, String message) try { response.sendError( HttpServletResponse.SC_FORBIDDEN, - "A client certificate is required for accessing OpenICF application but the server's listener is not configured for mutual authentication (or the client did not provide a certificate)."); + message + + ": a client certificate is required for accessing OpenICF application but the server's listener is not configured for mutual authentication (or the client did not provide a certificate)."); } catch (IOException e) { // } diff --git a/OpenICF-java-framework/connector-server-jetty/src/test/java/org/forgerock/openicf/framework/server/jetty/UnauthorizedResponseTest.java b/OpenICF-java-framework/connector-server-jetty/src/test/java/org/forgerock/openicf/framework/server/jetty/UnauthorizedResponseTest.java new file mode 100644 index 00000000..62608831 --- /dev/null +++ b/OpenICF-java-framework/connector-server-jetty/src/test/java/org/forgerock/openicf/framework/server/jetty/UnauthorizedResponseTest.java @@ -0,0 +1,104 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 3A Systems, LLC. + */ +package org.forgerock.openicf.framework.server.jetty; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Collections; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicReference; + +import javax.security.auth.callback.NameCallback; + +import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest; +import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse; +import org.forgerock.openicf.framework.remote.rpc.OperationMessageListener; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * When createWebSocket() cannot resolve a principal, it must reject the upgrade with the + * specific reason it determined, not a generic message that hides why (OpenIdentityPlatform/OpenICF). + */ +public class UnauthorizedResponseTest { + + private static OperationMessageListener noopListener() { + return (OperationMessageListener) Proxy.newProxyInstance( + UnauthorizedResponseTest.class.getClassLoader(), + new Class[] { OperationMessageListener.class }, + new InvocationHandler() { + public Object invoke(Object p, Method m, Object[] a) { + return null; + } + }); + } + + private static JettyServerUpgradeRequest upgradeRequest() { + return (JettyServerUpgradeRequest) Proxy.newProxyInstance( + UnauthorizedResponseTest.class.getClassLoader(), + new Class[] { JettyServerUpgradeRequest.class }, + new InvocationHandler() { + public Object invoke(Object p, Method m, Object[] a) { + if ("getSubProtocols".equals(m.getName())) { + return Collections.emptyList(); + } + return null; + } + }); + } + + @Test(timeOut = 30000) + public void testUnauthorizedResponseIncludesTheReason() throws Exception { + ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1); + try { + // Never sets a name on the callback, so authenticate() cannot resolve a principal. + Authenticator silentAuthenticator = new Authenticator() { + @Override + public void authenticate(JettyServerUpgradeRequest request, + JettyServerUpgradeResponse response, NameCallback callback) { + } + }; + OpenICFWebSocketCreator creator = new OpenICFWebSocketCreator(null, noopListener(), + silentAuthenticator, scheduler); + + final AtomicReference sentMessage = new AtomicReference(); + JettyServerUpgradeResponse response = (JettyServerUpgradeResponse) Proxy.newProxyInstance( + UnauthorizedResponseTest.class.getClassLoader(), + new Class[] { JettyServerUpgradeResponse.class }, + new InvocationHandler() { + public Object invoke(Object p, Method m, Object[] a) { + if ("isCommitted".equals(m.getName())) { + return Boolean.FALSE; + } + if ("sendError".equals(m.getName())) { + sentMessage.set((String) a[1]); + } + return null; + } + }); + + Object result = creator.createWebSocket(upgradeRequest(), response); + + Assert.assertNull(result, "an unresolved principal must not get a websocket endpoint"); + Assert.assertNotNull(sentMessage.get(), "createWebSocket() must reject with an error response"); + Assert.assertTrue(sentMessage.get().contains("Unknown Principal"), + "the rejection must surface the specific reason it was passed, not just the generic explanation"); + } finally { + scheduler.shutdownNow(); + } + } +} diff --git a/OpenICF-ldap-connector/src/main/java/org/identityconnectors/ldap/ADUserAccountControl.java b/OpenICF-ldap-connector/src/main/java/org/identityconnectors/ldap/ADUserAccountControl.java index 79c144c6..c1efb3bf 100644 --- a/OpenICF-ldap-connector/src/main/java/org/identityconnectors/ldap/ADUserAccountControl.java +++ b/OpenICF-ldap-connector/src/main/java/org/identityconnectors/ldap/ADUserAccountControl.java @@ -20,6 +20,8 @@ * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" + * + * Portions Copyrighted 2026 3A Systems LLC. */ package org.identityconnectors.ldap; @@ -208,9 +210,9 @@ public class ADUserAccountControl { public ADUserAccountControl() { } - private ADUserAccountControl(int uac, int msDSUac) { + ADUserAccountControl(int uac, int msDSUac) { this.uac = uac; - this.msDSUac = uac; + this.msDSUac = msDSUac; } public boolean isNormalAccount() { diff --git a/OpenICF-ldap-connector/src/main/java/org/identityconnectors/ldap/sync/activedirectory/ActiveDirectoryChangeLogSyncStrategy.java b/OpenICF-ldap-connector/src/main/java/org/identityconnectors/ldap/sync/activedirectory/ActiveDirectoryChangeLogSyncStrategy.java index b9b6ccdd..91782d92 100644 --- a/OpenICF-ldap-connector/src/main/java/org/identityconnectors/ldap/sync/activedirectory/ActiveDirectoryChangeLogSyncStrategy.java +++ b/OpenICF-ldap-connector/src/main/java/org/identityconnectors/ldap/sync/activedirectory/ActiveDirectoryChangeLogSyncStrategy.java @@ -20,6 +20,8 @@ * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" + * + * Portions Copyrighted 2026 3A Systems LLC. */ package org.identityconnectors.ldap.sync.activedirectory; @@ -106,7 +108,7 @@ public SyncToken getLatestSyncToken() { public void sync(SyncToken token, final SyncResultsHandler handler, final OperationOptions options) { if (oclass.is(DIRSYNC_EVENTS_OBJCLASS)) { - handleEvents(token, handler, options); + handleEvents(token, handler); } else { // ldapsearch -h host -p 389 -b "ou=test,dc=example,dc=com" -D "cn=administrator,cn=users,dc=example,dc=com" -w xxx "(uSNChanged>=52410)" // We use the uSNchanged attribute to detect changes on entries and newly created entries. @@ -412,7 +414,7 @@ private byte[] getDirSyncCookie() { return null; } - private void handleEvents(SyncToken token, SyncResultsHandler handler, OperationOptions options) { + private void handleEvents(SyncToken token, SyncResultsHandler handler) { ArrayList changes = new ArrayList(); String searchFilter = "(|(objectClass=group)(objectclass=user))"; Control[] rspCtls; diff --git a/OpenICF-ldap-connector/src/test/java/org/identityconnectors/ldap/ADUserAccountControlTests.java b/OpenICF-ldap-connector/src/test/java/org/identityconnectors/ldap/ADUserAccountControlTests.java new file mode 100644 index 00000000..1e662701 --- /dev/null +++ b/OpenICF-ldap-connector/src/test/java/org/identityconnectors/ldap/ADUserAccountControlTests.java @@ -0,0 +1,54 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 3A Systems, LLC. + */ +package org.identityconnectors.ldap; + +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertTrue; + +import org.testng.annotations.Test; + +/** + * uac (userAccountControl) and msDSUac (ms-DS-User-Account-Control-Computed) are two + * distinct LDAP attributes with different semantics; the constructor must keep them separate. + */ +public class ADUserAccountControlTests { + + @Test + public void testLockoutAndPasswordExpiredAreReadFromMsDSUacNotUac() { + // uac carries none of the bits under test; msDSUac carries both. + ADUserAccountControl control = + new ADUserAccountControl(ADUserAccountControl.NORMAL_ACCOUNT, + ADUserAccountControl.LOCKOUT | ADUserAccountControl.PASSWORD_EXPIRED); + + assertTrue("isAccountLockOut() must read the msDSUac value, not uac", + control.isAccountLockOut()); + assertTrue("isPasswordExpired() must read the msDSUac value, not uac", + control.isPasswordExpired()); + } + + @Test + public void testLockoutAndPasswordExpiredIgnoreUacBits() { + // uac carries both bits; msDSUac carries neither, so the computed status must be false. + ADUserAccountControl control = + new ADUserAccountControl(ADUserAccountControl.LOCKOUT + | ADUserAccountControl.PASSWORD_EXPIRED, 0); + + assertFalse("isAccountLockOut() must not be derived from uac", + control.isAccountLockOut()); + assertFalse("isPasswordExpired() must not be derived from uac", + control.isPasswordExpired()); + } +}