diff --git a/elemental-parent/pom.xml b/elemental-parent/pom.xml index 68a31ad942..015b2c0f92 100644 --- a/elemental-parent/pom.xml +++ b/elemental-parent/pom.xml @@ -322,6 +322,7 @@
XML_STYLE
SLASHSTAR_STYLE SLASHSTAR_STYLE + SCRIPT_STYLE XML_STYLE SCRIPT_STYLE XML_STYLE diff --git a/exist-core/pom.xml b/exist-core/pom.xml index 5a8a49665f..b3342b02a2 100644 --- a/exist-core/pom.xml +++ b/exist-core/pom.xml @@ -883,6 +883,7 @@ src/test/java/org/exist/collections/triggers/XQueryTriggerSetUidTest.java src/test/java/org/exist/collections/triggers/XQueryTriggerTest.java src/main/java/org/exist/config/Configuration.java + src/main/java/org/exist/config/ConfigurationDocumentTrigger.java src/main/java/org/exist/config/ConfigurationImpl.java src/main/java/org/exist/config/Configurator.java src/test/java/org/exist/config/TwoDatabasesTest.java @@ -1003,10 +1004,14 @@ src/main/java/org/exist/repo/ExistRepository.java src/main/java/org/exist/scheduler/UserXQueryJob.java src/main/java/org/exist/scheduler/impl/QuartzSchedulerImpl.java + src/main/java/org/exist/security/AbstractRealm.java + src/main/java/org/exist/security/AXSchemaType.java src/main/java/org/exist/security/EffectiveSubject.java + src/main/java/org/exist/security/EXistSchemaType.java src/test/java/org/exist/security/FnDocSecurityTest.java src/main/java/org/exist/security/Permission.java src/main/java/org/exist/security/PermissionRequired.java + src/main/java/org/exist/security/Principal.java src/test/java/org/exist/security/RestApiSecurityTest.java src/main/java/org/exist/security/SecurityManager.java src/main/java/org/exist/security/SimpleACLPermissionInternal.java @@ -1015,7 +1020,11 @@ src/test/java/org/exist/security/UnixStylePermissionTest.java src/test/java/org/exist/security/XqueryApiTest.java src/main/java/org/exist/security/internal/AccountImpl.java + src/main/java/org/exist/security/internal/RealmImpl.java + src/main/java/org/exist/security/internal/SecurityManagerImpl.java src/main/java/org/exist/security/internal/aider/UnixStylePermissionAider.java + src/main/java/org/exist/security/management/AccountsManagement.java + src/main/java/org/exist/security/management/GroupsManagement.java src/main/java/org/exist/source/Source.java src/main/java/org/exist/source/SourceFactory.java src/main/java/org/exist/source/URLSource.java @@ -1587,6 +1596,7 @@ src/test/java/org/exist/collections/triggers/XQueryTriggerSetUidTest.java src/test/java/org/exist/collections/triggers/XQueryTriggerTest.java src/main/java/org/exist/config/Configuration.java + src/main/java/org/exist/config/ConfigurationDocumentTrigger.java src/main/java/org/exist/config/ConfigurationImpl.java src/main/java/org/exist/config/Configurator.java src/test/java/org/exist/config/TwoDatabasesTest.java @@ -1722,11 +1732,15 @@ src/main/java/org/exist/resolver/XercesXmlResolverAdapter.java src/main/java/org/exist/scheduler/UserXQueryJob.java src/main/java/org/exist/scheduler/impl/QuartzSchedulerImpl.java + src/main/java/org/exist/security/AbstractRealm.java + src/main/java/org/exist/security/AXSchemaType.java src/main/java/org/exist/security/EffectiveSubject.java + src/main/java/org/exist/security/EXistSchemaType.java src/test/java/org/exist/security/FnDocSecurityTest.java src/main/java/org/exist/security/Permission.java src/main/java/org/exist/security/PermissionRequired.java src/main/java/org/exist/security/PermissionRequiredCheck.java + src/main/java/org/exist/security/Principal.java src/test/java/org/exist/security/RestApiSecurityTest.java src/main/java/org/exist/security/SecurityManager.java src/main/java/org/exist/security/SimpleACLPermissionInternal.java @@ -1735,7 +1749,11 @@ src/test/java/org/exist/security/UnixStylePermissionTest.java src/test/java/org/exist/security/XqueryApiTest.java src/main/java/org/exist/security/internal/AccountImpl.java + src/main/java/org/exist/security/internal/RealmImpl.java + src/main/java/org/exist/security/internal/SecurityManagerImpl.java src/main/java/org/exist/security/internal/aider/UnixStylePermissionAider.java + src/main/java/org/exist/security/management/AccountsManagement.java + src/main/java/org/exist/security/management/GroupsManagement.java src/main/java/org/exist/source/Source.java src/main/java/org/exist/source/SourceFactory.java src/main/java/org/exist/source/URLSource.java diff --git a/exist-core/src/main/java/org/exist/config/ConfigurationDocumentTrigger.java b/exist-core/src/main/java/org/exist/config/ConfigurationDocumentTrigger.java index 328a556941..6126e90a7e 100644 --- a/exist-core/src/main/java/org/exist/config/ConfigurationDocumentTrigger.java +++ b/exist-core/src/main/java/org/exist/config/ConfigurationDocumentTrigger.java @@ -1,4 +1,28 @@ /* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * * eXist-db Open Source Native XML Database * Copyright (C) 2001 The eXist-db Authors * @@ -332,9 +356,10 @@ private void processPrincipal(final PrincipalType principalType) throws SAXExcep //check if there is a name collision, i.e. another principal with the same name final String principalName = findName(); - // first check if the account or group exists before trying to retrieve it - // otherwise the LDAP realm will create a new user, leading to an endless loop + + // NOTE(AR) first check if the account or group exists before trying to retrieve it otherwise a realm (e.g. LDAP) may create a new user, which could lead to an endless loop and eventually a StackOverflowError final boolean principalExists = principalName != null && principalType.hasPrincipal(sm, principalName); + Principal existingPrincipleByName = null; if (principalExists) { existingPrincipleByName = principalType.getPrincipal(sm, principalName); diff --git a/exist-core/src/main/java/org/exist/config/Configurator.java b/exist-core/src/main/java/org/exist/config/Configurator.java index ada04750a9..391d6d463a 100644 --- a/exist-core/src/main/java/org/exist/config/Configurator.java +++ b/exist-core/src/main/java/org/exist/config/Configurator.java @@ -66,6 +66,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.annotation.Nullable; import javax.xml.XMLConstants; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; @@ -156,9 +157,15 @@ protected static T getAnnotation(Field field, Class an * * @return The Getter method for the property or null */ - public static Method searchForGetMethod(final Class clazz, final String property) { + public static @Nullable Method searchForGetMethod(final Class clazz, final String property) { try { - final String methodName = ("get" + property).toLowerCase(); + String methodName = property; + if (!methodName.startsWith("get-") && !methodName.startsWith("get_")) { + methodName = "get" + methodName; + } + methodName = methodName.replace("-", ""); + methodName = methodName.replace("_", ""); + for (final Method method : clazz.getMethods()) { if (method.getName().equalsIgnoreCase(methodName)) { return method; @@ -178,9 +185,15 @@ public static Method searchForGetMethod(final Class clazz, final String prope * * @return The Setter method for the field or null */ - public static Method searchForSetMethod(final Class clazz, final Field field) { + public static @Nullable Method searchForSetMethod(final Class clazz, final Field field) { try { - final String methodName = ("set" + field.getName()).toLowerCase(); + String methodName = field.getName(); + if (!methodName.startsWith("set-") && !methodName.startsWith("set_")) { + methodName = "set" + methodName; + } + methodName = methodName.replace("-", ""); + methodName = methodName.replace("_", ""); + for (final Method method : clazz.getMethods()) { if (method.getName().equalsIgnoreCase(methodName)) { return method; @@ -200,9 +213,14 @@ public static Method searchForSetMethod(final Class clazz, final Field field) * * @return The Adder method for the property or null */ - public static Method searchForAddMethod(final Class clazz, final String property) { + public static @Nullable Method searchForAddMethod(final Class clazz, final String property) { try { - final String methodName = ("add" + property).toLowerCase(); + String methodName = property; + if (!methodName.startsWith("add-") && !methodName.startsWith("add_")) { + methodName = "add" + methodName; + } + methodName = methodName.replace("-", ""); + methodName = methodName.replace("_", ""); for (final Method method : clazz.getMethods()) { if (method.getName().equalsIgnoreCase(methodName) && method.getParameterTypes().length == 1 @@ -216,9 +234,15 @@ public static Method searchForAddMethod(final Class clazz, final String prope return null; } - public static Method searchForInsertMethod(final Class clazz, final String property) { + public static @Nullable Method searchForInsertMethod(final Class clazz, final String property) { try { - final String methodName = ("insert" + property).toLowerCase(); + String methodName = property; + if (!methodName.startsWith("insert-") && !methodName.startsWith("insert_")) { + methodName = "insert" + methodName; + } + methodName = methodName.replace("-", ""); + methodName = methodName.replace("_", ""); + for (final Method method : clazz.getMethods()) { if (method.getName().equalsIgnoreCase(methodName) && method.getParameterTypes().length == 2 diff --git a/exist-core/src/main/java/org/exist/security/AXSchemaType.java b/exist-core/src/main/java/org/exist/security/AXSchemaType.java index 59fb6b5e09..305a77c614 100644 --- a/exist-core/src/main/java/org/exist/security/AXSchemaType.java +++ b/exist-core/src/main/java/org/exist/security/AXSchemaType.java @@ -1,4 +1,28 @@ /* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * * eXist-db Open Source Native XML Database * Copyright (C) 2001 The eXist-db Authors * @@ -21,6 +45,8 @@ */ package org.exist.security; +import javax.annotation.Nullable; + /** * * @author Adam Retter @@ -54,7 +80,7 @@ public String getAlias() { return alias; } - public static AXSchemaType valueOfNamespace(final String namespace) { + public static @Nullable AXSchemaType valueOfNamespace(final String namespace) { for(final AXSchemaType axSchemaType : AXSchemaType.values()) { if(axSchemaType.getNamespace().equals(namespace)) { return axSchemaType; @@ -63,7 +89,7 @@ public static AXSchemaType valueOfNamespace(final String namespace) { return null; } - public static AXSchemaType valueOfAlias(final String alias) { + public static @Nullable AXSchemaType valueOfAlias(final String alias) { for(final AXSchemaType axSchemaType : AXSchemaType.values()) { if(axSchemaType.getAlias().equals(alias)) { return axSchemaType; diff --git a/exist-core/src/main/java/org/exist/security/AbstractRealm.java b/exist-core/src/main/java/org/exist/security/AbstractRealm.java index 8f14f1538b..e038911693 100644 --- a/exist-core/src/main/java/org/exist/security/AbstractRealm.java +++ b/exist-core/src/main/java/org/exist/security/AbstractRealm.java @@ -1,4 +1,28 @@ /* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * * eXist-db Open Source Native XML Database * Copyright (C) 2001 The eXist-db Authors * @@ -22,12 +46,15 @@ package org.exist.security; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Optional; +import com.evolvedbinary.j8fu.tuple.Tuple2; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.Database; @@ -49,6 +76,10 @@ import org.exist.util.LockException; import org.exist.xmldb.XmldbURI; +import javax.annotation.Nullable; + +import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple; + /** * @author Dmitriy Shabanov * @@ -117,7 +148,7 @@ private void loadGroupsFromRealmStorage(final DBBroker broker) throws Configurat final GroupImpl group = new GroupImpl(r, conf); getSecurityManager().registerGroup(group); - principalDb.put(group.getName(), group); + principalDb.put(group.getName(), Tuple(PrincipalState.PERSISTENT, Optional.of(group))); //set collection if(group.getId() > 0) { @@ -181,7 +212,7 @@ private void loadAccountsFromRealmStorage(final DBBroker broker) throws Configur } getSecurityManager().registerAccount(account); - principalDb.put(account.getName(), account); + principalDb.put(account.getName(), Tuple(PrincipalState.PERSISTENT, Optional.of(account))); //set collection if(account.getId() > 0) { @@ -252,34 +283,67 @@ public void save() throws PermissionDeniedException, EXistException { configuration.save(); } + public final void registerAccountCreation(final String name) { + usersByName.write(principalDb -> { + @Nullable final Tuple2> accountEntry = principalDb.get(name); + if (accountEntry != null) { + // if the account is already being created or already persisted then we don't need to do anything + return; + } + + principalDb.put(name, Tuple(PrincipalState.CREATING, Optional.empty())); + }); + } + //Accounts management methods public final Account registerAccount(final Account account) { usersByName.write(principalDb -> { - if(principalDb.containsKey(account.getName())) { - throw new IllegalArgumentException("Account " + account.getName() + " exist."); + @Nullable final Tuple2> accountEntry = principalDb.get(account.getName()); + if (accountEntry != null && accountEntry._1 == PrincipalState.PERSISTENT) { + throw new IllegalArgumentException("The Account " + account.getName() + " already exists."); } - principalDb.put(account.getName(), account); + principalDb.put(account.getName(), Tuple(PrincipalState.PERSISTENT, Optional.of(account))); }); return account; } + + public final void registerGroupCreation(final String name) { + groupsByName.write(principalDb -> { + @Nullable final Tuple2> accountEntry = principalDb.get(name); + if (accountEntry != null) { + // if the group is already being created or already persisted then we don't need to do anything + return; + } + + principalDb.put(name, Tuple(PrincipalState.CREATING, Optional.empty())); + }); + } public final Group registerGroup(final Group group) { groupsByName.write(principalDb -> { - if(principalDb.containsKey(group.getName())) { - throw new IllegalArgumentException("Group " + group.getName() + " already exists."); + @Nullable final Tuple2> groupEntry = principalDb.get(group.getName()); + if (groupEntry != null && groupEntry._1 == PrincipalState.PERSISTENT) { + throw new IllegalArgumentException("The Group " + group.getName() + " already exists."); } - principalDb.put(group.getName(), group); + principalDb.put(group.getName(), Tuple(PrincipalState.PERSISTENT, Optional.of(group))); }); return group; } @Override - public Account getAccount(final String name) { - return usersByName.read(principalDb -> principalDb.get(name)); + public @Nullable Account getAccount(final String name) { + return usersByName.read(principalDb -> { + @Nullable final Tuple2> principalEntry = principalDb.get(name); + if (principalEntry == null || principalEntry._1 != PrincipalState.PERSISTENT) { + // skip non-existent and not yet persisted entries + return null; + } + return principalEntry._2.get(); + }); } @Override @@ -288,7 +352,7 @@ public boolean hasAccount(final String accountName) { } @Override - public final boolean hasAccount(final Account account) { + public boolean hasAccount(final Account account) { return hasAccountLocal(account); } @@ -299,12 +363,33 @@ public boolean hasAccountLocal(final Account account) { @Override public boolean hasAccountLocal(final String accountName) { - return usersByName.read(principalDb -> principalDb.containsKey(accountName)); + return usersByName.read(principalDb -> { + @Nullable final Tuple2> principalEntry = principalDb.get(accountName); + // skip non-existent and not yet persisted entries + return principalEntry != null && principalEntry._1 == PrincipalState.PERSISTENT; + }); } @Override public final java.util.Collection getAccounts() { - return usersByName.read(Map::values); + return usersByName.read(principalDb -> { + @Nullable List accounts = null; + for (final Tuple2> principalEntry : principalDb.values()) { + // skip not yet persisted entries + if (principalEntry._1 == PrincipalState.PERSISTENT) { + if (accounts == null) { + accounts = new ArrayList<>(); + } + accounts.add(principalEntry._2.get()); + } + } + + if (accounts == null) { + accounts = Collections.emptyList(); + } + + return accounts; + }); } //Groups management methods @@ -321,7 +406,11 @@ public boolean hasGroup(final String name) { @Override public boolean hasGroupLocal(final String groupName) { - return groupsByName.read(principalDb -> principalDb.containsKey(groupName)); + return groupsByName.read(principalDb -> { + @Nullable final Tuple2> principalEntry = principalDb.get(groupName); + // skip not yet persisted entries + return principalEntry != null && principalEntry._1 == PrincipalState.PERSISTENT; + }); } @Override @@ -330,13 +419,37 @@ public final boolean hasGroupLocal(final Group role) { } @Override - public Group getGroup(final String name) { - return groupsByName.read(principalDb -> principalDb.get(name)); + public @Nullable Group getGroup(final String name) { + return groupsByName.read(principalDb -> { + @Nullable final Tuple2> principalEntry = principalDb.get(name); + if (principalEntry == null || principalEntry._1 != PrincipalState.PERSISTENT) { + // skip non-existent and not yet persisted entries + return null; + } + return principalEntry._2.get(); + }); } @Override public final java.util.Collection getGroups() { - return groupsByName.read(Map::values); + return groupsByName.read(principalDb -> { + @Nullable List groups = null; + for (final Tuple2> principalEntry : principalDb.values()) { + // skip not yet persisted entries + if (principalEntry._1 == PrincipalState.PERSISTENT) { + if (groups == null) { + groups = new ArrayList<>(); + } + groups.add(principalEntry._2.get()); + } + } + + if (groups == null) { + groups = Collections.emptyList(); + } + + return groups; + }); } //collections related methods @@ -476,7 +589,7 @@ public Group getExternalGroup(final String name) { //configuration methods @Override public boolean isConfigured() { - return (configuration != null); + return configuration != null; } @Override @@ -523,8 +636,24 @@ public java.util.Collection findGroupnamesWhereGroupnameStarts public java.util.Collection findGroupnamesWhereGroupnameContains(final String fragment) { return Collections.emptyList(); } - - protected static class PrincipalDbByName extends ConcurrentValueWrapper> { + + protected enum PrincipalState { + CREATING, + PERSISTENT + } + + /** + * Invariants: + * + * When PrincipalState == PrincipalState#CREATING + * Optional is always empty. + * + * When PrincipalState == PrincipalState#PERSISTENT + * Optional is always present. + * + * @param The type of the Principal. + */ + protected static class PrincipalDbByName extends ConcurrentValueWrapper>>> { public PrincipalDbByName() { super(new HashMap<>(65)); } diff --git a/exist-core/src/main/java/org/exist/security/EXistSchemaType.java b/exist-core/src/main/java/org/exist/security/EXistSchemaType.java index 379ef0097d..a5d7d49534 100644 --- a/exist-core/src/main/java/org/exist/security/EXistSchemaType.java +++ b/exist-core/src/main/java/org/exist/security/EXistSchemaType.java @@ -1,4 +1,28 @@ /* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * * eXist-db Open Source Native XML Database * Copyright (C) 2001 The eXist-db Authors * @@ -21,6 +45,8 @@ */ package org.exist.security; +import javax.annotation.Nullable; + /** * * @author Adam Retter @@ -46,7 +72,7 @@ public String getAlias() { return alias; } - public static EXistSchemaType valueOfNamespace(final String namespace) { + public static @Nullable EXistSchemaType valueOfNamespace(final String namespace) { for(final EXistSchemaType existSchemaType : EXistSchemaType.values()) { if(existSchemaType.getNamespace().equals(namespace)) { return existSchemaType; @@ -55,7 +81,7 @@ public static EXistSchemaType valueOfNamespace(final String namespace) { return null; } - public static EXistSchemaType valueOfAlias(final String alias) { + public static @Nullable EXistSchemaType valueOfAlias(final String alias) { for(final EXistSchemaType existSchemaType : EXistSchemaType.values()) { if(existSchemaType.getAlias().equals(alias)) { return existSchemaType; diff --git a/exist-core/src/main/java/org/exist/security/Principal.java b/exist-core/src/main/java/org/exist/security/Principal.java index d1e7e5e2b0..60fa7aee9c 100644 --- a/exist-core/src/main/java/org/exist/security/Principal.java +++ b/exist-core/src/main/java/org/exist/security/Principal.java @@ -1,4 +1,28 @@ /* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * * eXist-db Open Source Native XML Database * Copyright (C) 2001 The eXist-db Authors * @@ -25,6 +49,8 @@ import org.exist.config.ConfigurationException; import org.exist.security.realm.Realm; import org.exist.storage.DBBroker; + +import javax.annotation.Nullable; import java.util.Set; /** @@ -45,7 +71,7 @@ public interface Principal extends java.security.Principal, Configurable { void setMetadataValue(SchemaType schemaType, String value); - String getMetadataValue(SchemaType schemaType); + @Nullable String getMetadataValue(SchemaType schemaType); Set getMetadataKeys(); diff --git a/exist-core/src/main/java/org/exist/security/SecurityManager.java b/exist-core/src/main/java/org/exist/security/SecurityManager.java index 03ec8dd705..7cf0686329 100644 --- a/exist-core/src/main/java/org/exist/security/SecurityManager.java +++ b/exist-core/src/main/java/org/exist/security/SecurityManager.java @@ -56,6 +56,8 @@ import org.exist.storage.txn.Txn; import org.exist.xmldb.XmldbURI; +import javax.annotation.Nullable; + /** * SecurityManager is responsible for managing users and groups. * @@ -89,8 +91,16 @@ public interface SecurityManager extends Configurable { void registerGroup(Group group); - Account getAccount(int id); + @Nullable Account getAccount(int id); + /** + * Returns true if an account of this name + * is known to the Security Manager. + * + * @param name the account name + * + * @return true if an account with the provided name is known. + */ boolean hasAccount(String name); Account addAccount(Account user) throws PermissionDeniedException, EXistException; @@ -104,18 +114,35 @@ public interface SecurityManager extends Configurable { boolean updateGroup(Group group) throws PermissionDeniedException, EXistException; - Account getAccount(String name); + @Nullable Account getAccount(String name); Group addGroup(DBBroker broker, Group group) throws PermissionDeniedException, EXistException; @Deprecated void addGroup(DBBroker broker, String group) throws PermissionDeniedException, EXistException; + /** + * Returns true if a group of this name + * is known to the Security Manager. + * + * @param name the group name + * + * @return true if a group with the provided name is known. + */ boolean hasGroup(String name); + + /** + * Returns true if a group of this name + * is known to the Security Manager. + * + * @param group the group + * + * @return true if a group with the provided name is known. + */ boolean hasGroup(Group group); - Group getGroup(String name); - Group getGroup(int gid); + @Nullable Group getGroup(String name); + @Nullable Group getGroup(int gid); boolean deleteGroup(String name) throws PermissionDeniedException, EXistException; diff --git a/exist-core/src/main/java/org/exist/security/internal/RealmImpl.java b/exist-core/src/main/java/org/exist/security/internal/RealmImpl.java index 48f821ab96..e7b0d0c979 100644 --- a/exist-core/src/main/java/org/exist/security/internal/RealmImpl.java +++ b/exist-core/src/main/java/org/exist/security/internal/RealmImpl.java @@ -1,4 +1,28 @@ /* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * * eXist-db Open Source Native XML Database * Copyright (C) 2001 The eXist-db Authors * @@ -24,6 +48,8 @@ import java.security.Principal; import java.util.*; import java.util.stream.Collectors; + +import com.evolvedbinary.j8fu.tuple.Tuple2; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.EXistException; @@ -33,7 +59,7 @@ import org.exist.config.ReferenceImpl; import org.exist.security.AXSchemaType; import org.exist.security.AbstractAccount; -import org.exist.security.AbstractPrincipal; +import org.exist.security.AbstractGroup; import org.exist.security.AbstractRealm; import org.exist.security.Account; import org.exist.security.AuthenticationException; @@ -49,6 +75,8 @@ import org.exist.storage.txn.Txn; import org.exist.xmldb.XmldbURI; +import javax.annotation.Nullable; + /** * @author Dmitriy Shabanov * @@ -172,8 +200,9 @@ public boolean deleteAccount(final Account account) throws PermissionDeniedExcep } usersByName.write2E(principalDb -> { - final AbstractAccount remove_account = (AbstractAccount)principalDb.get(account.getName()); - if(remove_account == null){ + @Nullable final Tuple2> removeAccountEntry = principalDb.get(account.getName()); + if (removeAccountEntry == null || removeAccountEntry._1 == PrincipalState.CREATING) { + // skip not existent or not yet persisted entries throw new IllegalArgumentException("No such account exists!"); } @@ -184,26 +213,28 @@ public boolean deleteAccount(final Account account) throws PermissionDeniedExcep throw new PermissionDeniedException("The '" + account.getName() + "' account is required by the system for correct operation, and you cannot delete it! You may be able to disable it instead."); } - try(final DBBroker broker = getDatabase().getBroker()) { + try (final DBBroker broker = getDatabase().getBroker()) { final Account user = broker.getCurrentSubject(); - if(!(account.getName().equals(user.getName()) || user.hasDbaRole()) ) { + if (!(account.getName().equals(user.getName()) || user.hasDbaRole()) ) { throw new PermissionDeniedException("You are not allowed to delete '" + account.getName() + "' user"); } - remove_account.setRemoved(true); - remove_account.setCollection(broker, collectionRemovedAccounts, XmldbURI.create(UUIDGenerator.getUUID()+".xml")); + final AbstractAccount removeAccount = (AbstractAccount) removeAccountEntry._2.get(); - try(final Txn txn = broker.continueOrBeginTransaction()) { - collectionAccounts.removeXMLResource(txn, broker, XmldbURI.create( remove_account.getName() + ".xml")); + removeAccount.setRemoved(true); + removeAccount.setCollection(broker, collectionRemovedAccounts, XmldbURI.create(UUIDGenerator.getUUID()+".xml")); + + try (final Txn txn = broker.continueOrBeginTransaction()) { + collectionAccounts.removeXMLResource(txn, broker, XmldbURI.create(removeAccount.getName() + ".xml")); txn.commit(); } catch(final Exception e) { LOG.warn(e.getMessage(), e); } - getSecurityManager().registerAccount(remove_account); - principalDb.remove(remove_account.getName()); + getSecurityManager().registerAccount(removeAccount); + principalDb.remove(removeAccount.getName()); } }); @@ -217,8 +248,9 @@ public boolean deleteGroup(final Group group) throws PermissionDeniedException, } groupsByName.write2E(principalDb -> { - final AbstractPrincipal remove_group = (AbstractPrincipal)principalDb.get(group.getName()); - if (remove_group == null) { + @Nullable final Tuple2> removeGroupEntry = principalDb.get(group.getName()); + if (removeGroupEntry == null || removeGroupEntry._1 == PrincipalState.CREATING) { + // skip not existent or not yet persisted entries throw new IllegalArgumentException("Group does '" + group.getName() + "' not exist!"); } @@ -231,14 +263,20 @@ public boolean deleteGroup(final Group group) throws PermissionDeniedException, final DBBroker broker = getDatabase().getActiveBroker(); final Subject subject = broker.getCurrentSubject(); - ((Group)remove_group).assertCanModifyGroup(subject); + final Group removeGroup = removeGroupEntry._2.get(); + + removeGroup.assertCanModifyGroup(subject); // check that this is not an active primary group final Optional isPrimaryGroupOf = usersByName.read(usersDb -> { - for(final Account account : usersDb.values()) { - final Group accountPrimaryGroup = account.getDefaultGroup(); - if (accountPrimaryGroup != null && accountPrimaryGroup.getId() == remove_group.getId()) { - return Optional.of(account.getName()); + for (final Tuple2> accountEntry : usersDb.values()) { + // skip not yet persisted entries + if (accountEntry._1 == PrincipalState.PERSISTENT) { + final Account account = accountEntry._2.get(); + final Group accountPrimaryGroup = account.getDefaultGroup(); + if (accountPrimaryGroup != null && accountPrimaryGroup.getId() == removeGroup.getId()) { + return Optional.of(account.getName()); + } } } return Optional.empty(); @@ -247,19 +285,19 @@ public boolean deleteGroup(final Group group) throws PermissionDeniedException, throw new PermissionDeniedException("Account '" + isPrimaryGroupOf.get() + "' still has '" + group.getName() + "' as their primary group!"); } - remove_group.setRemoved(true); - remove_group.setCollection(broker, collectionRemovedGroups, XmldbURI.create(UUIDGenerator.getUUID() + ".xml")); - try(final Txn txn = broker.continueOrBeginTransaction()) { + ((AbstractGroup) removeGroup).setRemoved(true); + ((AbstractGroup) removeGroup).setCollection(broker, collectionRemovedGroups, XmldbURI.create(UUIDGenerator.getUUID() + ".xml")); + try (final Txn txn = broker.continueOrBeginTransaction()) { - collectionGroups.removeXMLResource(txn, broker, XmldbURI.create(remove_group.getName() + ".xml" )); + collectionGroups.removeXMLResource(txn, broker, XmldbURI.create(removeGroup.getName() + ".xml" )); txn.commit(); } catch (final Exception e) { LOG.warn(e.getMessage(), e); } - getSecurityManager().registerGroup((Group)remove_group); - principalDb.remove(remove_group.getName()); + getSecurityManager().registerGroup(removeGroup); + principalDb.remove(removeGroup.getName()); }); return true; @@ -292,18 +330,24 @@ public Subject authenticate(final String accountName, Object credentials) throws @Override public List findUsernamesWhereUsernameStarts(final String prefix) { return usersByName.read(principalDb -> - principalDb.keySet() - .stream() - .filter(userName -> userName.startsWith(prefix)) - .collect(Collectors.toList()) + principalDb.entrySet() + .stream() + // skip not yet persisted entries + .filter(entry -> entry.getValue()._1 == PrincipalState.PERSISTENT) + .map(Map.Entry::getKey) + .filter(userName -> userName.startsWith(prefix)) + .collect(Collectors.toList()) ); } @Override public List findGroupnamesWhereGroupnameStarts(final String prefix) { return groupsByName.read(principalDb -> - principalDb.keySet() + principalDb.entrySet() .stream() + // skip not yet persisted entries + .filter(entry -> entry.getValue()._1 == PrincipalState.PERSISTENT) + .map(Map.Entry::getKey) .filter(groupName -> groupName.startsWith(prefix)) .collect(Collectors.toList()) ); @@ -312,8 +356,11 @@ public List findGroupnamesWhereGroupnameStarts(final String prefix) { @Override public Collection findGroupnamesWhereGroupnameContains(final String fragment) { return groupsByName.read(principalDb -> - principalDb.keySet() + principalDb.entrySet() .stream() + // skip not yet persisted entries + .filter(entry -> entry.getValue()._1 == PrincipalState.PERSISTENT) + .map(Map.Entry::getKey) .filter(groupName -> groupName.contains(fragment)) .collect(Collectors.toList()) ); @@ -321,19 +368,36 @@ public Collection findGroupnamesWhereGroupnameContains(final S @Override public List findAllGroupNames() { - return groupsByName.read(principalDb -> new ArrayList<>(principalDb.keySet())); + return groupsByName.read(principalDb -> + principalDb.entrySet() + .stream() + // skip not yet persisted entries + .filter(entry -> entry.getValue()._1 == PrincipalState.PERSISTENT) + .map(Map.Entry::getKey) + .collect(Collectors.toList()) + ); } @Override public List findAllUserNames() { - return usersByName.read(principalDb -> new ArrayList<>(principalDb.keySet())); + return usersByName.read(principalDb -> + principalDb.entrySet() + .stream() + // skip not yet persisted entries + .filter(entry -> entry.getValue()._1 == PrincipalState.PERSISTENT) + .map(Map.Entry::getKey) + .collect(Collectors.toList()) + ); } @Override public List findAllGroupMembers(final String groupName) { return usersByName.read(principalDb -> - principalDb.values() + principalDb.entrySet() .stream() + // skip not yet persisted entries + .filter(entry -> entry.getValue()._1 == PrincipalState.PERSISTENT) + .map(entry -> entry.getValue()._2.get()) .filter(account -> account.hasGroup(groupName)) .map(Principal::getName) .collect(Collectors.toList()) @@ -342,11 +406,40 @@ public List findAllGroupMembers(final String groupName) { @Override public List findUsernamesWhereNameStarts(final String startsWith) { - return Collections.emptyList(); //TODO at present exist users cannot have personal name details, used in LDAP realm + return usersByName.read(principalDb -> + principalDb.entrySet() + .stream() + // skip not yet persisted entries + .filter(entry -> entry.getValue()._1 == PrincipalState.PERSISTENT) + .map(entry -> entry.getValue()._2.get()) + .filter(account -> { + @Nullable final String fullName = account.getMetadataValue(AXSchemaType.FULLNAME); + return fullName != null && fullName.startsWith(startsWith); + }) + .map(Account::getName) + .collect(Collectors.toList()) + ); } @Override public List findUsernamesWhereNamePartStarts(final String startsWith) { - return Collections.emptyList(); //TODO at present exist users cannot have personal name details, used in LDAP realm + return usersByName.read(principalDb -> + principalDb.entrySet() + .stream() + // skip not yet persisted entries + .filter(entry -> entry.getValue()._1 == PrincipalState.PERSISTENT) + .map(entry -> entry.getValue()._2.get()) + .filter(account -> { + @Nullable final String firstName = account.getMetadataValue(AXSchemaType.FIRSTNAME); + if (firstName != null && firstName.startsWith(startsWith)) { + return true; + } + + @Nullable final String lastName = account.getMetadataValue(AXSchemaType.LASTNAME); + return lastName != null && lastName.startsWith(startsWith); + }) + .map(Account::getName) + .collect(Collectors.toList()) + ); } } diff --git a/exist-core/src/main/java/org/exist/security/internal/SecurityManagerImpl.java b/exist-core/src/main/java/org/exist/security/internal/SecurityManagerImpl.java index e2e53e54fd..c2966ab240 100644 --- a/exist-core/src/main/java/org/exist/security/internal/SecurityManagerImpl.java +++ b/exist-core/src/main/java/org/exist/security/internal/SecurityManagerImpl.java @@ -1,4 +1,28 @@ /* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * * eXist-db Open Source Native XML Database * Copyright (C) 2001 The eXist-db Authors * @@ -27,6 +51,7 @@ import net.jcip.annotations.ThreadSafe; import org.exist.scheduler.JobDescription; import org.exist.security.AbstractRealm; + import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -75,6 +100,10 @@ import org.quartz.JobExecutionContext; import org.quartz.SimpleTrigger; +import javax.annotation.Nullable; + +import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple; + /** * SecurityManager is responsible for managing users and groups. * @@ -117,7 +146,7 @@ public class SecurityManagerImpl implements SecurityManager, BrokerPoolService { private static final String authenticationEntryPoint = "/authentication/login"; private RealmImpl defaultRealm; - + @ConfigurationFieldAsElement("realm") @ConfigurationFieldClassMask("org.exist.security.realm.%1$s.%2$sRealm") private List realms = new ArrayList<>(); @@ -282,8 +311,8 @@ public boolean deleteAccount(final Account account) throws PermissionDeniedExcep } @Override - public Account getAccount(final String name) { - for(final Realm realm : realms) { + public @Nullable Account getAccount(final String name) { + for (final Realm realm : realms) { final Account account = realm.getAccount(name); if (account != null) { return account; @@ -297,7 +326,7 @@ public Account getAccount(final String name) { } @Override - public final Account getAccount(final int id) { + public @Nullable final Account getAccount(final int id) { return usersById.read(principalDb -> principalDb.get(id)); } @@ -317,10 +346,10 @@ public boolean hasGroup(final Group group) { } @Override - public Group getGroup(final String name) { - for(final Realm realm : realms) { + public @Nullable Group getGroup(final String name) { + for (final Realm realm : realms) { final Group group = realm.getGroup(name); - if(group != null) { + if (group != null) { return group; } } @@ -328,7 +357,7 @@ public Group getGroup(final String name) { } @Override - public final Group getGroup(final int id) { + public @Nullable final Group getGroup(final int id) { return groupsById.read(principalDb -> principalDb.get(id)); } @@ -342,7 +371,7 @@ public boolean hasAdminPrivileges(final Account user) { @Override public boolean hasAccount(final String name) { for(final Realm realm : realms) { - if(realm.hasAccount(name)) { + if (realm.hasAccount(name)) { return true; } } @@ -518,16 +547,16 @@ public void addGroup(final DBBroker broker, final String name) throws Permission @Override public Group addGroup(final DBBroker broker, final Group group) throws PermissionDeniedException, EXistException { - if(group.getRealmId() == null) { - throw new ConfigurationException("Group must have realm id."); + if (group.getRealmId() == null) { + throw new ConfigurationException("Group must have a Realm ID."); } - if(group.getName() == null || group.getName().isEmpty()) { - throw new ConfigurationException("Group must have name."); + if (group.getName() == null || group.getName().isEmpty()) { + throw new ConfigurationException("Group must have a name."); } final int id; - if(group.getId() != Group.UNDEFINED_ID) { + if (group.getId() != Group.UNDEFINED_ID) { id = group.getId(); } else { id = groupsById.getNextPrincipalId(); @@ -537,14 +566,19 @@ public Group addGroup(final DBBroker broker, final Group group) throws Permissio if (registeredRealm.hasGroupLocal(group.getName())) { throw new ConfigurationException("The group '" + group.getName() + "' at realm '" + group.getRealmId() + "' already exists."); } - - final GroupImpl newGroup = new GroupImpl(broker, registeredRealm, id, group.getName(), group.getManagers()); - for(final SchemaType metadataKey : group.getMetadataKeys()) { - final String metadataValue = group.getMetadataValue(metadataKey); - newGroup.setMetadataValue(metadataKey, metadataValue); - } - try(final ManagedLock lock = ManagedLock.acquire(groupLocks.getLock(newGroup), LockMode.WRITE_LOCK)) { + try (final ManagedLock lock = ManagedLock.acquire(groupLocks.getLock(id), LockMode.WRITE_LOCK)) { + + // NOTE(AR) set a flag to indicate we are creating the Group, helps prevent a loop between the Realm and the Configurator classes + registeredRealm.registerGroupCreation(group.getName()); + + // NOTE(AR) create the new group object - Configurator will write an XML file into the database here + final GroupImpl newGroup = new GroupImpl(broker, registeredRealm, id, group.getName(), group.getManagers()); + for(final SchemaType metadataKey : group.getMetadataKeys()) { + final String metadataValue = group.getMetadataValue(metadataKey); + newGroup.setMetadataValue(metadataKey, metadataValue); + } + registerGroup(newGroup); registeredRealm.registerGroup(newGroup); @@ -563,16 +597,16 @@ public final Account addAccount(final Account account) throws PermissionDeniedE @Override public final Account addAccount(final DBBroker broker, final Account account) throws PermissionDeniedException, EXistException{ - if(account.getRealmId() == null) { - throw new ConfigurationException("Account must have realm id."); + if (account.getRealmId() == null) { + throw new ConfigurationException("Account must have a Realm ID."); } - if(account.getName() == null || account.getName().isEmpty()) { - throw new ConfigurationException("Account must have name."); + if (account.getName() == null || account.getName().isEmpty()) { + throw new ConfigurationException("Account must have a name."); } final int id; - if(account.getId() != Account.UNDEFINED_ID) { + if (account.getId() != Account.UNDEFINED_ID) { id = account.getId(); } else { id = usersById.getNextPrincipalId(); @@ -583,8 +617,13 @@ public final Account addAccount(final DBBroker broker, final Account account) th throw new ConfigurationException("The account '" + account.getName() + "' at realm '" + account.getRealmId() + "' already exists."); } - final AccountImpl newAccount = new AccountImpl(broker, registeredRealm, id, account); - try (final ManagedLock lock = ManagedLock.acquire(accountLocks.getLock(newAccount), LockMode.WRITE_LOCK)) { + try (final ManagedLock lock = ManagedLock.acquire(accountLocks.getLock(id), LockMode.WRITE_LOCK)) { + // NOTE(AR) set a flag to indicate we are creating the Account, helps prevent a loop between the Realm and the Configurator classes + registeredRealm.registerAccountCreation(account.getName()); + + // NOTE(AR) create the new account object - Configurator will write an XML file into the database here + final AccountImpl newAccount = new AccountImpl(broker, registeredRealm, id, account); + registerAccount(newAccount); registeredRealm.registerAccount(newAccount); @@ -899,6 +938,10 @@ private static class PrincipalLocks { public ReadWriteLock getLock(final T principal) { return lockStripes.get(principal.getId()); } + + public ReadWriteLock getLock(final int principalId) { + return lockStripes.get(principalId); + } } @ThreadSafe diff --git a/exist-core/src/main/java/org/exist/security/management/AccountsManagement.java b/exist-core/src/main/java/org/exist/security/management/AccountsManagement.java index 625c6b3c0f..f57329c62a 100644 --- a/exist-core/src/main/java/org/exist/security/management/AccountsManagement.java +++ b/exist-core/src/main/java/org/exist/security/management/AccountsManagement.java @@ -1,4 +1,28 @@ /* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * * eXist-db Open Source Native XML Database * Copyright (C) 2001 The eXist-db Authors * @@ -26,17 +50,35 @@ import org.exist.security.PermissionDeniedException; import org.exist.security.Account; +import javax.annotation.Nullable; + /** * @author Dmitriy Shabanov - * */ public interface AccountsManagement { Account addAccount(Account account) throws PermissionDeniedException, EXistException, ConfigurationException; - Account getAccount(String name); + @Nullable Account getAccount(String name); + /** + * Returns true if an account of this name + * is known to the Security Manager. + * + * @param account the account + * + * @return true if an account with the provided name is known. + */ boolean hasAccount(Account account); + + /** + * Returns true if an account of this name + * is known to the Security Manager. + * + * @param name the account name + * + * @return true if an account with the provided name is known. + */ boolean hasAccount(String name); boolean hasAccountLocal(Account account); diff --git a/exist-core/src/main/java/org/exist/security/management/GroupsManagement.java b/exist-core/src/main/java/org/exist/security/management/GroupsManagement.java index d66df882aa..357159c3ef 100644 --- a/exist-core/src/main/java/org/exist/security/management/GroupsManagement.java +++ b/exist-core/src/main/java/org/exist/security/management/GroupsManagement.java @@ -1,4 +1,28 @@ /* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * * eXist-db Open Source Native XML Database * Copyright (C) 2001 The eXist-db Authors * @@ -27,17 +51,35 @@ import org.exist.security.PermissionDeniedException; import org.exist.storage.DBBroker; +import javax.annotation.Nullable; + /** * @author Dmitriy Shabanov - * */ public interface GroupsManagement { Group addGroup(DBBroker broker, Group group) throws PermissionDeniedException, EXistException, ConfigurationException; - Group getGroup(String name); + @Nullable Group getGroup(String name); + /** + * Returns true if a group of this name + * is known to the Security Manager. + * + * @param group the group + * + * @return true if a group with the provided name is known. + */ boolean hasGroup(Group group); + + /** + * Returns true if a group of this name + * is known to the Security Manager. + * + * @param name the group name + * + * @return true if a group with the provided name is known. + */ boolean hasGroup(String name); boolean hasGroupLocal(Group group); diff --git a/exist-distribution/pom.xml b/exist-distribution/pom.xml index 71a69650af..fcad77dbc0 100644 --- a/exist-distribution/pom.xml +++ b/exist-distribution/pom.xml @@ -289,12 +289,6 @@ - - ${project.groupId} - exist-security-activedirectory - ${project.version} - runtime - - - 4.0.0 - - - xyz.elemental.fork.org.exist-db - exist-security - 7.6.0-SNAPSHOT - .. - - - exist-security-activedirectory - jar - - eXist-db Active Directory Security Module - eXist-db NoSQL Database Active Directory Security Module - - - scm:git:https://github.com/evolvedbinary/elemental.git - scm:git:https://github.com/evolvedbinary/elemental.git - scm:git:https://github.com/evolvedbinary/elemental.git - HEAD - - - - - - com.mycila - license-maven-plugin - - - - - -
${project.parent.relativePath}/../../elemental-parent/elemental-LGPL-21-ONLY-license.template.txt
- - pom.xml - src/** - -
- - - - -
${project.parent.relativePath}/../../elemental-parent/elemental-LGPL-21-ONLY-license.template.txt
- -
${project.parent.relativePath}/../../exist-parent/existdb-LGPL-21-license.template.txt
-
- - pom.xml - src/test/resources/log4j2.xml - -
- - - -
${project.parent.relativePath}/../../exist-parent/existdb-LGPL-21-license.template.txt
- - pom.xml - src/test/resources/log4j2.xml - -
-
- - - ${project.parent.relativePath}/../../exist-parent/xquery-license-style.xml - - -
-
-
-
- - - - xyz.elemental.fork.org.exist-db - exist-core - ${project.version} - - - xyz.elemental.fork.org.exist-db - exist-security-ldap - ${project.version} - - - - org.apache.logging.log4j - log4j-api - - - - commons-io - commons-io - test - - - - - junit - junit - test - - - - -
\ No newline at end of file diff --git a/extensions/security/activedirectory/src/main/java/org/exist/security/realm/activedirectory/ActiveDirectoryRealm.java b/extensions/security/activedirectory/src/main/java/org/exist/security/realm/activedirectory/ActiveDirectoryRealm.java deleted file mode 100644 index 21d825ad99..0000000000 --- a/extensions/security/activedirectory/src/main/java/org/exist/security/realm/activedirectory/ActiveDirectoryRealm.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * eXist-db Open Source Native XML Database - * Copyright (C) 2001 The eXist-db Authors - * - * info@exist-db.org - * http://www.exist-db.org - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ -package org.exist.security.realm.activedirectory; - -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.SearchControls; -import javax.naming.directory.SearchResult; -import javax.naming.ldap.LdapContext; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.exist.config.Configuration; -import org.exist.config.annotation.*; -import org.exist.security.AuthenticationException; -import org.exist.security.Subject; -import org.exist.security.AbstractAccount; -import org.exist.security.internal.SecurityManagerImpl; -import org.exist.security.internal.SubjectAccreditedImpl; -import org.exist.security.internal.aider.UserAider; -import org.exist.security.realm.ldap.LDAPRealm; -import org.exist.security.realm.ldap.LdapContextFactory; -import org.exist.storage.DBBroker; - -/** - * @author Dmitriy Shabanov - * - */ -@ConfigurationClass("realm") //TODO: id = ActiveDirectory -public class ActiveDirectoryRealm extends LDAPRealm { - - private final static Logger LOG = LogManager.getLogger(LDAPRealm.class); - - @ConfigurationFieldAsAttribute("id") - public static String ID = "ActiveDirectory"; - - @ConfigurationFieldAsAttribute("version") - public final static String version = "1.0"; - - public ActiveDirectoryRealm(SecurityManagerImpl sm, Configuration config) { - super(sm, config); - } - - @Override - protected LdapContextFactory ensureContextFactory() { - if (this.ldapContextFactory == null) { - - if (LOG.isDebugEnabled()) { - LOG.debug("No LdapContextFactory specified - creating a default instance."); - } - - LdapContextFactory factory = new ContextFactory(configuration); - - this.ldapContextFactory = factory; - } - return this.ldapContextFactory; - } - - /* - * (non-Javadoc) - * - * @see org.exist.security.Realm#getId() - */ - @Override - public String getId() { - return ID; - } - - /* - * (non-Javadoc) - * - * @see org.exist.security.Realm#authenticate(java.lang.String, - * java.lang.Object) - */ - @Override - public Subject authenticate(final String username, Object credentials) throws AuthenticationException { - - String returnedAtts[] = { "sn", "givenName", "mail" }; - String searchFilter = "(&(objectClass=user)(sAMAccountName=" + username + "))"; - - // Create the search controls - SearchControls searchCtls = new SearchControls(); - searchCtls.setReturningAttributes(returnedAtts); - - // Specify the search scope - searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE); - - LdapContext ctxGC = null; - boolean ldapUser = false; - - try { - ctxGC = ensureContextFactory().getLdapContext(username, String.valueOf(credentials)); - - // Search objects in GC using filters - NamingEnumeration answer = ctxGC.search(((ContextFactory) ensureContextFactory()).getSearchBase(), searchFilter, searchCtls); - - while (answer.hasMoreElements()) { - SearchResult sr = answer.next(); - Attributes attrs = sr.getAttributes(); - Map amap = null; - if (attrs != null) { - amap = new HashMap<>(); - NamingEnumeration ne = attrs.getAll(); - while (ne.hasMore()) { - Attribute attr = ne.next(); - amap.put(attr.getID(), attr.get()); - ldapUser = true; - } - ne.close(); - } - } - } catch (NamingException e) { - e.printStackTrace(); - throw new AuthenticationException( - AuthenticationException.UNNOWN_EXCEPTION, - e.getMessage()); - } - - if (ldapUser) { - AbstractAccount account = (AbstractAccount) getAccount(username); - if (account == null) { - try(final DBBroker broker = getDatabase().get(Optional.of(getSecurityManager().getSystemSubject()))) { - //perform as SYSTEM user - account = (AbstractAccount) getSecurityManager().addAccount(new UserAider(ID, username)); - } catch (Exception e) { - throw new AuthenticationException( - AuthenticationException.UNNOWN_EXCEPTION, - e.getMessage(), e); - } - } - - return new SubjectAccreditedImpl(account, ctxGC); - } - - return null; - } -} \ No newline at end of file diff --git a/extensions/security/activedirectory/src/main/java/org/exist/security/realm/activedirectory/ContextFactory.java b/extensions/security/activedirectory/src/main/java/org/exist/security/realm/activedirectory/ContextFactory.java deleted file mode 100644 index 5ae55bf401..0000000000 --- a/extensions/security/activedirectory/src/main/java/org/exist/security/realm/activedirectory/ContextFactory.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * eXist-db Open Source Native XML Database - * Copyright (C) 2001 The eXist-db Authors - * - * info@exist-db.org - * http://www.exist-db.org - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ -package org.exist.security.realm.activedirectory; - -import org.exist.config.Configuration; -import org.exist.config.annotation.ConfigurationClass; -import org.exist.config.annotation.ConfigurationFieldAsElement; -import org.exist.security.realm.ldap.LdapContextFactory; - -/** - * @author Dmitriy Shabanov - * - */ -@ConfigurationClass("context") -public class ContextFactory extends LdapContextFactory { - - @ConfigurationFieldAsElement("domain") - protected String domain = null; - - @ConfigurationFieldAsElement("searchBase") - private String searchBase = null; - - protected ContextFactory(Configuration config) { - super(config); - -// if (domain == null) { -// //throw error? -// domain = ""; -// } -// -// principalPatternFormat = new MessageFormat("{0}@"+domain); - } - - public String getSearchBase() { - return searchBase; - } - - public String getDomain() { - return domain; - } - -} diff --git a/extensions/security/activedirectory/src/test/java/org/exist/security/realm/activedirectory/ActiveDirectoryRealmTest.java b/extensions/security/activedirectory/src/test/java/org/exist/security/realm/activedirectory/ActiveDirectoryRealmTest.java deleted file mode 100644 index 52678033e1..0000000000 --- a/extensions/security/activedirectory/src/test/java/org/exist/security/realm/activedirectory/ActiveDirectoryRealmTest.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * eXist-db Open Source Native XML Database - * Copyright (C) 2001 The eXist-db Authors - * - * info@exist-db.org - * http://www.exist-db.org - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ -package org.exist.security.realm.activedirectory; - -import static org.junit.Assert.*; - -import java.io.InputStream; -import java.nio.charset.StandardCharsets; - -import org.exist.config.Configuration; -import org.exist.config.Configurator; -import org.exist.security.AuthenticationException; -import org.exist.security.Subject; -import org.apache.commons.io.input.UnsynchronizedByteArrayInputStream; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Ignore; -import org.junit.Test; - -/** - * @author Dmitriy Shabanov - * - */ -public class ActiveDirectoryRealmTest { - - private static String config = - "" + - " " + -// " principalPattern='CN={0},OU=users,DC=bnb,DC=bulungur,dc=nb' " + -// " searchBase='ou=users,dc=bnb,dc=bulungur,dc=nb' " + - " ldap://fake.com:389" + -// " strong" + - " " + - ""; - - private static ActiveDirectoryRealm realm; - - /** - * @throws java.lang.Exception - */ - @BeforeClass - public static void setUpBeforeClass() throws Exception { - InputStream is = new UnsynchronizedByteArrayInputStream(config.getBytes(StandardCharsets.UTF_8)); - - Configuration config = Configurator.parse(is); - - realm = new ActiveDirectoryRealm(null, config); - } - - /** - * @throws java.lang.Exception - */ - @AfterClass - public static void tearDownAfterClass() throws Exception { - } - - /** - * Test method for {@link org.exist.security.realm.activedirectory.ActiveDirectoryRealm#authenticate(java.lang.String, java.lang.Object)}. - */ - @Ignore - @Test - public void testAuthenticate() { - Subject currentUser = null; - try { - currentUser = realm.authenticate("accounter@fake.com", "password"); - } catch (AuthenticationException e) { - e.printStackTrace(); - fail(e.getMessage()); - } - - assertNotNull(currentUser); - } - -} diff --git a/extensions/security/ldap/README.md b/extensions/security/ldap/README.md index 818b248680..ae0436e389 100644 --- a/extensions/security/ldap/README.md +++ b/extensions/security/ldap/README.md @@ -1,36 +1,128 @@ -To enable LDAP authentication you need to make sure that the file /db/system/security/config.xml content something similar to that below +# LDAP Security Realm for Elemental - +The LDAP Security Realm allows Elemental to obtain authentication and user and group information from LDAP. + +To enable the LDAP Security Realm, you need to make sure that the security configuration `/db/system/security/config.xml` within +the database contains a definition for th LDAP Realm and configuration that matches your LDAP server's schemas. + +## NIS Example + +The following is an example of an LDAP Security Realm configuration for connecting to an LDAP Server that provides the NIS Schema. +This has been tested with [Apache Directory Server](https://directory.apache.org/). + +```xml + + + ... + + - ldap://directory.mydomain.com:389 - ... - ... + simple + true + ldaps://gb.myorg.com:389 + gb.myorg.com + uid={0},ou=Users,dc=gb,dc=myorg,dc=com - ou=department,dc=directory,dc=mydomain,dc=com - some-ldap-user - some-ldap-password - - (&(objectClass=user)(sAMAccountName=${account-name})) - ... - .. - .... - .... - dc=gb,dc=myorg,dc=com + some-username + some-password + + + objectClass=inetOrgPerson + uidNumber + uid + uid + gidNumber + + givenName + sn + displayName + preferredLanguage + mail + + - (&(objectClass=group)(sAMAccountName=${group-name})) - ... - .. - .... - .... + objectClass=posixGroup + gidNumber + cn + memberUid + + description + + + some-other-group-1 + some-other-group-2 + - ... + + + ldap-users + + - ... + +``` + +## Microsoft Active Directory Example + +The following is an example of an LDAP Security Realm configuration for connecting to Microsoft Active Directory. + +```xml + -url - the URL to your LDAP directory server. -base - the LDAP base to use when resolving users and groups \ No newline at end of file + ... + + + + simple + true + sAMAccountName={0},ou=Users,dc=gb,dc=myorg,dc=com + ldaps://gb.myorg.com:636 + gb.myorg.com + + dc=gb,dc=myorg,dc=com + some-username + some-password + + + (&(objectClass=user)(memberof=cn=some-group,ou=Groups,dc=gb,dc=myorg,dc=com)) + objectSid + sAMAccountName + distinguishedName + primaryGroupID + memberOf + + mail + givenName + sn + name + + + + objectClass=group + objectSid + sAMAccountName + distinguishedName + member + + description + + + Domain Users + some-group + + + + + + ad-users + + + + + + +``` \ No newline at end of file diff --git a/extensions/security/ldap/ldap-realm.xsd b/extensions/security/ldap/ldap-realm.xsd index ef40d485ca..d88e178d9d 100644 --- a/extensions/security/ldap/ldap-realm.xsd +++ b/extensions/security/ldap/ldap-realm.xsd @@ -1,6 +1,30 @@
${project.parent.relativePath}/../../exist-parent/existdb-LGPL-21-license.template.txt
+ ldap-realm.xsd pom.xml - src/main/java/org/exist/security/realm/ldap/LdapContextFactory.java + src/test/** + src/main/java/org/exist/security/realm/ldap/AbstractLDAPSearchPrincipal.java + src/main/java/org/exist/security/realm/ldap/LDAPContextFactory.java + src/main/java/org/exist/security/realm/ldap/LDAPRealm.java + src/main/java/org/exist/security/realm/ldap/LDAPSearchAccount.java + src/main/java/org/exist/security/realm/ldap/LDAPSearchAttributeKey.java + src/main/java/org/exist/security/realm/ldap/LDAPSearchContext.java + src/main/java/org/exist/security/realm/ldap/LDAPSearchGroup.java + src/main/java/org/exist/security/realm/ldap/LDAPTransformationContext.java + src/main/java/org/exist/security/realm/ldap/LDAPUtils.java + src/main/java/org/exist/security/realm/ldap/SearchAttribute.java @@ -141,12 +181,6 @@ log4j-api - - commons-io - commons-io - test - - com.evolvedbinary.j8fu j8fu @@ -159,8 +193,49 @@ - junit - junit + org.junit.jupiter + junit-jupiter-api + test + + + + xyz.elemental + elemental-media-type-api + ${project.version} + test + + + + com.evolvedbinary.thirdparty.xml-apis + xml-apis + test + + + + org.apache.directory.server + apacheds-test-framework + ${apache.ds.version} + test + + + + org.apache.directory.server + apacheds-server-annotations + ${apache.ds.version} + test + + + + org.apache.directory.server + apacheds-protocol-ldap + ${apache.ds.version} + test + + + + org.apache.directory.server + apacheds-core-annotations + ${apache.ds.version} test diff --git a/extensions/security/ldap/src/main/java/org/exist/security/realm/TransformationContext.java b/extensions/security/ldap/src/main/java/org/exist/security/realm/TransformationContext.java index e993f4d897..4fbd69af5e 100644 --- a/extensions/security/ldap/src/main/java/org/exist/security/realm/TransformationContext.java +++ b/extensions/security/ldap/src/main/java/org/exist/security/realm/TransformationContext.java @@ -24,8 +24,9 @@ import java.util.List; /** - * @author aretter + * @author Adam Retter. */ public interface TransformationContext { List getAdditionalGroups(); + List getAdditionalGroupManagers(); } diff --git a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/AbstractLDAPSearchPrincipal.java b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/AbstractLDAPSearchPrincipal.java index b935352adb..98403aec2c 100644 --- a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/AbstractLDAPSearchPrincipal.java +++ b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/AbstractLDAPSearchPrincipal.java @@ -1,4 +1,28 @@ /* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * * eXist-db Open Source Native XML Database * Copyright (C) 2001 The eXist-db Authors * @@ -21,6 +45,7 @@ */ package org.exist.security.realm.ldap; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -32,6 +57,10 @@ import org.exist.config.annotation.ConfigurationClass; import org.exist.config.annotation.ConfigurationFieldAsElement; import org.exist.security.AXSchemaType; +import org.exist.security.EXistSchemaType; +import org.exist.security.SchemaType; + +import javax.annotation.Nullable; /** * @author aretter @@ -68,16 +97,30 @@ public String getSearchAttribute(final LDAPSearchAttributeKey ldapSearchAttribut return searchAttributes.get(ldapSearchAttributeKey.getKey()); } - public String getMetadataSearchAttribute(final AXSchemaType axSchemaType) { - return metadataSearchAttributes.get(axSchemaType.getNamespace()); + public String getMetadataSearchAttribute(final SchemaType schemaType) { + return metadataSearchAttributes.get(schemaType.getNamespace()); } - - public Set getMetadataSearchAttributeKeys() { - final Set metadataSearchAttributeKeys = new HashSet<>(); + public Set getMetadataSearchAttributeKeys() { + @Nullable Set metadataSearchAttributeKeys = null; for (final String key : metadataSearchAttributes.keySet()) { - metadataSearchAttributeKeys.add(AXSchemaType.valueOfNamespace(key)); + @Nullable SchemaType value = EXistSchemaType.valueOfNamespace(key); + if (value == null) { + value = AXSchemaType.valueOfNamespace(key); + } + + if (value != null) { + if (metadataSearchAttributeKeys == null) { + metadataSearchAttributeKeys = new HashSet<>(metadataSearchAttributes.size()); + } + metadataSearchAttributeKeys.add(value); + } } + + if (metadataSearchAttributeKeys == null) { + return Collections.emptySet(); + } + return metadataSearchAttributeKeys; } @@ -98,33 +141,4 @@ public LDAPPrincipalBlackList getBlackList() { public LDAPPrincipalWhiteList getWhiteList() { return whiteList; } - - public enum LDAPSearchAttributeKey { - NAME("name"), - DN("dn"), - MEMBER_OF("memberOf"), - MEMBER("member"), - PRIMARY_GROUP_TOKEN("primaryGroupToken"), - PRIMARY_GROUP_ID("primaryGroupID"), - OBJECT_SID("objectSid"); - - private final String key; - - LDAPSearchAttributeKey(final String key) { - this.key = key; - } - - public String getKey() { - return key; - } - - public static LDAPSearchAttributeKey valueOfKey(final String key) { - for (final LDAPSearchAttributeKey ldapSearchAttributeKey : LDAPSearchAttributeKey.values()) { - if (ldapSearchAttributeKey.getKey().equals(key)) { - return ldapSearchAttributeKey; - } - } - return null; - } - } } diff --git a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LdapContextFactory.java b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPContextFactory.java similarity index 76% rename from extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LdapContextFactory.java rename to extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPContextFactory.java index 232bbfb07a..60e3ed4b86 100644 --- a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LdapContextFactory.java +++ b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPContextFactory.java @@ -48,6 +48,7 @@ import java.text.MessageFormat; import java.util.Hashtable; import java.util.Map; +import javax.annotation.Nullable; import javax.naming.Context; import javax.naming.NamingException; import javax.naming.ldap.InitialLdapContext; @@ -61,27 +62,55 @@ import org.exist.config.annotation.ConfigurationClass; import org.exist.config.annotation.ConfigurationFieldAsElement; +import static org.exist.security.realm.ldap.LDAPUtils.formatUsername; import static org.exist.util.StringUtil.isNullOrEmptyOrWs; /** * @author Dmitriy Shabanov */ @ConfigurationClass("context") -public class LdapContextFactory implements Configurable { +public class LDAPContextFactory implements Configurable { - private static final Logger LOG = LogManager.getLogger(LdapContextFactory.class); + private static final Logger LOG = LogManager.getLogger(LDAPContextFactory.class); private static final String SUN_CONNECTION_POOLING_PROPERTY = "com.sun.jndi.ldap.connect.pool"; @ConfigurationFieldAsElement("authentication") protected String authentication = "simple"; + /** + * When set to true, the LDAP environment property + * {@code com.sun.jndi.ldap.trace.ber} will be + * set, and dumps of incoming and outgoing LDAP ASN.1 BER + * packets will be written to Standard Error. + */ + @ConfigurationFieldAsElement("trace-ber") + private final boolean traceBer = false; + + /** + * When set to true, LDAPS will be used + * instead of LDAP to access the LDAP server. + */ @ConfigurationFieldAsElement("use-ssl") private final boolean ssl = false; + /** + * If present, the described format is used for formatting unqualified usernames. + * + * For example, given: + *
{@code
+     *      uid={0},ou=Users,dc=gb,dc=myorg,dc=com
+     *      user1
+     * }
+     *
+ * + * The default username when sent to the LDAP Server will be reformatted as: {@code uid=user1,ou=Users,dc=gb,dc=myorg,dc=com}. + * + * If the username is already fully-qualified, then this has no effect. + */ @ConfigurationFieldAsElement("principal-pattern") - protected String principalPattern = null; - protected MessageFormat principalPatternFormat; + @Nullable protected String principalPattern = null; + @Nullable protected MessageFormat principalPatternFormat; @ConfigurationFieldAsElement("url") protected String url = null; @@ -91,11 +120,11 @@ public class LdapContextFactory implements Configurable { protected String contextFactoryClassName = "com.sun.jndi.ldap.LdapCtxFactory"; - protected String systemUsername = null; - - protected String systemPassword = null; - - private boolean usePooling = true; + /** + * When set to true LDAP connection pooling will be used. + */ + @ConfigurationFieldAsElement("connection-pooling") + private boolean usePooling = false; private Configuration configuration = null; @@ -105,23 +134,18 @@ public class LdapContextFactory implements Configurable { @ConfigurationFieldAsElement("transformation") private LDAPTransformationContext realmTransformation; - public LdapContextFactory(final Configuration config) { + public LDAPContextFactory(final Configuration config) { configuration = Configurator.configure(this, config); if (principalPattern != null) { principalPatternFormat = new MessageFormat(principalPattern); } } - public LdapContext getSystemLdapContext() throws NamingException { - return getLdapContext(systemUsername, systemPassword); - } - public LdapContext getLdapContext(final String username, final String password) throws NamingException { return getLdapContext(username, password, null); } public LdapContext getLdapContext(String username, final String password, final Map additionalEnv) throws NamingException { - if (url == null) { throw new IllegalStateException("An LDAP URL must be specified of the form ldap://:"); } @@ -130,9 +154,8 @@ public LdapContext getLdapContext(String username, final String password, final throw new IllegalStateException("Password for LDAP authentication may not be empty."); } - if (username != null && principalPattern != null) { - username = principalPatternFormat.format(new String[]{username}); - } + // ensure the username is qualified if necessary + username = formatUsername(username, principalPatternFormat); final Hashtable env = new Hashtable<>(); @@ -152,14 +175,15 @@ public LdapContext getLdapContext(String username, final String password, final env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactoryClassName); env.put(Context.PROVIDER_URL, url); - //Absolutely nessecary for working with Active Directory + //Absolutely necessary for working with Active Directory env.put("java.naming.ldap.attributes.binary", "objectSid"); // the following is helpful in debugging errors - //env.put("com.sun.jndi.ldap.trace.ber", System.err); + if (traceBer) { + env.put("com.sun.jndi.ldap.trace.ber", System.err); + } - // Only pool connections for system contexts - if (usePooling && username != null && username.equals(systemUsername)) { + if (usePooling && username != null) { // Enable connection pooling env.put(SUN_CONNECTION_POOLING_PROPERTY, "true"); } diff --git a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPRealm.java b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPRealm.java index 0da40c2028..931bf79302 100644 --- a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPRealm.java +++ b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPRealm.java @@ -1,4 +1,28 @@ /* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * * eXist-db Open Source Native XML Database * Copyright (C) 2001 The eXist-db Authors * @@ -22,11 +46,14 @@ package org.exist.security.realm.ldap; import java.lang.reflect.Field; -import java.util.AbstractMap.SimpleEntry; +import java.util.Arrays; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.AbstractMap.SimpleEntry; +import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.naming.NamingEnumeration; import javax.naming.NamingException; @@ -37,8 +64,9 @@ import javax.naming.directory.SearchResult; import javax.naming.ldap.LdapContext; +import com.evolvedbinary.j8fu.function.BiFunctionE; +import com.evolvedbinary.j8fu.function.Function3E; import com.evolvedbinary.j8fu.tuple.Tuple2; -import com.evolvedbinary.j8fu.function.BiFunction3E; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.EXistException; @@ -57,11 +85,11 @@ import org.exist.security.internal.SubjectAccreditedImpl; import org.exist.security.internal.aider.GroupAider; import org.exist.security.internal.aider.UserAider; -import org.exist.security.realm.ldap.AbstractLDAPSearchPrincipal.LDAPSearchAttributeKey; import org.exist.storage.DBBroker; import org.exist.storage.txn.Txn; import static com.evolvedbinary.j8fu.tuple.Tuple.Tuple; +import static org.exist.security.realm.ldap.LDAPUtils.*; /** * @author Dmitriy Shabanov @@ -72,6 +100,8 @@ public class LDAPRealm extends AbstractRealm { private static final Logger LOG = LogManager.getLogger(LDAPRealm.class); + private static final Pattern SID_PATTERN = Pattern.compile("S-[0-9]+-[0-9]+(-[0-9]+)+"); + @ConfigurationFieldAsAttribute("id") public static String ID = "LDAP"; @@ -82,18 +112,18 @@ public class LDAPRealm extends AbstractRealm { private boolean principalsAreCaseInsensitive; @ConfigurationFieldAsElement("context") - protected LdapContextFactory ldapContextFactory; + protected LDAPContextFactory ldapContextFactory; public LDAPRealm(final SecurityManagerImpl sm, final Configuration config) { super(sm, config); } - protected LdapContextFactory ensureContextFactory() { + protected LDAPContextFactory ensureContextFactory() { if (this.ldapContextFactory == null) { if (LOG.isDebugEnabled()) { LOG.debug("No LdapContextFactory specified - creating a default instance."); } - this.ldapContextFactory = new LdapContextFactory(configuration); + this.ldapContextFactory = new LDAPContextFactory(configuration); } return this.ldapContextFactory; } @@ -108,21 +138,30 @@ public void start(final DBBroker broker, final Txn transaction) throws EXistExce super.start(broker, transaction); } - private String ensureCase(final String username) { - if (username == null) { + /** + * When principals-are-case-insensitive="true" + * is set in the Realm security config then + * the provided principal will be converted + * to lower case. + * + * @param principal the principal + * + * @return the principal which may have been lower-cased. + */ + private @Nullable String ensureCase(@Nullable final String principal) { + if (principal == null) { return null; } if (principalsAreCaseInsensitive) { - return username.toLowerCase(); + return principal.toLowerCase(); } - return username; + return principal; } @Override public Subject authenticate(final String username, final Object credentials) throws AuthenticationException { - final String name = ensureCase(username); // Binds using the username and password provided by the user. @@ -153,53 +192,98 @@ public Subject authenticate(final String username, final Object credentials) thr } } finally { - LdapUtils.closeContext(ctx); + LDAPUtils.closeContext(ctx); } } + /** + * Find all the groups for an LDAP user. + * + * Groups have a `member` attribute, so we look for groups that have an attribute that matches member=user.dn + * + * @param ctx the LDAP context + * @param broker the database broker + * @param ldapUser details of the LDAP user to find group membership for + * + * @return the list of groups that the user is a member of + * + * @throws NamingException if an error occurs whilst querying LDAP + */ private List getGroupMembershipForLdapUser(final LdapContext ctx, final DBBroker broker, final SearchResult ldapUser) throws NamingException { - final List memberOf_groups = new ArrayList<>(); - final LDAPSearchContext search = ensureContextFactory().getSearch(); - final String userDistinguishedName = (String) ldapUser.getAttributes().get(search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.DN)).get(); - final List memberOf_groupNames = findGroupnamesForUserDistinguishedName(ctx, userDistinguishedName); - for (final String memberOf_groupName : memberOf_groupNames) { - memberOf_groups.add(getGroup(ctx, broker, memberOf_groupName)); + + @Nullable final String accountDnAttrName = search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.DN); + if (accountDnAttrName == null) { + LOG.warn("Expected a 'dn' attribute to be specified in the LDAP Realm config for search/account, but it is missing. Unable to find groups for a user name!"); + return Collections.emptyList(); + } + + @Nullable List memberOfGroups = null; + + final String userDistinguishedName = (String) ldapUser.getAttributes().get(accountDnAttrName).get(); + final List memberOfGroupNames = findGroupnamesForUserDistinguishedName(ctx, userDistinguishedName); + for (final String memberOfGroupName : memberOfGroupNames) { + final Group group = getGroup(broker, ctx, memberOfGroupName); + if (memberOfGroups == null) { + memberOfGroups = new ArrayList<>(); + } + memberOfGroups.add(group); } //TODO expand to a general method that rewrites the useraider based on the realTransformation - if (ensureContextFactory().getTransformationContext() != null) { - final List additionalGroupNames = ensureContextFactory().getTransformationContext().getAdditionalGroups(); - if (additionalGroupNames != null) { + if (memberOfGroups != null) { + if (ensureContextFactory().getTransformationContext() != null) { + final List additionalGroupNames = ensureContextFactory().getTransformationContext().getAdditionalGroups(); for (final String additionalGroupName : additionalGroupNames) { final Group additionalGroup = getSecurityManager().getGroup(additionalGroupName); if (additionalGroup != null) { - memberOf_groups.add(additionalGroup); + memberOfGroups.add(additionalGroup); } } } } - return memberOf_groups; + if (memberOfGroups == null) { + return Collections.emptyList(); + } + + return memberOfGroups; } - private List> getMetadataForLdapUser(final SearchResult ldapUser) throws NamingException { - final List> metadata = new ArrayList<>(); + private List> getMetadataForLdapUser(final SearchResult ldapUser) throws NamingException { final LDAPSearchAccount searchAccount = ensureContextFactory().getSearch().getSearchAccount(); final Attributes userAttributes = ldapUser.getAttributes(); + return getMetadataForLdapPrincipal(searchAccount, userAttributes); + } + + private List> getMetadataForLdapGroup(final SearchResult ldapGroup) throws NamingException { + final LDAPSearchGroup searchGroup = ensureContextFactory().getSearch().getSearchGroup(); + final Attributes groupAttributes = ldapGroup.getAttributes(); + return getMetadataForLdapPrincipal(searchGroup, groupAttributes); + } - //store any requested metadata - for (final AXSchemaType axSchemaType : searchAccount.getMetadataSearchAttributeKeys()) { - final String searchAttribute = searchAccount.getMetadataSearchAttribute(axSchemaType); - if (userAttributes != null) { - final Attribute userAttribute = userAttributes.get(searchAttribute); - if (userAttribute != null) { - final String attributeValue = userAttribute.get().toString(); - metadata.add(new SimpleEntry<>(axSchemaType, attributeValue)); + private List> getMetadataForLdapPrincipal(final AbstractLDAPSearchPrincipal searchPrincipal, final Attributes attributes) throws NamingException { + @Nullable List> metadata = null; + + // Get SchemaType values + for (final SchemaType schemaType : searchPrincipal.getMetadataSearchAttributeKeys()) { + final String searchAttribute = searchPrincipal.getMetadataSearchAttribute(schemaType); + if (attributes != null) { + final Attribute attribute = attributes.get(searchAttribute); + if (attribute != null) { + final String attributeValue = attribute.get().toString(); + if (metadata == null) { + metadata = new ArrayList<>(); + } + metadata.add(new SimpleEntry<>(schemaType, attributeValue)); } } } + if (metadata == null) { + return Collections.emptyList(); + } + return metadata; } @@ -216,39 +300,40 @@ public Account refreshAccountFromLdap(final Account account) throws PermissionDe LdapContext ctx = null; try { - ctx = getContext(invokingUser); + ctx = getContext(invokingUser); final SearchResult ldapUser = findAccountByAccountName(ctx, account.getName()); if (ldapUser == null) { throw new AuthenticationException(AuthenticationException.ACCOUNT_NOT_FOUND, "Could not find the account in the LDAP"); } - return executeAsSystemUser(ctx, (ctx2, broker) -> { + final LdapContext ctx2 = ctx; + return executeAsSystemUser(broker -> { int update = UPDATE_NONE; //1) get the ldap group membership - final List memberOf_groups = getGroupMembershipForLdapUser(ctx2, broker, ldapUser); + final List memberOfGroups = getGroupMembershipForLdapUser(ctx2, broker, ldapUser); //2) get the ldap primary group final String primaryGroup = findGroupBySID(ctx2, getPrimaryGroupSID(ldapUser)); //append the ldap primaryGroup to the head of the ldap group list, and compare //to the account group list - memberOf_groups.add(0, getGroup(ctx2, broker, primaryGroup)); + memberOfGroups.add(0, getGroup(broker, ctx2, primaryGroup)); final String accountGroups[] = account.getGroups(); if (!accountGroups[0].equals(ensureCase(primaryGroup))) { update |= UPDATE_GROUP; } else { - if (accountGroups.length != memberOf_groups.size()) { + if (accountGroups.length != memberOfGroups.size()) { update |= UPDATE_GROUP; } else { for (final String accountGroup : accountGroups) { boolean found = false; - for (final Group memberOf_group : memberOf_groups) { - if (accountGroup.equals(ensureCase(memberOf_group.getName()))) { + for (final Group memberOfGroup : memberOfGroups) { + if (accountGroup.equals(ensureCase(memberOfGroup.getName()))) { found = true; break; } @@ -263,7 +348,7 @@ public Account refreshAccountFromLdap(final Account account) throws PermissionDe } //3) check metadata - final List> ldapMetadatas = getMetadataForLdapUser(ldapUser); + final List> ldapMetadatas = getMetadataForLdapUser(ldapUser); final Set accountMetadataKeys = account.getMetadataKeys(); if (accountMetadataKeys.size() != ldapMetadatas.size()) { @@ -274,7 +359,7 @@ public Account refreshAccountFromLdap(final Account account) throws PermissionDe boolean found = false; - for (SimpleEntry ldapMetadata : ldapMetadatas) { + for (final SimpleEntry ldapMetadata : ldapMetadatas) { if (accountMetadataKey.equals(ldapMetadata.getKey()) && accountMetadataValue.equals(ldapMetadata.getValue())) { found = true; break; @@ -293,7 +378,7 @@ public Account refreshAccountFromLdap(final Account account) throws PermissionDe try { final Field fld = account.getClass().getSuperclass().getDeclaredField("groups"); fld.setAccessible(true); - fld.set(account, memberOf_groups); + fld.set(account, memberOfGroups); } catch (final NoSuchFieldException | IllegalAccessException nsfe) { throw new EXistException(nsfe.getMessage(), nsfe); } @@ -302,7 +387,7 @@ public Account refreshAccountFromLdap(final Account account) throws PermissionDe //update the metdata? if ((update & UPDATE_METADATA) == UPDATE_METADATA) { account.clearMetadata(); - for (final SimpleEntry ldapMetadata : ldapMetadatas) { + for (final SimpleEntry ldapMetadata : ldapMetadatas) { account.setMetadataValue(ldapMetadata.getKey(), ldapMetadata.getValue()); } } @@ -319,7 +404,7 @@ public Account refreshAccountFromLdap(final Account account) throws PermissionDe } catch (final NamingException | EXistException ne) { throw new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage(), ne); } finally { - LdapUtils.closeContext(ctx); + LDAPUtils.closeContext(ctx); } } @@ -328,14 +413,14 @@ private Account createAccountInDatabase(final LdapContext ctx, final String user //final LDAPSearchAccount searchAccount = ensureContextFactory().getSearch().getSearchAccount(); try { - return executeAsSystemUser(ctx, (ctx2, broker) -> { + return executeAsSystemUser(broker -> { if (LOG.isDebugEnabled()) { LOG.debug("Saving account '{}'.", username); } - //get (or create) the primary group if it doesnt exist - final Group primaryGroup = getGroup(ctx, broker, primaryGroupName); + // get (or create) the primary group if it doesn't exist + final Group primaryGroup = getGroup(broker, ctx, primaryGroupName); //get (or create) member groups /*LDAPSearchContext search = ensureContextFactory().getSearch(); @@ -351,12 +436,12 @@ private Account createAccountInDatabase(final LdapContext ctx, final String user final UserAider userAider = new UserAider(ID, username, primaryGroup); //add the member groups - for (final Group memberOf_group : getGroupMembershipForLdapUser(ctx, broker, ldapUser)) { - userAider.addGroup(memberOf_group); + for (final Group memberOfGroup : getGroupMembershipForLdapUser(ctx, broker, ldapUser)) { + userAider.addGroup(memberOfGroup); } //store any requested metadata - for (final SimpleEntry metadata : getMetadataForLdapUser(ldapUser)) { + for (final SimpleEntry metadata : getMetadataForLdapUser(ldapUser)) { userAider.setMetadataValue(metadata.getKey(), metadata.getValue()); } @@ -396,21 +481,73 @@ private Account createAccountInDatabase(final LdapContext ctx, final String user } } - private interface LDAPFunction extends BiFunction3E {} + @FunctionalInterface + private interface LDAPFunction extends Function3E {} - private R executeAsSystemUser(final LdapContext ctx, final LDAPFunction ldapFunction) throws EXistException, PermissionDeniedException, NamingException { + private R executeAsSystemUser(final LDAPFunction ldapFunction) throws EXistException, PermissionDeniedException, NamingException { try (final DBBroker broker = getDatabase().get(Optional.of(getSecurityManager().getSystemSubject()))) { //perform as SYSTEM user - return ldapFunction.apply(ctx, broker); + return ldapFunction.apply(broker); + } + } + + private Group createGroupInDatabase(final String groupName, final SearchResult ldapGroup) throws AuthenticationException { + try { + return executeAsSystemUser(broker -> { + try { + return createGroupInDatabase(broker, groupName, ldapGroup); + } catch (final AuthenticationException ae) { + LOG.error(ae.getMessage(), ae); + return null; + } + }); + } catch (final Exception e) { + if (LOG.isDebugEnabled()) { + LOG.debug(e); + } + throw new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, e.getMessage(), e); } } - private Group createGroupInDatabase(final DBBroker broker, final String groupname) throws AuthenticationException { + private Group createGroupInDatabase(final DBBroker broker, final String groupName, final SearchResult ldapGroup) throws AuthenticationException { + final GroupAider groupAider = new GroupAider(ID, groupName); try { - //return sm.addGroup(instantiateGroup(this, groupname)); - return getSecurityManager().addGroup(broker, new GroupAider(ID, groupname)); - } catch (Exception e) { + // set any group managers + if (ensureContextFactory().getTransformationContext() != null) { + final List additionalGroupManagers = ensureContextFactory().getTransformationContext().getAdditionalGroupManagers(); + for (String additionalGroupManagerName : additionalGroupManagers) { + + if ("default".equals(additionalGroupManagerName)) { + // default user should be taken from the default-username of the LDAP Realm Security Config + additionalGroupManagerName = ensureContextFactory().getSearch().getDefaultUsername(); + } + + // TODO(AR) adding group managers can cause a StackOverflowError at present +// // NOTE(AR) we need to make sure we are not requesting an account to be a group manager of a group that we are creating as part of creating an account, that is to say that a user cannot be a manager of their primary group +// @Nullable final PrincipalState additionalGroupManagerState = getAccountPrincipalState(additionalGroupManagerName); +// @Nullable final Account additionalGroupManager; +// if (PrincipalState.CREATING == additionalGroupManagerState) { +// // skip non-persistent entries +// additionalGroupManager = null; +// } else { +// additionalGroupManager = getSecurityManager().getAccount(additionalGroupManagerName); +// } +// +// if (additionalGroupManager != null) { +// groupAider.addManager(additionalGroupManager); +// } + } + } + + // store any requested metadata + for (final SimpleEntry metadata : getMetadataForLdapGroup(ldapGroup)) { + groupAider.setMetadataValue(metadata.getKey(), metadata.getValue()); + } + + return getSecurityManager().addGroup(broker, groupAider); + + } catch (final Exception e) { throw new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, e.getMessage(), e); } } @@ -437,106 +574,143 @@ private LdapContext getContext(final Optional invokingUser) throws Nami * @return An LDAP Context */ private LdapContext getContextWithCredentials(final Optional> optCredentials) throws NamingException { - final LdapContextFactory ctxFactory = ensureContextFactory(); + final LDAPContextFactory ctxFactory = ensureContextFactory(); final Tuple2 credentials = optCredentials.orElseGet(() -> defaultCredentials(ctxFactory)); return ctxFactory.getLdapContext(credentials._1, credentials._2, null); } - private Tuple2 defaultCredentials(final LdapContextFactory ctxFactory) { + private Tuple2 defaultCredentials(final LDAPContextFactory ctxFactory) { final LDAPSearchContext searchCtx = ctxFactory.getSearch(); return Tuple(searchCtx.getDefaultUsername(), searchCtx.getDefaultPassword()); } @Override - public final synchronized Account getAccount(String name) { + public final synchronized @Nullable Account getAccount(String name) { name = ensureCase(name); - //first attempt to get the cached account - final Account acct = super.getAccount(name); + // first attempt to get the cached account + final Account account = super.getAccount(name); + if (account != null) { + return account; + } - if (acct != null) { - return acct; - } else { - LdapContext ctx = null; - try { - ctx = getContext(getSecurityManager().getDatabase().getActiveBroker().getCurrentSubject()); - return getAccount(ctx, name); - } catch (final NamingException ne) { - if (LOG.isDebugEnabled()) { - LOG.debug(ne.getMessage(), ne); - } - LOG.error(new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage())); - return null; - } finally { - if (ctx != null) { - LdapUtils.closeContext(ctx); - } + // second find the account in LDAP + LdapContext ctx = null; + try { + ctx = getContext(getSecurityManager().getDatabase().getActiveBroker().getCurrentSubject()); + return getAccount(ctx, name); + } catch (final NamingException ne) { + if (LOG.isDebugEnabled()) { + LOG.debug(ne.getMessage(), ne); } + LOG.error(new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage())); + return null; + } finally { + LDAPUtils.closeContext(ctx); } } - private synchronized Account getAccount(final LdapContext ctx, String name) { + private synchronized @Nullable Account getAccount(final LdapContext ctx, String name) { name = ensureCase(name); if (LOG.isDebugEnabled()) { LOG.debug("Get request for account '{}'.", name); } - //first attempt to get the cached account - final Account acct = super.getAccount(name); - - if (acct != null) { + // first attempt to get the cached account + final Account account = super.getAccount(name); + if (account != null) { if (LOG.isDebugEnabled()) { LOG.debug("Cached used."); } //XXX: synchronize with LDAP - return acct; - } else { - //if the account is not cached, we should try and find it in LDAP and cache it if it exists - try { - //do the lookup - final SearchResult ldapUser = findAccountByAccountName(ctx, name); + return account; + } - if (LOG.isDebugEnabled()) { - LOG.debug("LDAP search return '{}'.", ldapUser); - } + // if the account is not cached, we should try and find it in LDAP and cache it if it exists + try { + //do the lookup + @Nullable final SearchResult ldapUser = findAccountByAccountName(ctx, name); - if (ldapUser == null) { - return null; - } else { - //found a user from ldap so cache them and return - try { - final String primaryGroupSID = getPrimaryGroupSID(ldapUser); - final String primaryGroup = findGroupBySID(ctx, primaryGroupSID); - if (LOG.isDebugEnabled()) { - LOG.debug("LDAP search for primary group by SID '{}', found '{}'.", primaryGroupSID, primaryGroup); - } - if (primaryGroup == null) { - //or exception? - return null; - } - return createAccountInDatabase(ctx, name, ldapUser, ensureCase(primaryGroup)); - //registerAccount(acct); //TODO do we need this - } catch (final AuthenticationException ae) { - LOG.error(ae.getMessage(), ae); - return null; + if (LOG.isDebugEnabled()) { + LOG.debug("LDAP search return '{}'.", ldapUser); + } + + if (ldapUser == null) { + return null; + } + + //found a user from ldap so cache them and return + try { + final String primaryGroupSID = getPrimaryGroupSID(ldapUser); + @Nullable final String primaryGroup = findGroupBySID(ctx, primaryGroupSID); + + if (primaryGroup == null) { + if (LOG.isDebugEnabled()) { + LOG.debug("LDAP search for primary group by SID '{}', found nothing'.", primaryGroupSID); } + return null; } - } catch (final NamingException ne) { + if (LOG.isDebugEnabled()) { - LOG.debug(ne.getMessage(), ne); + LOG.debug("LDAP search for primary group by SID '{}', found '{}'.", primaryGroupSID, primaryGroup); + } + + if (isFullyQualified(name)) { + // adjust name to make it unqualified for the database + name = extractPrincipalFromDn(name); } - //LOG.error(new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage())); + + return createAccountInDatabase(ctx, name, ldapUser, ensureCase(primaryGroup)); + //registerAccount(acct); //TODO do we need this + } catch (final AuthenticationException ae) { + LOG.error(ae.getMessage(), ae); return null; } + + } catch (final NamingException ne) { + if (LOG.isDebugEnabled()) { + LOG.debug(ne.getMessage(), ne); + } + //LOG.error(new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage())); + return null; } } + @Override + public boolean hasAccount(final Account account) { + return hasAccount(account.getName()); + } + @Override public boolean hasAccount(final String name) { + @Nullable final PrincipalState accountState = getAccountPrincipalState(name); + + // NOTE(AR) We need to check here whether we are already in the process of creating it in the database, we then use that to prevent creating it recursively through calls to getAccount -> createAccountInDatabase + if (PrincipalState.CREATING == accountState) { + // skip non-persistent entries + return false; + } + + if (PrincipalState.PERSISTENT == accountState) { + // we already have it + return true; + } + return getAccount(name) != null; } + private @Nullable PrincipalState getAccountPrincipalState(final String accountName) { + return usersByName.read(principalDb -> { + @Nullable final Tuple2> principalEntry = principalDb.get(accountName); + if (principalEntry == null) { + return null; + } + + return principalEntry._1; + }); + } + /** * The binary data is in form: * byte[0] - revision level @@ -589,106 +763,197 @@ private static String decodeSID(final byte[] sid) { private String getPrimaryGroupSID(final SearchResult ldapUser) throws NamingException { final LDAPSearchContext search = ensureContextFactory().getSearch(); - final Object objSID = ldapUser.getAttributes().get(search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.OBJECT_SID)).get(); - final String strObjectSid; - if (objSID instanceof String) { - strObjectSid = objSID.toString(); - } else { - strObjectSid = decodeSID((byte[]) objSID); + @Nullable final String userObjectSidAttrName = search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.OBJECT_SID); + String strUserObjectSid = null; + if (userObjectSidAttrName != null) { + @Nullable final Attribute userObjectSidAttr = ldapUser.getAttributes().get(userObjectSidAttrName); + if (userObjectSidAttr != null) { + @Nullable final Object userObjectSid = userObjectSidAttr.get(); + if (userObjectSid != null) { + if (userObjectSid instanceof String) { + strUserObjectSid = userObjectSid.toString(); + } else if (userObjectSid instanceof byte[]) { + strUserObjectSid = decodeSID((byte[]) userObjectSid); + } else { + throw new NamingException("LDAP Account: " + ldapUser.getName() + " attribute: " + userObjectSidAttrName + " has an unexpected type of: " + userObjectSid.getClass().getName()); + } + } else { + LOG.warn("LDAP Account: " + ldapUser.getName() + " attribute: " + userObjectSidAttrName + " has a null value"); + } + } else { + LOG.warn("LDAP Account: " + ldapUser.getName() + " attribute: " + userObjectSidAttrName + " is not present"); + } } - final String strPrimaryGroupID = (String) ldapUser.getAttributes().get(search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.PRIMARY_GROUP_ID)).get(); + @Nullable final String userPrimaryGroupIdAttrName = search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.PRIMARY_GROUP_ID); + @Nullable final String strUserPrimaryGroupId; + if (userPrimaryGroupIdAttrName != null) { + @Nullable final Attribute userPrimaryGroupIdAttr = ldapUser.getAttributes().get(userPrimaryGroupIdAttrName); + if (userPrimaryGroupIdAttr != null) { + @Nullable final Object userPrimaryGroupId = userPrimaryGroupIdAttr.get(); + if (userPrimaryGroupId != null) { + strUserPrimaryGroupId = (String) userPrimaryGroupId; + } else { + throw new NamingException("LDAP Account: " + ldapUser.getName() + " attribute:" + userPrimaryGroupIdAttrName + " has null value"); + } + } else { + throw new NamingException("LDAP Account: " + ldapUser.getName() + " is missing attribute:" + userPrimaryGroupIdAttrName); + } + } else { + throw new NamingException("Configuration for Account attribute primaryGroupID is missing from database security config.xml"); + } - return strObjectSid.substring(0, strObjectSid.lastIndexOf('-') + 1) + strPrimaryGroupID; + if (strUserObjectSid != null && SID_PATTERN.matcher(strUserObjectSid).matches()) { + return strUserObjectSid.substring(0, strUserObjectSid.lastIndexOf('-') + 1) + strUserPrimaryGroupId; + } else { + return strUserPrimaryGroupId; + } } - public final synchronized Group getGroup(final Subject invokingUser, final DBBroker broker, String name) { + @Override + public final synchronized @Nullable Group getGroup(String name) { name = ensureCase(name); - final Group grp = getGroup(name); - if (grp != null) { - return grp; - } else { - //if the group is not cached, we should try and find it in LDAP and cache it if it exists - LdapContext ctx = null; - try { - ctx = getContext(invokingUser); + // first attempt to get the cached group + final Group group = super.getGroup(name); + if (group != null) { + return group; + } - return getGroup(ctx, broker, name); - } catch (final NamingException ne) { - LOG.error(new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage())); - return null; - } finally { - if (ctx != null) { - LdapUtils.closeContext(ctx); - } + // second find the account in LDAP + LdapContext ctx = null; + try { + ctx = getContext(getSecurityManager().getDatabase().getActiveBroker().getCurrentSubject()); + return getGroup(ctx, name); + } catch (final NamingException ne) { + if (LOG.isDebugEnabled()) { + LOG.debug(ne.getMessage(), ne); } + LOG.error(new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage())); + return null; + } finally { + LDAPUtils.closeContext(ctx); } } - private synchronized Group getGroup(final LdapContext ctx, final DBBroker broker, final String name) { - if (name == null) { + private synchronized @Nullable Group getGroup(final Subject invokingUser, final DBBroker broker, String name) { + name = ensureCase(name); + + final Group group = super.getGroup(name); + if (group != null) { + return group; + } + + //if the group is not cached, we should try and find it in LDAP and cache it if it exists + LdapContext ctx = null; + try { + ctx = getContext(invokingUser); + return getGroup(broker, ctx, name); + + } catch (final NamingException ne) { + LOG.error(new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage())); return null; + + } finally { + LDAPUtils.closeContext(ctx); } + } - final String gName = ensureCase(name); + private synchronized @Nullable Group getGroup(final LdapContext ctx, @Nullable String groupName) { + return getGroup(ctx, groupName, this::createGroupInDatabase); + } - final Group grp = getGroup(gName); - if (grp != null) { - return grp; - } else { - //if the group is not cached, we should try and find it in LDAP and cache it if it exists + private synchronized @Nullable Group getGroup(final DBBroker broker, final LdapContext ctx, @Nullable String groupName) { + return getGroup(ctx, groupName, (gn, ldapGroup) -> createGroupInDatabase(broker, gn, ldapGroup)); + } + + private synchronized @Nullable Group getGroup(final LdapContext ctx, @Nullable String groupName, final BiFunctionE fnCreateDatabaseGroup) { + if (groupName == null) { + return null; + } + + groupName = ensureCase(escapeSearchAttribute(groupName)); + + final Group group = super.getGroup(groupName); + if (group != null) { + return group; + } + + //if the group is not cached, we should try and find it in LDAP and cache it if it exists + try { + //do the lookup + @Nullable final SearchResult ldapGroup = findGroupByGroupName(ctx, removeDomainPostfix(groupName)); + if (ldapGroup == null) { + return null; + } + + //found a group from ldap so cache them and return try { - //do the lookup - final SearchResult ldapGroup = findGroupByGroupName(ctx, removeDomainPostfix(gName)); - if (ldapGroup == null) { - return null; - } else { - //found a group from ldap so cache them and return - try { - return createGroupInDatabase(broker, gName); - //registerGroup(grp); //TODO do we need to do this? - } catch (final AuthenticationException ae) { - LOG.error(ae.getMessage(), ae); - return null; - } - } - } catch (final NamingException ne) { - LOG.error(new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage())); + return fnCreateDatabaseGroup.apply(groupName, ldapGroup); + //registerGroup(grp); //TODO do we need to do this? + } catch (final AuthenticationException ae) { + LOG.error(ae.getMessage(), ae); return null; } + + } catch (final NamingException ne) { + LOG.error(new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage())); + return null; } } + @Override + public boolean hasGroup(final Group group) { + return hasGroup(group.getName()); + } + @Override public boolean hasGroup(final String name) { + @Nullable final PrincipalState groupState = groupsByName.read(principalDb -> { + @Nullable final Tuple2> principalEntry = principalDb.get(name); + if (principalEntry == null) { + return null; + } + + return principalEntry._1; + }); + + // NOTE(AR) We need to check here whether we are already in the process of creating it in the database, we then use that to prevent creating it recursively through calls to getAccount -> createGroupInDatabase + if (PrincipalState.CREATING == groupState) { + // skip non-persistent entries + return false; + } + + if (PrincipalState.PERSISTENT == groupState) { + // we already have it + return true; + } + return getGroup((Subject)null, getSecurityManager().getDatabase().getActiveBroker(), name) != null; } - private String addDomainPostfix(final String principalName) { - String name = principalName; - if (!name.contains("@")) { - name += '@' + ensureContextFactory().getDomain(); + private String addDomainPostfix(String principalName) { + if (!principalName.contains("@")) { + principalName += '@' + ensureContextFactory().getDomain(); } - return name; + return principalName; } - private String removeDomainPostfix(final String principalName) { - String name = principalName; - if (name.contains("@") && name.endsWith(ensureContextFactory().getDomain())) { - name = name.substring(0, name.indexOf('@')); + private String removeDomainPostfix(String principalName) { + if (principalName.contains("@") && principalName.endsWith(ensureContextFactory().getDomain())) { + principalName = principalName.substring(0, principalName.indexOf('@')); } - return name; + return principalName; } - private boolean checkAccountRestrictionList(final String accountname) { + private boolean checkAccountRestrictionList(final String accountName) { final LDAPSearchContext search = ensureContextFactory().getSearch(); - return checkPrincipalRestrictionList(accountname, search.getSearchAccount()); + return checkPrincipalRestrictionList(accountName, search.getSearchAccount()); } - private boolean checkGroupRestrictionList(final String groupname) { + private boolean checkGroupRestrictionList(final String groupName) { final LDAPSearchContext search = ensureContextFactory().getSearch(); - return checkPrincipalRestrictionList(groupname, search.getSearchGroup()); + return checkPrincipalRestrictionList(groupName, search.getSearchGroup()); } private boolean checkPrincipalRestrictionList(final String principalName, final AbstractLDAPSearchPrincipal searchPrinciple) { @@ -743,22 +1008,32 @@ private String escapeSearchAttribute(final String searchAttribute) { .replace(")", "\\29"); } - private SearchResult findAccountByAccountName(final DirContext ctx, final String accountName) throws NamingException { - + private @Nullable SearchResult findAccountByAccountName(final DirContext ctx, String accountName) throws NamingException { if (!checkAccountRestrictionList(accountName)) { return null; } - final String userName = escapeSearchAttribute(removeDomainPostfix(accountName)); + accountName = escapeSearchAttribute(removeDomainPostfix(accountName)); final LDAPSearchContext search = ensureContextFactory().getSearch(); - final SearchAttribute sa = new SearchAttribute(search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.NAME), userName); - final String searchFilter = buildSearchFilter(search.getSearchAccount().getSearchFilterPrefix(), sa); + final String searchBase; + final String searchFilter; + if (isFullyQualified(accountName)) { + // account name is already fully qualified so we can just retrieve it directly + searchBase = extractBaseFromDn(accountName); + final SearchAttribute[] searchAttributes = extractSearchAttributesFromDn(accountName); + searchFilter = buildSearchFilter(search.getSearchAccount().getSearchFilterPrefix(), searchAttributes); + } else { + // search for the account by its name + searchBase = search.getBase(); + final SearchAttribute searchAttribute = new SearchAttribute(search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.NAME), accountName); + searchFilter = buildSearchFilter(search.getSearchAccount().getSearchFilterPrefix(), searchAttribute); + } final SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); - final NamingEnumeration results = ctx.search(search.getBase(), searchFilter, searchControls); + final NamingEnumeration results = ctx.search(searchBase, searchFilter, searchControls); SearchResult searchResult = null; if (results.hasMoreElements()) { @@ -773,7 +1048,7 @@ private SearchResult findAccountByAccountName(final DirContext ctx, final String return searchResult; } - private String findGroupBySID(final DirContext ctx, final String sid) throws NamingException { + private @Nullable String findGroupBySID(final DirContext ctx, final String sid) throws NamingException { final LDAPSearchContext search = ensureContextFactory().getSearch(); final SearchAttribute sa = new SearchAttribute(search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.OBJECT_SID), sid); @@ -799,20 +1074,32 @@ private String findGroupBySID(final DirContext ctx, final String sid) throws Nam return null; } - private @Nullable SearchResult findGroupByGroupName(final DirContext ctx, final String groupName) throws NamingException { - + private @Nullable SearchResult findGroupByGroupName(final DirContext ctx, String groupName) throws NamingException { if (!checkGroupRestrictionList(groupName)) { return null; } + groupName = escapeSearchAttribute(removeDomainPostfix(groupName)); + final LDAPSearchContext search = ensureContextFactory().getSearch(); - final SearchAttribute sa = new SearchAttribute(search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.NAME), escapeSearchAttribute(groupName)); - final String searchFilter = buildSearchFilter(search.getSearchGroup().getSearchFilterPrefix(), sa); + final String searchBase; + final String searchFilter; + if (isFullyQualified(groupName)) { + // group name is already fully qualified so we can just retrieve it directly + searchBase = extractBaseFromDn(groupName); + final SearchAttribute[] searchAttributes = extractSearchAttributesFromDn(groupName); + searchFilter = buildSearchFilter(search.getSearchGroup().getSearchFilterPrefix(), searchAttributes); + } else { + // search for the group by its name + searchBase = search.getAbsoluteBase(); + final SearchAttribute searchAttribute = new SearchAttribute(search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.NAME), groupName); + searchFilter = buildSearchFilter(search.getSearchGroup().getSearchFilterPrefix(), searchAttribute); + } final SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); - final NamingEnumeration results = ctx.search(search.getAbsoluteBase(), searchFilter, searchControls); + final NamingEnumeration results = ctx.search(searchBase, searchFilter, searchControls); if (results.hasMoreElements()) { final SearchResult searchResult = results.nextElement(); @@ -847,7 +1134,7 @@ public boolean updateAccount(final Account account) throws PermissionDeniedExcep @Override public boolean deleteAccount(final Account account) { - // TODO we dont support writting to LDAP + // TODO we dont support writing to LDAP //XXX: delete local cache? return false; } @@ -863,43 +1150,39 @@ public boolean deleteGroup(final Group group) { return false; } - private class SearchAttribute { - private final String name; - private final String value; - - SearchAttribute(final String name, final String value) { - this.name = name; - this.value = value; - } - - public String getName() { - return name; - } - - public String getValue() { - return value; + private String buildSearchFilter(final String searchPrefix, @Nullable final String dn) { + final StringBuilder builder = new StringBuilder(); + builder.append("("); + builder.append(buildSearchCriteria(searchPrefix)); + if (dn != null) { + builder.append("("); + builder.append(dn); + builder.append(")"); } + builder.append(")"); + return builder.toString(); } - private String buildSearchFilter(final String searchPrefix, final SearchAttribute sa) { - + private String buildSearchFilter(final String searchPrefix, @Nullable final SearchAttribute... searchAttributes) { final StringBuilder builder = new StringBuilder(); builder.append("("); builder.append(buildSearchCriteria(searchPrefix)); - - if (sa.getName() != null && sa.getValue() != null) { - builder.append("("); - builder.append(sa.getName()); - builder.append("="); - builder.append(sa.getValue()); - builder.append(")"); + if (searchAttributes != null) { + for (final SearchAttribute searchAttribute : searchAttributes) { + if (searchAttribute.getName() != null && searchAttribute.getValue() != null) { + builder.append("("); + builder.append(searchAttribute.getName()); + builder.append("="); + builder.append(searchAttribute.getValue()); + builder.append(")"); + } + } } builder.append(")"); return builder.toString(); } private String buildSearchFilterUnion(final String searchPrefix, final List searchAttributes) { - final StringBuilder builder = new StringBuilder(); builder.append("("); builder.append(buildSearchCriteria(searchPrefix)); @@ -931,8 +1214,6 @@ public List findUsernamesWhereNameStarts(String startsWith) { startsWith = escapeSearchAttribute(ensureCase(startsWith)); - final List usernames = new ArrayList<>(); - LdapContext ctx = null; try { ctx = getContext(getSecurityManager().getCurrentSubject()); @@ -947,30 +1228,36 @@ public List findUsernamesWhereNameStarts(String startsWith) { final NamingEnumeration results = ctx.search(search.getBase(), searchFilter, searchControls); + @Nullable List userNames = null; while (results.hasMoreElements()) { final SearchResult searchResult = results.nextElement(); final String username = ensureCase(addDomainPostfix((String) searchResult.getAttributes().get(search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.NAME)).get())); if (checkAccountRestrictionList(username)) { - usernames.add(username); + if (userNames == null) { + userNames = new ArrayList<>(); + } + userNames.add(username); } } + + if (userNames == null) { + return Collections.emptyList(); + } + + return userNames; + } catch (final NamingException ne) { LOG.error(new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage())); + return Collections.emptyList(); } finally { - if (ctx != null) { - LdapUtils.closeContext(ctx); - } + LDAPUtils.closeContext(ctx); } - - return usernames; } @Override - public List findUsernamesWhereNamePartStarts(final String startsWith) { + public List findUsernamesWhereNamePartStarts(String startsWith) { - final String sWith = escapeSearchAttribute(ensureCase(startsWith)); - - final List usernames = new ArrayList<>(); + startsWith = escapeSearchAttribute(ensureCase(startsWith)); LdapContext ctx = null; try { @@ -978,11 +1265,9 @@ public List findUsernamesWhereNamePartStarts(final String startsWith) { final LDAPSearchContext search = ensureContextFactory().getSearch(); - final SearchAttribute firstNameSa = new SearchAttribute(search.getSearchAccount().getMetadataSearchAttribute(AXSchemaType.FIRSTNAME), sWith + "*"); - final SearchAttribute lastNameSa = new SearchAttribute(search.getSearchAccount().getMetadataSearchAttribute(AXSchemaType.LASTNAME), sWith + "*"); - final List sas = new ArrayList<>(); - sas.add(firstNameSa); - sas.add(lastNameSa); + final SearchAttribute firstNameSa = new SearchAttribute(search.getSearchAccount().getMetadataSearchAttribute(AXSchemaType.FIRSTNAME), startsWith + "*"); + final SearchAttribute lastNameSa = new SearchAttribute(search.getSearchAccount().getMetadataSearchAttribute(AXSchemaType.LASTNAME), startsWith + "*"); + final List sas = Arrays.asList(firstNameSa, lastNameSa); final String searchFilter = buildSearchFilterUnion(search.getSearchAccount().getSearchFilterPrefix(), sas); @@ -992,37 +1277,42 @@ public List findUsernamesWhereNamePartStarts(final String startsWith) { final NamingEnumeration results = ctx.search(search.getBase(), searchFilter, searchControls); + @Nullable List userNames = null; while (results.hasMoreElements()) { final SearchResult searchResult = results.nextElement(); final String username = ensureCase(addDomainPostfix((String) searchResult.getAttributes().get(search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.NAME)).get())); if (checkAccountRestrictionList(username)) { - usernames.add(username); + if (userNames == null) { + userNames = new ArrayList<>(); + } + userNames.add(username); } } + + if (userNames == null) { + return Collections.emptyList(); + } + + return userNames; } catch (final NamingException ne) { LOG.error(new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage())); + return Collections.emptyList(); } finally { - if (ctx != null) { - LdapUtils.closeContext(ctx); - } + LDAPUtils.closeContext(ctx); } - - return usernames; } @Override - public List findUsernamesWhereUsernameStarts(final String startsWith) { - - final String sWith = escapeSearchAttribute(ensureCase(startsWith)); + public List findUsernamesWhereUsernameStarts(String startsWith) { - final List usernames = new ArrayList<>(); + startsWith = escapeSearchAttribute(ensureCase(startsWith)); LdapContext ctx = null; try { ctx = getContext(getSecurityManager().getCurrentSubject()); final LDAPSearchContext search = ensureContextFactory().getSearch(); - final SearchAttribute sa = new SearchAttribute(search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.NAME), sWith + "*"); + final SearchAttribute sa = new SearchAttribute(search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.NAME), startsWith + "*"); final String searchFilter = buildSearchFilter(search.getSearchAccount().getSearchFilterPrefix(), sa); final SearchControls searchControls = new SearchControls(); @@ -1031,68 +1321,91 @@ public List findUsernamesWhereUsernameStarts(final String startsWith) { final NamingEnumeration results = ctx.search(search.getBase(), searchFilter, searchControls); + @Nullable List userNames = null; while (results.hasMoreElements()) { final SearchResult searchResult = results.nextElement(); final String username = ensureCase(addDomainPostfix((String) searchResult.getAttributes().get(search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.NAME)).get())); if (checkAccountRestrictionList(username)) { - usernames.add(username); + if (userNames == null) { + userNames = new ArrayList<>(); + } + userNames.add(username); } } + + if (userNames == null) { + return Collections.emptyList(); + } + + return userNames; + } catch (final NamingException ne) { LOG.error(new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage())); + return Collections.emptyList(); } finally { - if (ctx != null) { - LdapUtils.closeContext(ctx); - } + LDAPUtils.closeContext(ctx); } - - return usernames; } - private List findGroupnamesForUserDistinguishedName(final LdapContext ctx, final String userDistinguishedName) { - final List groupnames = new ArrayList<>(); + private List findGroupnamesForUserDistinguishedName(final LdapContext ctx, String userDistinguishedName) { + + userDistinguishedName = escapeSearchAttribute(userDistinguishedName); try { final LDAPSearchContext search = ensureContextFactory().getSearch(); - final SearchAttribute sa = new SearchAttribute(search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.MEMBER), escapeSearchAttribute(userDistinguishedName)); + + @Nullable final String groupMemberAttrName = search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.MEMBER); + if (groupMemberAttrName == null) { + LOG.warn("Expected a 'member' attribute to be specified in the LDAP Realm config for search/group, but it is missing. Unable to find groups for a user name!"); + return Collections.emptyList(); + } + + final SearchAttribute sa = new SearchAttribute(groupMemberAttrName, userDistinguishedName); final String searchFilter = buildSearchFilter(search.getSearchGroup().getSearchFilterPrefix(), sa); final SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); searchControls.setReturningAttributes(new String[]{search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.NAME)}); - final NamingEnumeration results = ctx.search(search.getAbsoluteBase(), searchFilter, searchControls); + @Nullable List groupNames = null; while (results.hasMoreElements()) { final SearchResult searchResult = results.nextElement(); - final String groupname = ensureCase(addDomainPostfix((String) searchResult.getAttributes().get(search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.NAME)).get())); - if (checkGroupRestrictionList(groupname)) { - groupnames.add(groupname); + final String groupName = ensureCase(addDomainPostfix((String) searchResult.getAttributes().get(search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.NAME)).get())); + if (checkGroupRestrictionList(groupName)) { + if (groupNames == null) { + groupNames = new ArrayList<>(); + } + groupNames.add(groupName); } } + + if (groupNames == null) { + return Collections.emptyList(); + } + + return groupNames; + } catch (final NamingException ne) { LOG.error(new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage())); + return Collections.emptyList(); } - - return groupnames; } @Override - public List findGroupnamesWhereGroupnameStarts(final String startsWith) { - - final String sWith = escapeSearchAttribute(ensureCase(startsWith)); + public List findGroupnamesWhereGroupnameStarts(String startsWith) { - final List groupnames = new ArrayList<>(); + startsWith = escapeSearchAttribute(ensureCase(startsWith)); LdapContext ctx = null; try { ctx = getContext(getSecurityManager().getCurrentSubject()); final LDAPSearchContext search = ensureContextFactory().getSearch(); - final SearchAttribute sa = new SearchAttribute(search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.NAME), sWith + "*"); + final SearchAttribute sa = new SearchAttribute(search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.NAME), startsWith + "*"); final String searchFilter = buildSearchFilter(search.getSearchGroup().getSearchFilterPrefix(), sa); final SearchControls searchControls = new SearchControls(); @@ -1101,68 +1414,79 @@ public List findGroupnamesWhereGroupnameStarts(final String startsWith) final NamingEnumeration results = ctx.search(search.getBase(), searchFilter, searchControls); + @Nullable List groupNames = null; while (results.hasMoreElements()) { final SearchResult searchResult = results.nextElement(); - final String groupname = ensureCase(addDomainPostfix((String) searchResult.getAttributes().get(search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.NAME)).get())); - if (checkGroupRestrictionList(groupname)) { - groupnames.add(groupname); + final String groupName = ensureCase(addDomainPostfix((String) searchResult.getAttributes().get(search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.NAME)).get())); + if (checkGroupRestrictionList(groupName)) { + if (groupNames == null) { + groupNames = new ArrayList<>(); + } + groupNames.add(groupName); } } + + if (groupNames == null) { + return Collections.emptyList(); + } + + return groupNames; + } catch (final NamingException ne) { LOG.error(new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage())); + return Collections.emptyList(); } finally { - if (ctx != null) { - LdapUtils.closeContext(ctx); - } + LDAPUtils.closeContext(ctx); } - - return groupnames; } @Override - public List findGroupnamesWhereGroupnameContains(final String fragment) { + public List findGroupnamesWhereGroupnameContains(String fragment) { - final String part = escapeSearchAttribute(ensureCase(fragment)); - - final List groupnames = new ArrayList<>(); + fragment = escapeSearchAttribute(ensureCase(fragment)); LdapContext ctx = null; try { ctx = getContext(getSecurityManager().getCurrentSubject()); final LDAPSearchContext search = ensureContextFactory().getSearch(); - final SearchAttribute sa = new SearchAttribute(search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.NAME), "*" + part + "*"); + final SearchAttribute sa = new SearchAttribute(search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.NAME), "*" + fragment + "*"); final String searchFilter = buildSearchFilter(search.getSearchGroup().getSearchFilterPrefix(), sa); final SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); searchControls.setReturningAttributes(new String[]{search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.NAME)}); - final NamingEnumeration results = ctx.search(search.getBase(), searchFilter, searchControls); + @Nullable List groupNames = null; while (results.hasMoreElements()) { final SearchResult searchResult = results.nextElement(); - final String groupname = ensureCase(addDomainPostfix((String) searchResult.getAttributes().get(search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.NAME)).get())); - if (checkGroupRestrictionList(groupname)) { - groupnames.add(groupname); + final String groupName = ensureCase(addDomainPostfix((String) searchResult.getAttributes().get(search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.NAME)).get())); + if (checkGroupRestrictionList(groupName)) { + if (groupNames == null) { + groupNames = new ArrayList<>(); + } + groupNames.add(groupName); } } + + if (groupNames == null) { + return Collections.emptyList(); + } + + return groupNames; + } catch (final NamingException ne) { LOG.error(ne); + return Collections.emptyList(); } finally { - if (ctx != null) { - LdapUtils.closeContext(ctx); - } + LDAPUtils.closeContext(ctx); } - - return groupnames; } @Override public List findAllGroupNames() { - final List groupnames = new ArrayList<>(); - LdapContext ctx = null; try { ctx = getContext(getSecurityManager().getCurrentSubject()); @@ -1177,28 +1501,38 @@ public List findAllGroupNames() { final NamingEnumeration results = ctx.search(search.getBase(), searchFilter, searchControls); + @Nullable List groupNames = null; while (results.hasMoreElements()) { final SearchResult searchResult = results.nextElement(); - final String groupname = ensureCase(addDomainPostfix((String) searchResult.getAttributes().get(search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.NAME)).get())); - if (checkGroupRestrictionList(groupname)) { - groupnames.add(groupname); + final String groupName = (String) searchResult.getAttributes().get(search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.NAME)).get(); + if (checkGroupRestrictionList(groupName)) { + if (groupNames == null) { + groupNames = new ArrayList<>(); + } + + // expand group name to realm's group name + final String realmGroupName = ensureCase(addDomainPostfix(groupName)); + + groupNames.add(realmGroupName); } } + + if (groupNames == null) { + return Collections.emptyList(); + } + + return groupNames; + } catch (final NamingException ne) { LOG.error(new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage())); + return Collections.emptyList(); } finally { - if (ctx != null) { - LdapUtils.closeContext(ctx); - } + LDAPUtils.closeContext(ctx); } - - return groupnames; } @Override public List findAllUserNames() { - final List usernames = new ArrayList<>(); - LdapContext ctx = null; try { ctx = getContext(getSecurityManager().getCurrentSubject()); @@ -1213,33 +1547,39 @@ public List findAllUserNames() { final NamingEnumeration results = ctx.search(search.getBase(), searchFilter, searchControls); + @Nullable List userNames = null; while (results.hasMoreElements()) { final SearchResult searchResult = results.nextElement(); - final String accountname = ensureCase(addDomainPostfix((String) searchResult.getAttributes().get(search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.NAME)).get())); - if (checkAccountRestrictionList(accountname)) { - usernames.add(accountname); + final String accountName = ensureCase(addDomainPostfix((String) searchResult.getAttributes().get(search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.NAME)).get())); + if (checkAccountRestrictionList(accountName)) { + if (userNames == null) { + userNames = new ArrayList<>(); + } + userNames.add(accountName); } } + + if (userNames == null) { + return Collections.emptyList(); + } + + return userNames; + } catch (final NamingException ne) { LOG.error(new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage())); + return Collections.emptyList(); } finally { - if (ctx != null) { - LdapUtils.closeContext(ctx); - } + LDAPUtils.closeContext(ctx); } - - return usernames; } @Override - public List findAllGroupMembers(final String groupName) { - - final String name = escapeSearchAttribute(ensureCase(groupName)); + public List findAllGroupMembers(String groupName) { - final List groupMembers = new ArrayList<>(); + groupName = ensureCase(escapeSearchAttribute(removeDomainPostfix(groupName))); - if (!checkGroupRestrictionList(name)) { - return groupMembers; + if (!checkGroupRestrictionList(groupName)) { + return Collections.emptyList(); } LdapContext ctx = null; @@ -1247,48 +1587,78 @@ public List findAllGroupMembers(final String groupName) { ctx = getContext(getSecurityManager().getCurrentSubject()); //find the dn of the group - SearchResult searchResult = findGroupByGroupName(ctx, removeDomainPostfix(name)); + SearchResult searchResult = findGroupByGroupName(ctx, groupName); if (searchResult == null) { // no such group - return groupMembers; + return Collections.emptyList(); } final LDAPSearchContext search = ensureContextFactory().getSearch(); - final String dnGroup = (String) searchResult.getAttributes().get(search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.DN)).get(); - //find all accounts that are a member of the group - final SearchAttribute sa = new SearchAttribute(search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.MEMBER_OF), escapeSearchAttribute(dnGroup)); - final String searchFilter = buildSearchFilter(search.getSearchAccount().getSearchFilterPrefix(), sa); - final SearchControls searchControls = new SearchControls(); - searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); - searchControls.setReturningAttributes(new String[]{search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.NAME)}); - - final NamingEnumeration results = ctx.search(search.getBase(), searchFilter, searchControls); + @Nullable final String accountMemberOfAttrName = search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.MEMBER_OF); + @Nullable final String groupDnAttName = search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.DN); + @Nullable final String groupMemberAttrName = search.getSearchGroup().getSearchAttribute(LDAPSearchAttributeKey.MEMBER); + + @Nullable List groupMembers = null; + + if (accountMemberOfAttrName != null && groupDnAttName != null) { + // Active Directory (like) - accounts have a memberOf attribute, so we look for accounts that are a member of our group by its dn + + @Nullable final Attribute groupDnAttr = searchResult.getAttributes().get(groupDnAttName); + if (groupDnAttr != null) { + final String groupDn = (String) groupDnAttr.get(); + + // Find all accounts that have a memberOf=groupDn + final SearchAttribute sa = new SearchAttribute(accountMemberOfAttrName, groupDn); + final String searchFilter = buildSearchFilter(search.getSearchAccount().getSearchFilterPrefix(), sa); + final SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + searchControls.setReturningAttributes(new String[] { search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.NAME) }); + final NamingEnumeration results = ctx.search(search.getBase(), searchFilter, searchControls); + while (results.hasMoreElements()) { + searchResult = results.nextElement(); + final String member = ensureCase(addDomainPostfix((String) searchResult.getAttributes().get(search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.NAME)).get())); + if (checkAccountRestrictionList(member)) { + if (groupMembers == null) { + groupMembers = new ArrayList<>(); + } + groupMembers.add(member); + } + } + } - while (results.hasMoreElements()) { - searchResult = results.nextElement(); - final String member = ensureCase(addDomainPostfix((String) searchResult.getAttributes().get(search.getSearchAccount().getSearchAttribute(LDAPSearchAttributeKey.NAME)).get())); - if (checkAccountRestrictionList(member)) { - groupMembers.add(member); + } else if (groupMemberAttrName != null) { + // NIS/POSIX (like) - groups have a member attribute, so we retrieve all the of those properties + @Nullable final Attribute groupMemberAttr = searchResult.getAttributes().get(groupMemberAttrName); + if (groupMemberAttr != null) { + for (int i = 0; i < groupMemberAttr.size(); i++) { + final String member = ensureCase(addDomainPostfix((String) groupMemberAttr.get(i))); + if (groupMembers == null) { + groupMembers = new ArrayList<>(); + } + groupMembers.add(member); + } } } + if (groupMembers == null) { + return Collections.emptyList(); + } + + return groupMembers; + } catch (final NamingException ne) { LOG.error(new AuthenticationException(AuthenticationException.UNNOWN_EXCEPTION, ne.getMessage())); + return Collections.emptyList(); } finally { - if (ctx != null) { - LdapUtils.closeContext(ctx); - } + LDAPUtils.closeContext(ctx); } - - return groupMembers; } - private final class AuthenticatedLdapSubjectAccreditedImpl extends SubjectAccreditedImpl { - + public final class AuthenticatedLdapSubjectAccreditedImpl extends SubjectAccreditedImpl { private final String authenticatedCredentials; - private AuthenticatedLdapSubjectAccreditedImpl(final AbstractAccount account, final LdapContext ctx, final String authenticatedCredentials) { + public AuthenticatedLdapSubjectAccreditedImpl(final AbstractAccount account, final LdapContext ctx, final String authenticatedCredentials) { super(account, ctx); this.authenticatedCredentials = authenticatedCredentials; } diff --git a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPSearchAccount.java b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPSearchAccount.java index e45ebc49fe..5f1b04ed92 100644 --- a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPSearchAccount.java +++ b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPSearchAccount.java @@ -1,4 +1,28 @@ /* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * * eXist-db Open Source Native XML Database * Copyright (C) 2001 The eXist-db Authors * @@ -35,7 +59,7 @@ public class LDAPSearchAccount extends AbstractLDAPSearchPrincipal implements Co public LDAPSearchAccount(final Configuration config) { super(config); - //it require, because class's fields initializing after super constructor + // required as class's fields are initialized after super constructor if (this.configuration != null) { this.configuration = Configurator.configure(this, this.configuration); } diff --git a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPSearchAttributeKey.java b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPSearchAttributeKey.java new file mode 100644 index 0000000000..0a643ae3aa --- /dev/null +++ b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPSearchAttributeKey.java @@ -0,0 +1,160 @@ +/* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.security.realm.ldap; + +enum LDAPSearchAttributeKey { + + /** + * The system name of the principal. + * For an Account, this is the Username, + * and for a Group this is the Groupname. + * + * Account examples: + *
    + *
  • For NIS/POSIX use uid.
  • + *
  • For Active Directory use sAMAccountName.
  • + *
+ * + * Group examples: + *
    + *
  • For NIS/POSIX use cn.
  • + *
  • For Active Directory use sAMAccountName.
  • + *
+ */ + NAME("name"), + + /** + * The Distinguished Name of the LDAP object. + * + * Account examples: + *
    + *
  • For NIS/POSIX use uid.
  • + *
  • For Active Directory use distinguishedName.
  • + *
+ * + * Group examples: + *
    + *
  • For NIS/POSIX this is not needed.
  • + *
  • For Active Directory use distinguishedName.
  • + *
+ */ + DN("dn"), + + + /** + * Present only on the Account, indicates that the Account is a member of a Group. + * + * Account examples: + *
    + *
  • For NIS/POSIX this is not needed.
  • + *
  • For Active Directory use memberOf.
  • + *
+ */ + MEMBER_OF("memberOf"), + + /** + * Present only on the Account, indicates that the Account has a Primary Group. + * + * Account examples: + *
    + *
  • For NIS/POSIX use gidNumber.
  • + *
  • For Active Directory use primaryGroupID.
  • + *
+ */ + PRIMARY_GROUP_ID("primaryGroupID"), + + /** + * Currently unused. + */ + PRIMARY_GROUP_TOKEN("primaryGroupToken"), + + /** + * Present only on the Group, appears zero-or more times in the directory to indicate the members of a group. + * + * Account examples: + *
    + *
  • For NIS/POSIX use memberUid.
  • + *
  • For Active Directory use member.
  • + *
+ */ + MEMBER("member"), + + /** + * A unique id for the Principal. + * + * Account examples: + *
    + *
  • For NIS/POSIX use uidNumber.
  • + *
  • For Active Directory use objectSid.
  • + *
+ * + * Group examples: + *
    + *
  • For NIS/POSIX use gidNumber.
  • + *
  • For Active Directory use objectSid.
  • + *
+ */ + OBJECT_SID("objectSid"); + + private final String key; + + LDAPSearchAttributeKey(final String key) { + this.key = key; + } + + public String getKey() { + return key; + } + + public static LDAPSearchAttributeKey valueOfKey(final String key) { + for (final LDAPSearchAttributeKey ldapSearchAttributeKey : LDAPSearchAttributeKey.values()) { + if (ldapSearchAttributeKey.getKey().equals(key)) { + return ldapSearchAttributeKey; + } + } + return null; + } +} diff --git a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPSearchContext.java b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPSearchContext.java index 67bdd9c7e2..8445731d1c 100644 --- a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPSearchContext.java +++ b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPSearchContext.java @@ -1,4 +1,28 @@ /* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * * eXist-db Open Source Native XML Database * Copyright (C) 2001 The eXist-db Authors * @@ -35,18 +59,36 @@ @ConfigurationClass("search") public class LDAPSearchContext implements Configurable { + /** + * The LDAP base address to perform searches within. + */ @ConfigurationFieldAsElement("base") protected String base = null; + /** + * The LDAP username to use to access + * the LDAP server when there is no particular + * user involved. Typically used for metadata queries + * such as findAllUserNames. + */ @ConfigurationFieldAsElement("default-username") protected String defaultUsername = null; + /** + * The LDAP password to accompany the {@link #defaultUsername}. + */ @ConfigurationFieldAsElement("default-password") protected String defaultPassword = null; + /** + * Configuration for accessing LDAP Account information. + */ @ConfigurationFieldAsElement("account") protected LDAPSearchAccount searchAccount = null; + /** + * Configuration for accessing LDAP Group information. + */ @ConfigurationFieldAsElement("group") protected LDAPSearchGroup searchGroup = null; @@ -60,6 +102,14 @@ public String getBase() { return base; } + /** + * Get the domain address from the LDAP base address. + * + * e.g. if the `base` is `ou=Users,dc=gb,dc=myorg,dc=com` + * this function will return `dc=gb,dc=myorg,dc=com`. + * + * @return the domain address. + */ public String getAbsoluteBase() throws NamingException { if (getBase() != null) { int index; diff --git a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPSearchGroup.java b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPSearchGroup.java index 268bfc2fb8..60b2f9d10c 100644 --- a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPSearchGroup.java +++ b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPSearchGroup.java @@ -1,4 +1,28 @@ /* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * * eXist-db Open Source Native XML Database * Copyright (C) 2001 The eXist-db Authors * @@ -36,8 +60,8 @@ public class LDAPSearchGroup extends AbstractLDAPSearchPrincipal implements Conf public LDAPSearchGroup(final Configuration config) { super(config); - //it require, because class's fields initializing after super constructor - if(this.configuration != null) { + // required as class's fields are initialized after super constructor + if (this.configuration != null) { this.configuration = Configurator.configure(this, this.configuration); } } diff --git a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPTransformationContext.java b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPTransformationContext.java index 474716884f..fca8e09529 100644 --- a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPTransformationContext.java +++ b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPTransformationContext.java @@ -1,4 +1,28 @@ /* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * * eXist-db Open Source Native XML Database * Copyright (C) 2001 The eXist-db Authors * @@ -32,14 +56,16 @@ import org.exist.config.annotation.ConfigurationFieldAsElement; /** - * @author aretter + * @author Adam Retter. */ @ConfigurationClass("transformation") public class LDAPTransformationContext implements TransformationContext, Configurable { @ConfigurationFieldAsElement("add-group") - //protected List addGroup = new ArrayList(); - protected String addGroup; //TODO convert to list + private List additionalGroups = new ArrayList<>(); + + @ConfigurationFieldAsElement("add-group-manager") + private List additionalGroupManagers = new ArrayList<>(); private final Configuration configuration; @@ -47,13 +73,34 @@ public LDAPTransformationContext(final Configuration config) { this.configuration = Configurator.configure(this, config); } + /** + * Add a additional group. + * + * @param group the group to add. + */ + public void addGroup(final String group) { + additionalGroups.add(group); + } + + /** + * Add an additional group manager. + * + * @param groupManager the group manager to add. + */ + public void addGroupManager(final String groupManager) { + additionalGroupManagers.add(groupManager); + } + @Override public List getAdditionalGroups() { - final List additionalGroups = new ArrayList<>(); - additionalGroups.add(addGroup); return additionalGroups; } + @Override + public List getAdditionalGroupManagers() { + return additionalGroupManagers; + } + @Override public boolean isConfigured() { return (configuration != null); diff --git a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPUtils.java b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPUtils.java new file mode 100644 index 0000000000..ca55a03715 --- /dev/null +++ b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LDAPUtils.java @@ -0,0 +1,217 @@ +/* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.security.realm.ldap; + +import javax.annotation.Nullable; +import javax.naming.NamingException; +import javax.naming.ldap.LdapContext; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Dmitriy Shabanov + */ +class LDAPUtils { + private static final Logger LOG = LogManager.getLogger(LDAPUtils.class); + + static void closeContext(@Nullable final LdapContext ctx) { + if (ctx == null) { + return; + } + + try { + ctx.close(); + } catch (final NamingException e) { + if (LOG.isDebugEnabled()) { + LOG.error("Exception while closing LDAP context. ", e); + } + } + } + + /** + * Returns true if the string is fully qualified. + * + * @param string the string to test + * + * @return true if the string is fully qualified, false otherwise + */ + public static boolean isFullyQualified(final String string) { + return string.indexOf('=') > - 1; + } + + /** + * Formats a username. + * + * If the provided username is unqualified and a principal pattern has + * been set then the username is formatted with the principal pattern. + * + * @param username the username to format + * @param principalPatternFormat the formatter or null if no principal pattern is available + * + * @return the formatted username + */ + public static @Nullable String formatUsername(@Nullable final String username, @Nullable final MessageFormat principalPatternFormat) { + if (username == null) { + return null; + } + + if (isFullyQualified(username) || principalPatternFormat == null) { + return username; + } + + return principalPatternFormat.format(new String[]{ username }); + } + + /** + * Extracts the base from a distinguished name. + * + * e.g. Given: `uid=user2,ou=Users,dc=gb,dc=myorg,dc=com` + * this function will return: `ou=Users,dc=gb,dc=myorg,dc=com`. + * + * @param dn the distinguished name + * + * @return the base of the distinguished name + */ + public static @Nullable String extractBaseFromDn(@Nullable final String dn) { + if (dn == null) { + return null; + } + + StringBuilder base = null; + + final String[] attributes = dn.split(","); + for (final String attribute : attributes) { + if (attribute.startsWith("ou=") || attribute.startsWith("dc=")) { + if (base != null) { + base.append(','); + } else { + base = new StringBuilder(); + } + base.append(attribute); + } + } + + if (base == null) { + return null; + } + + return base.toString(); + } + + /** + * Extracts the search attributes from a distinguished name. + * + * e.g. Given: `uid=user2,ou=Users,dc=gb,dc=myorg,dc=com` + * this function will return: `uid=user2`. + * + * @param dn the distinguished name + * + * @return the Search Attributes from the distinguished name + */ + public static @Nullable SearchAttribute[] extractSearchAttributesFromDn(@Nullable final String dn) { + if (dn == null) { + return null; + } + + List searchAttributes = null; + + final String[] attributes = dn.split(","); + for (final String attribute : attributes) { + if (!attribute.startsWith("ou=") && !attribute.startsWith("dc=") && attribute.trim().length() > 0) { + final String[] attributeKeyValue = attribute.split("="); + if (attributeKeyValue.length == 2) { + if (searchAttributes == null) { + searchAttributes = new ArrayList<>(); + } + searchAttributes.add(new SearchAttribute(attributeKeyValue[0], attributeKeyValue[1])); + } + } + } + + if (searchAttributes == null) { + return null; + } + + return searchAttributes.toArray(new SearchAttribute[0]); + } + + /** + * Extracts the pricipal from a distinguished name. + * + * e.g. Given: `uid=user2,ou=Users,dc=gb,dc=myorg,dc=com` + * this function will return: `user2`. + * + * @param dn the distinguished name + * + * @return the principle + */ + public static @Nullable String extractPrincipalFromDn(@Nullable final String dn) { + if (dn == null) { + return null; + } + + if (!isFullyQualified(dn)) { + return dn; + } + + final String[] attributes = dn.split(","); + if (attributes.length == 0) { + return dn; + } + + final String[] attributeKeyValue = attributes[0].split("="); + if (attributeKeyValue.length != 2) { + return dn; + } + + return attributeKeyValue[1]; + } +} diff --git a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LdapUtils.java b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LdapUtils.java deleted file mode 100644 index f3ccb54e17..0000000000 --- a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/LdapUtils.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * eXist-db Open Source Native XML Database - * Copyright (C) 2001 The eXist-db Authors - * - * info@exist-db.org - * http://www.exist-db.org - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ -package org.exist.security.realm.ldap; - -import javax.naming.NamingException; -import javax.naming.ldap.LdapContext; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -/** - * @author Dmitriy Shabanov - */ -class LdapUtils { - private static final Logger LOG = LogManager.getLogger(LdapUtils.class); - - static void closeContext(final LdapContext ctx) { - try { - if (ctx != null) { - ctx.close(); - } - } catch (final NamingException e) { - if (LOG.isDebugEnabled()) { - LOG.error("Exception while closing LDAP context. ", e); - } - } - } -} diff --git a/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/SearchAttribute.java b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/SearchAttribute.java new file mode 100644 index 0000000000..c55003ebb2 --- /dev/null +++ b/extensions/security/ldap/src/main/java/org/exist/security/realm/ldap/SearchAttribute.java @@ -0,0 +1,64 @@ +/* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * NOTE: Parts of this file contain code from 'The eXist-db Authors'. + * The original license header is included below. + * + * ===================================================================== + * + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.security.realm.ldap; + +class SearchAttribute { + private final String name; + private final String value; + + SearchAttribute(final String name, final String value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } +} \ No newline at end of file diff --git a/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/AbstractLDAPRealmNisIT.java b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/AbstractLDAPRealmNisIT.java new file mode 100644 index 0000000000..f981482ac1 --- /dev/null +++ b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/AbstractLDAPRealmNisIT.java @@ -0,0 +1,447 @@ +/* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.security.realm.ldap; + +import org.apache.directory.server.core.integ.AbstractLdapTestUnit; +import org.exist.EXistException; +import org.exist.collections.Collection; +import org.exist.security.*; +import org.exist.security.SecurityManager; +import org.exist.security.internal.RealmImpl; +import org.exist.security.internal.aider.GroupAider; +import org.exist.storage.BrokerPool; +import org.exist.storage.DBBroker; +import org.exist.storage.txn.Txn; +import org.exist.test.ExistEmbeddedServer; +import org.exist.util.DatabaseConfigurationException; +import org.exist.util.LockException; +import org.exist.util.StringInputSource; +import org.exist.xmldb.XmldbURI; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xml.sax.SAXException; +import xyz.elemental.mediatype.MediaType; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration Tests for the LDAP Realm. + * + * @author Adam Retter + */ +public abstract class AbstractLDAPRealmNisIT extends AbstractLdapTestUnit { + private static ExistEmbeddedServer existEmbeddedServer; + + private static final Set EXPECTED_DATABASE_USER_NAMES = new HashSet<>(Arrays.asList( + "nobody", + "guest", + "admin", + "SYSTEM" + )); + + private static final Set EXPECTED_LDAP_USER_NAMES = new HashSet<>(Arrays.asList( + "user1@gb.myorg.com", + "user2@gb.myorg.com", + "user3@gb.myorg.com" + )); + + private static final Set EXPECTED_DATABASE_GROUP_NAMES = new HashSet<>(Arrays.asList( + "nogroup", + "guest", + "dba", + "ldap-users" + )); + + private static final Set EXPECTED_LDAP_GROUP_NAMES = new HashSet<>(Arrays.asList( + "group1@gb.myorg.com", + "group2@gb.myorg.com" + )); + + private final PrincipalPattern principalPattern; + private final DefaultUser defaultUser; + + public AbstractLDAPRealmNisIT(final PrincipalPattern principalPattern, final DefaultUser defaultUser) { + this.principalPattern = principalPattern; + this.defaultUser = defaultUser; + } + + private boolean directLdapAuthShouldFail() { + return principalPattern == PrincipalPattern.ABSENT + || principalPattern == PrincipalPattern.PRESENT_INVALID; + } + + private boolean defaultLdapAuthShouldFail() { + return defaultUser == DefaultUser.ABSENT + || defaultUser == DefaultUser.PRESENT_INVALID_UNQUALIFIED + || defaultUser == DefaultUser.PRESENT_INVALID_QUALIFIED + || (defaultUser == DefaultUser.PRESENT_VALID_UNQUALIFIED && (principalPattern == PrincipalPattern.ABSENT || principalPattern == PrincipalPattern.PRESENT_INVALID)); + } + + @BeforeEach + public void setup() throws DatabaseConfigurationException, EXistException, IOException, PermissionDeniedException, LockException, URISyntaxException, SAXException { + existEmbeddedServer = new ExistEmbeddedServer(true, true); + existEmbeddedServer.startDb(); + + // Start eXist-db with an LDAP Security config + createLocalLdapUsersGroup(existEmbeddedServer); + configureLdapRealm(existEmbeddedServer); + } + + @AfterEach + public void cleanup() { + existEmbeddedServer.stopDb(); + } + + @Test + public void authenticateUnqualified() throws AuthenticationException { + // Attempt to authenticate as user-2 via LDAP Realm + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + final org.exist.security.SecurityManager securityManager = brokerPool.getSecurityManager(); + + if (directLdapAuthShouldFail()) { + // direct authentication should fail + assertThrows(AuthenticationException.class, () -> + securityManager.authenticate("user2", "user2") + ); + + } else { + // direct authentication should succeed + final Subject ldapUser2Subject = securityManager.authenticate("user2", "user2"); + assertNotNull(ldapUser2Subject); + } + } + + @Test + public void authenticateQualified() throws AuthenticationException { + // Attempt to authenticate as user-2 via LDAP Realm + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + final org.exist.security.SecurityManager securityManager = brokerPool.getSecurityManager(); + + // direct authentication should succeed + final Subject ldapUser2Subject = securityManager.authenticate("uid=user2,ou=Users,dc=gb,dc=myorg,dc=com", "user2"); + assertNotNull(ldapUser2Subject); + } + + @Test + public void findAllUserNames() throws EXistException { + final Set expectedUserNames = new HashSet<>(EXPECTED_DATABASE_USER_NAMES); + if (!defaultLdapAuthShouldFail()) { + // default authentication should succeed, so we should see users from both the database and LDAP + expectedUserNames.addAll(EXPECTED_LDAP_USER_NAMES); + } + + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + final SecurityManager securityManager = brokerPool.getSecurityManager(); + try (final DBBroker broker = brokerPool.get(Optional.of(securityManager.getSystemSubject()))) { + final Set actualUserNames = new HashSet(securityManager.findAllUserNames()); + assertEquals(expectedUserNames, actualUserNames); + } + } + + @Test + public void findAllGroupNames() throws EXistException { + final Set expectedGroupNames = new HashSet<>(EXPECTED_DATABASE_GROUP_NAMES); + if (!defaultLdapAuthShouldFail()) { + // default authentication should succeed, so we should see users from both the database and LDAP + expectedGroupNames.addAll(EXPECTED_LDAP_GROUP_NAMES); + } + + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + final SecurityManager securityManager = brokerPool.getSecurityManager(); + try (final DBBroker broker = brokerPool.get(Optional.of(securityManager.getSystemSubject()))) { + final Set actualGroupNames = new HashSet(securityManager.findAllGroupNames()); + assertEquals(expectedGroupNames, actualGroupNames); + } + } + + @Test + public void findAllGroupMembers() throws EXistException { + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + final SecurityManager securityManager = brokerPool.getSecurityManager(); + + Set expectedGroupMembers = new HashSet<>(); + if (!defaultLdapAuthShouldFail()) { + // default authentication should succeed, so we should see users from both the database and LDAP + expectedGroupMembers.add("user1@gb.myorg.com"); + expectedGroupMembers.add("user2@gb.myorg.com"); + } + + try (final DBBroker broker = brokerPool.get(Optional.of(securityManager.getSystemSubject()))) { + final Set actualGroupMembers = new HashSet(securityManager.findAllGroupMembers("group1")); + assertEquals(expectedGroupMembers, actualGroupMembers); + } + + expectedGroupMembers.clear(); + if (!defaultLdapAuthShouldFail()) { + // default authentication should succeed, so we should see users from both the database and LDAP + expectedGroupMembers.add("user2@gb.myorg.com"); + } + + try (final DBBroker broker = brokerPool.get(Optional.of(securityManager.getSystemSubject()))) { + final Set actualGroupMembers = new HashSet(securityManager.findAllGroupMembers("group2")); + assertEquals(expectedGroupMembers, actualGroupMembers); + } + + // group 3 should never appear as it is not in the LDAP Realm security config's white-list + try (final DBBroker broker = brokerPool.get(Optional.of(securityManager.getSystemSubject()))) { + final Set actualGroupMembers = new HashSet(securityManager.findAllGroupMembers("group3")); + assertTrue(actualGroupMembers.isEmpty()); + } + } + + @Test + public void findUsernamesWhereNameStarts() throws EXistException { + final Set expectedUserNames = new HashSet<>(); + if (!defaultLdapAuthShouldFail()) { + // default authentication should succeed, so we should see users from LDAP + expectedUserNames.addAll(EXPECTED_LDAP_USER_NAMES); + } + + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + final SecurityManager securityManager = brokerPool.getSecurityManager(); + try (final DBBroker broker = brokerPool.get(Optional.of(securityManager.getSystemSubject()))) { + final Set actualUserNames = new HashSet(securityManager.findUsernamesWhereNameStarts("First")); + assertEquals(expectedUserNames, actualUserNames); + } + } + + @Test + public void findUsernamesWhereNamePartStarts() throws EXistException { + final Set expectedUserNames = new HashSet<>(); + if (!defaultLdapAuthShouldFail()) { + // default authentication should succeed, so we should see users from LDAP + expectedUserNames.addAll(EXPECTED_LDAP_USER_NAMES); + } + + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + final SecurityManager securityManager = brokerPool.getSecurityManager(); + try (final DBBroker broker = brokerPool.get(Optional.of(securityManager.getSystemSubject()))) { + final Set actualUserNames = new HashSet(securityManager.findUsernamesWhereNamePartStarts("First")); + assertEquals(expectedUserNames, actualUserNames); + } + } + + @Test + public void findUsernamesWhereUsernameStarts() throws EXistException { + final Set expectedUserNames = new HashSet<>(); + if (!defaultLdapAuthShouldFail()) { + // default authentication should succeed, so we should see users from LDAP + expectedUserNames.addAll(EXPECTED_LDAP_USER_NAMES); + } + + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + final SecurityManager securityManager = brokerPool.getSecurityManager(); + try (final DBBroker broker = brokerPool.get(Optional.of(securityManager.getSystemSubject()))) { + final Set actualUserNames = new HashSet(securityManager.findUsernamesWhereUsernameStarts("user")); + assertEquals(expectedUserNames, actualUserNames); + } + } + + @Test + public void findGroupnamesWhereGroupnameStarts() throws EXistException { + final Set expectedGroupNames = new HashSet<>(); + if (!defaultLdapAuthShouldFail()) { + // default authentication should succeed, so we should see users from LDAP + expectedGroupNames.addAll(EXPECTED_LDAP_GROUP_NAMES); + } + + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + final SecurityManager securityManager = brokerPool.getSecurityManager(); + try (final DBBroker broker = brokerPool.get(Optional.of(securityManager.getSystemSubject()))) { + final Set actualGroupNames = new HashSet(securityManager.findGroupnamesWhereGroupnameStarts("group")); + assertEquals(expectedGroupNames, actualGroupNames); + } + } + + @Test + public void findGroupnamesWhereGroupnameContains() throws EXistException { + final Set expectedGroupNames = new HashSet<>(); + expectedGroupNames.add("nogroup"); + if (!defaultLdapAuthShouldFail()) { + // default authentication should succeed, so we should see users from LDAP + expectedGroupNames.addAll(EXPECTED_LDAP_GROUP_NAMES); + } + + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + final SecurityManager securityManager = brokerPool.getSecurityManager(); + try (final DBBroker broker = brokerPool.get(Optional.of(securityManager.getSystemSubject()))) { + final Set actualGroupNames = new HashSet(securityManager.findGroupnamesWhereGroupnameContains("oup")); + assertEquals(expectedGroupNames, actualGroupNames); + } + } + + @Test + public void getAccount() throws EXistException { + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + final SecurityManager securityManager = brokerPool.getSecurityManager(); + try (final DBBroker broker = brokerPool.get(Optional.of(securityManager.getSystemSubject()))) { + + @Nullable final Account account = securityManager.getAccount("user2"); + if (defaultLdapAuthShouldFail()) { + // default authentication should fail, so we should not be able to retrieve the account details + assertNull(account); + + } else { + // default authentication should succeed, so we should now have the account details + assertEquals("user2", account.getUsername()); + assertEquals("user2", account.getName()); + assertEquals("group1@gb.myorg.com", account.getPrimaryGroup()); + assertArrayEquals(new String[]{ "group1@gb.myorg.com", "group2@gb.myorg.com", "ldap-users" }, account.getGroups()); + + assertEquals("First name 2", account.getMetadataValue(AXSchemaType.FIRSTNAME)); + assertEquals("Last name 2", account.getMetadataValue(AXSchemaType.LASTNAME)); + assertEquals("First Last 2", account.getMetadataValue(AXSchemaType.FULLNAME)); + assertEquals("es", account.getMetadataValue(AXSchemaType.LANGUAGE)); + assertEquals("user2@mail.com", account.getMetadataValue(AXSchemaType.EMAIL)); + } + } + } + + @Test + public void hasAccount() throws EXistException { + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + final SecurityManager securityManager = brokerPool.getSecurityManager(); + try (final DBBroker broker = brokerPool.get(Optional.of(securityManager.getSystemSubject()))) { + + if (defaultLdapAuthShouldFail()) { + // default authentication should fail, so we should not be able to retrieve the account details + assertFalse(securityManager.hasAccount("user2")); + + } else { + // default authentication should succeed, so we should now have the account details + assertTrue(securityManager.hasAccount("user2")); + } + + assertFalse(securityManager.hasAccount("no-such-account-exists")); + } + } + + @Test + public void getGroup() throws EXistException, PermissionDeniedException { + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + final SecurityManager securityManager = brokerPool.getSecurityManager(); + try (final DBBroker broker = brokerPool.get(Optional.of(securityManager.getSystemSubject()))) { + + @Nullable final Group group = securityManager.getGroup("group2"); + if (defaultLdapAuthShouldFail()) { + // default authentication should fail, so we should not be able to retrieve the group details + assertNull(group); + + } else { + // default authentication should succeed, so we should now have the group details + assertEquals("group2", group.getName()); + assertEquals(Collections.emptyList(), group.getManagers()); + + assertEquals("Group 2", group.getMetadataValue(EXistSchemaType.DESCRIPTION)); + } + } + } + + @Test + public void hasGroup() throws EXistException { + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + final SecurityManager securityManager = brokerPool.getSecurityManager(); + try (final DBBroker broker = brokerPool.get(Optional.of(securityManager.getSystemSubject()))) { + + if (defaultLdapAuthShouldFail()) { + // default authentication should fail, so we should not be able to retrieve the group details + assertFalse(securityManager.hasGroup("group2")); + + } else { + // default authentication should succeed, so we should now have the group details + assertTrue(securityManager.hasGroup("group2")); + } + + assertFalse(securityManager.hasGroup("no-such-group-exists")); + } + } + + private void createLocalLdapUsersGroup(final ExistEmbeddedServer existEmbeddedServer) throws EXistException, PermissionDeniedException { + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + final SecurityManager securityManager = brokerPool.getSecurityManager(); + try (final DBBroker broker = brokerPool.get(Optional.of(securityManager.getSystemSubject()))) { + final Group localLdapUsersGroup = new GroupAider(RealmImpl.ID, "ldap-users"); + securityManager.addGroup(broker, localLdapUsersGroup); + } + } + + private void configureLdapRealm(final ExistEmbeddedServer existEmbeddedServer) throws EXistException, PermissionDeniedException, IOException, SAXException, URISyntaxException, LockException { + final BrokerPool brokerPool = existEmbeddedServer.getBrokerPool(); + try (final DBBroker broker = brokerPool.get(Optional.of(brokerPool.getSecurityManager().getSystemSubject())); + final Txn transaction = brokerPool.getTransactionManager().beginTransaction()) { + + final XmldbURI securityCollectionUri = XmldbURI.SYSTEM.append("security"); + + try (final Collection securityCollection = broker.getOrCreateCollection(transaction, securityCollectionUri)) { + + final StringInputSource securityConfigInputSource = new StringInputSource(loadSecurityConfigXml()); + final MediaType xmlMediaType = broker.getBrokerPool().getMediaTypeService().getMediaTypeResolver().fromString(MediaType.APPLICATION_XML); + broker.storeDocument(transaction, XmldbURI.create("config.xml"), securityConfigInputSource, xmlMediaType, securityCollection); + + // NOTE: early release of Collection lock inline with Asymmetrical Locking scheme + securityCollection.close(); + } + } + } + + private String loadSecurityConfigXml() throws URISyntaxException, IOException { + final ElementalDbSecurityConfig securityConfigAnnotation = getClass().getAnnotation(ElementalDbSecurityConfig.class); + final String securityConfigFileName = securityConfigAnnotation.fileName(); + final URL configUrl = getClass().getResource("/" + securityConfigFileName); + final byte[] data = Files.readAllBytes(Paths.get(configUrl.toURI())); + final String str = new String(data, StandardCharsets.UTF_8); + return str.replace("ldap://localhost:389", "ldap://localhost:" + ldapServer.getPort() + ""); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + public @interface ElementalDbSecurityConfig { + String fileName(); + } + + public enum PrincipalPattern { + ABSENT, + PRESENT_VALID, + PRESENT_INVALID + } + + public enum DefaultUser { + ABSENT, + PRESENT_VALID_QUALIFIED, + PRESENT_VALID_UNQUALIFIED, + PRESENT_INVALID_QUALIFIED, + PRESENT_INVALID_UNQUALIFIED, + } +} diff --git a/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisIqduIT.java b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisIqduIT.java new file mode 100644 index 0000000000..7068174cb2 --- /dev/null +++ b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisIqduIT.java @@ -0,0 +1,56 @@ +/* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.security.realm.ldap; + +import org.apache.directory.server.annotations.CreateLdapServer; +import org.apache.directory.server.annotations.CreateTransport; +import org.apache.directory.server.core.annotations.ApplyLdifFiles; +import org.apache.directory.server.core.annotations.CreateDS; +import org.apache.directory.server.core.annotations.CreatePartition; +import org.apache.directory.server.core.annotations.LoadSchema; +import org.apache.directory.server.core.integ.ApacheDSTestExtension; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Integration Tests against an LDAP Server with NIS Schema + * where the Elemental Security Config has: + *
    + *
  • default-username that is fully qualified but invalid
  • + *
+ */ +@ExtendWith(ApacheDSTestExtension.class) +@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP") }, allowAnonymousAccess = true) +@CreateDS(name = "myDS", + loadedSchemas = { + @LoadSchema(name = "nis"), + }, + partitions = { + @CreatePartition(name = "test", suffix = "dc=gb,dc=myorg,dc=com") + } +) +@ApplyLdifFiles({"ldap-example-1.ldif"}) +@AbstractLDAPRealmNisIT.ElementalDbSecurityConfig(fileName = "ldap-example-1-iqdu.config.xml") +public class LDAPRealmNisIqduIT extends AbstractLDAPRealmNisIT { + + public LDAPRealmNisIqduIT() { + super(PrincipalPattern.ABSENT, DefaultUser.PRESENT_INVALID_QUALIFIED); + } +} diff --git a/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisIuduIT.java b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisIuduIT.java new file mode 100644 index 0000000000..dcf89f6bee --- /dev/null +++ b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisIuduIT.java @@ -0,0 +1,56 @@ +/* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.security.realm.ldap; + +import org.apache.directory.server.annotations.CreateLdapServer; +import org.apache.directory.server.annotations.CreateTransport; +import org.apache.directory.server.core.annotations.ApplyLdifFiles; +import org.apache.directory.server.core.annotations.CreateDS; +import org.apache.directory.server.core.annotations.CreatePartition; +import org.apache.directory.server.core.annotations.LoadSchema; +import org.apache.directory.server.core.integ.ApacheDSTestExtension; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Integration Tests against an LDAP Server with NIS Schema + * where the Elemental Security Config has: + *
    + *
  • default-username that is unqualified but invalid
  • + *
+ */ +@ExtendWith(ApacheDSTestExtension.class) +@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP") }, allowAnonymousAccess = true) +@CreateDS(name = "myDS", + loadedSchemas = { + @LoadSchema(name = "nis"), + }, + partitions = { + @CreatePartition(name = "test", suffix = "dc=gb,dc=myorg,dc=com") + } +) +@ApplyLdifFiles({"ldap-example-1.ldif"}) +@AbstractLDAPRealmNisIT.ElementalDbSecurityConfig(fileName = "ldap-example-1-iudu.config.xml") +public class LDAPRealmNisIuduIT extends AbstractLDAPRealmNisIT { + + public LDAPRealmNisIuduIT() { + super(PrincipalPattern.ABSENT, DefaultUser.PRESENT_INVALID_UNQUALIFIED); + } +} diff --git a/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisPpIqduIT.java b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisPpIqduIT.java new file mode 100644 index 0000000000..cd60a57abb --- /dev/null +++ b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisPpIqduIT.java @@ -0,0 +1,57 @@ +/* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.security.realm.ldap; + +import org.apache.directory.server.annotations.CreateLdapServer; +import org.apache.directory.server.annotations.CreateTransport; +import org.apache.directory.server.core.annotations.ApplyLdifFiles; +import org.apache.directory.server.core.annotations.CreateDS; +import org.apache.directory.server.core.annotations.CreatePartition; +import org.apache.directory.server.core.annotations.LoadSchema; +import org.apache.directory.server.core.integ.ApacheDSTestExtension; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Integration Tests against an LDAP Server with NIS Schema + * where the Elemental Security Config has: + *
    + *
  • principal-pattern
  • + *
  • default-username that is fully qualified but invalid
  • + *
+ */ +@ExtendWith(ApacheDSTestExtension.class) +@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP") }, allowAnonymousAccess = true) +@CreateDS(name = "myDS", + loadedSchemas = { + @LoadSchema(name = "nis"), + }, + partitions = { + @CreatePartition(name = "test", suffix = "dc=gb,dc=myorg,dc=com") + } +) +@ApplyLdifFiles({"ldap-example-1.ldif"}) +@AbstractLDAPRealmNisIT.ElementalDbSecurityConfig(fileName = "ldap-example-1-pp-iqdu.config.xml") +public class LDAPRealmNisPpIqduIT extends AbstractLDAPRealmNisIT { + + public LDAPRealmNisPpIqduIT() { + super(PrincipalPattern.PRESENT_VALID, DefaultUser.PRESENT_INVALID_QUALIFIED); + } +} diff --git a/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisPpIuduIT.java b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisPpIuduIT.java new file mode 100644 index 0000000000..5e5b8573dd --- /dev/null +++ b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisPpIuduIT.java @@ -0,0 +1,57 @@ +/* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.security.realm.ldap; + +import org.apache.directory.server.annotations.CreateLdapServer; +import org.apache.directory.server.annotations.CreateTransport; +import org.apache.directory.server.core.annotations.ApplyLdifFiles; +import org.apache.directory.server.core.annotations.CreateDS; +import org.apache.directory.server.core.annotations.CreatePartition; +import org.apache.directory.server.core.annotations.LoadSchema; +import org.apache.directory.server.core.integ.ApacheDSTestExtension; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Integration Tests against an LDAP Server with NIS Schema + * where the Elemental Security Config has: + *
    + *
  • principal-pattern
  • + *
  • default-username that is unqualified but invalid
  • + *
+ */ +@ExtendWith(ApacheDSTestExtension.class) +@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP") }, allowAnonymousAccess = true) +@CreateDS(name = "myDS", + loadedSchemas = { + @LoadSchema(name = "nis"), + }, + partitions = { + @CreatePartition(name = "test", suffix = "dc=gb,dc=myorg,dc=com") + } +) +@ApplyLdifFiles({"ldap-example-1.ldif"}) +@AbstractLDAPRealmNisIT.ElementalDbSecurityConfig(fileName = "ldap-example-1-pp-iudu.config.xml") +public class LDAPRealmNisPpIuduIT extends AbstractLDAPRealmNisIT { + + public LDAPRealmNisPpIuduIT() { + super(PrincipalPattern.PRESENT_VALID, DefaultUser.PRESENT_INVALID_UNQUALIFIED); + } +} diff --git a/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisPpVqduIT.java b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisPpVqduIT.java new file mode 100644 index 0000000000..d7d49af408 --- /dev/null +++ b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisPpVqduIT.java @@ -0,0 +1,57 @@ +/* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.security.realm.ldap; + +import org.apache.directory.server.annotations.CreateLdapServer; +import org.apache.directory.server.annotations.CreateTransport; +import org.apache.directory.server.core.annotations.ApplyLdifFiles; +import org.apache.directory.server.core.annotations.CreateDS; +import org.apache.directory.server.core.annotations.CreatePartition; +import org.apache.directory.server.core.annotations.LoadSchema; +import org.apache.directory.server.core.integ.ApacheDSTestExtension; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Integration Tests against an LDAP Server with NIS Schema + * where the Elemental Security Config has: + *
    + *
  • principal-pattern
  • + *
  • default-username that is fully qualified and valid
  • + *
+ */ +@ExtendWith(ApacheDSTestExtension.class) +@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP") }, allowAnonymousAccess = true) +@CreateDS(name = "myDS", + loadedSchemas = { + @LoadSchema(name = "nis"), + }, + partitions = { + @CreatePartition(name = "test", suffix = "dc=gb,dc=myorg,dc=com") + } +) +@ApplyLdifFiles({"ldap-example-1.ldif"}) +@AbstractLDAPRealmNisIT.ElementalDbSecurityConfig(fileName = "ldap-example-1-pp-vqdu.config.xml") +public class LDAPRealmNisPpVqduIT extends AbstractLDAPRealmNisIT { + + public LDAPRealmNisPpVqduIT() { + super(PrincipalPattern.PRESENT_VALID, DefaultUser.PRESENT_VALID_QUALIFIED); + } +} diff --git a/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisPpVuduIT.java b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisPpVuduIT.java new file mode 100644 index 0000000000..c5dffd7228 --- /dev/null +++ b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisPpVuduIT.java @@ -0,0 +1,57 @@ +/* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.security.realm.ldap; + +import org.apache.directory.server.annotations.CreateLdapServer; +import org.apache.directory.server.annotations.CreateTransport; +import org.apache.directory.server.core.annotations.ApplyLdifFiles; +import org.apache.directory.server.core.annotations.CreateDS; +import org.apache.directory.server.core.annotations.CreatePartition; +import org.apache.directory.server.core.annotations.LoadSchema; +import org.apache.directory.server.core.integ.ApacheDSTestExtension; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Integration Tests against an LDAP Server with NIS Schema + * where the Elemental Security Config has: + *
    + *
  • principal-pattern
  • + *
  • default-username that is unqualified and valid
  • + *
+ */ +@ExtendWith(ApacheDSTestExtension.class) +@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP") }, allowAnonymousAccess = true) +@CreateDS(name = "myDS", + loadedSchemas = { + @LoadSchema(name = "nis"), + }, + partitions = { + @CreatePartition(name = "test", suffix = "dc=gb,dc=myorg,dc=com") + } +) +@ApplyLdifFiles({"ldap-example-1.ldif"}) +@AbstractLDAPRealmNisIT.ElementalDbSecurityConfig(fileName = "ldap-example-1-pp-vudu.config.xml") +public class LDAPRealmNisPpVuduIT extends AbstractLDAPRealmNisIT { + + public LDAPRealmNisPpVuduIT() { + super(PrincipalPattern.PRESENT_VALID, DefaultUser.PRESENT_VALID_UNQUALIFIED); + } +} diff --git a/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisVqduIT.java b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisVqduIT.java new file mode 100644 index 0000000000..c5012c127b --- /dev/null +++ b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisVqduIT.java @@ -0,0 +1,56 @@ +/* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.security.realm.ldap; + +import org.apache.directory.server.annotations.CreateLdapServer; +import org.apache.directory.server.annotations.CreateTransport; +import org.apache.directory.server.core.annotations.ApplyLdifFiles; +import org.apache.directory.server.core.annotations.CreateDS; +import org.apache.directory.server.core.annotations.CreatePartition; +import org.apache.directory.server.core.annotations.LoadSchema; +import org.apache.directory.server.core.integ.ApacheDSTestExtension; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Integration Tests against an LDAP Server with NIS Schema + * where the Elemental Security Config has: + *
    + *
  • default-username that is fully qualified and valid
  • + *
+ */ +@ExtendWith(ApacheDSTestExtension.class) +@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP") }, allowAnonymousAccess = true) +@CreateDS(name = "myDS", + loadedSchemas = { + @LoadSchema(name = "nis"), + }, + partitions = { + @CreatePartition(name = "test", suffix = "dc=gb,dc=myorg,dc=com") + } +) +@ApplyLdifFiles({"ldap-example-1.ldif"}) +@AbstractLDAPRealmNisIT.ElementalDbSecurityConfig(fileName = "ldap-example-1-vqdu.config.xml") +public class LDAPRealmNisVqduIT extends AbstractLDAPRealmNisIT { + + public LDAPRealmNisVqduIT() { + super(PrincipalPattern.ABSENT, DefaultUser.PRESENT_VALID_QUALIFIED); + } +} diff --git a/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisVuduIT.java b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisVuduIT.java new file mode 100644 index 0000000000..1c9cbfc531 --- /dev/null +++ b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmNisVuduIT.java @@ -0,0 +1,56 @@ +/* + * Elemental + * Copyright (C) 2024, Evolved Binary Ltd + * + * admin@evolvedbinary.com + * https://www.evolvedbinary.com | https://www.elemental.xyz + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; version 2.1. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.security.realm.ldap; + +import org.apache.directory.server.annotations.CreateLdapServer; +import org.apache.directory.server.annotations.CreateTransport; +import org.apache.directory.server.core.annotations.ApplyLdifFiles; +import org.apache.directory.server.core.annotations.CreateDS; +import org.apache.directory.server.core.annotations.CreatePartition; +import org.apache.directory.server.core.annotations.LoadSchema; +import org.apache.directory.server.core.integ.ApacheDSTestExtension; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * Integration Tests against an LDAP Server with NIS Schema + * where the Elemental Security Config has: + *
    + *
  • default-username that is unqualified and valid
  • + *
+ */ +@ExtendWith(ApacheDSTestExtension.class) +@CreateLdapServer(transports = {@CreateTransport(protocol = "LDAP") }, allowAnonymousAccess = true) +@CreateDS(name = "myDS", + loadedSchemas = { + @LoadSchema(name = "nis"), + }, + partitions = { + @CreatePartition(name = "test", suffix = "dc=gb,dc=myorg,dc=com") + } +) +@ApplyLdifFiles({"ldap-example-1.ldif"}) +@AbstractLDAPRealmNisIT.ElementalDbSecurityConfig(fileName = "ldap-example-1-vudu.config.xml") +public class LDAPRealmNisVuduIT extends AbstractLDAPRealmNisIT { + + public LDAPRealmNisVuduIT() { + super(PrincipalPattern.ABSENT, DefaultUser.PRESENT_VALID_UNQUALIFIED); + } +} diff --git a/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmTest.java b/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmTest.java deleted file mode 100644 index b979fcf38c..0000000000 --- a/extensions/security/ldap/src/test/java/org/exist/security/realm/ldap/LDAPRealmTest.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * eXist-db Open Source Native XML Database - * Copyright (C) 2001 The eXist-db Authors - * - * info@exist-db.org - * http://www.exist-db.org - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ -package org.exist.security.realm.ldap; - -import static org.junit.Assert.*; - -import java.io.InputStream; - -import org.exist.config.Configuration; -import org.exist.config.Configurator; -import org.exist.security.AuthenticationException; -import org.exist.security.Account; -import org.apache.commons.io.input.UnsynchronizedByteArrayInputStream; -import org.junit.AfterClass; -import org.junit.BeforeClass; -import org.junit.Ignore; -import org.junit.Test; - -import static java.nio.charset.StandardCharsets.UTF_8; - -/** - * @author Dmitriy Shabanov - * - */ -public class LDAPRealmTest { - - private static String config = - "" + - " " + - " cn={0},dc=local" + - " ldap://localhost:389" + - " " + - ""; - - private static LDAPRealm realm; - - /** - * @throws java.lang.Exception - */ - @BeforeClass - public static void setUpBeforeClass() throws Exception { - try (final InputStream is = new UnsynchronizedByteArrayInputStream(config.getBytes(UTF_8))) { - Configuration config = Configurator.parse(is); - realm = new LDAPRealm(null, config); - } - } - - /** - * @throws java.lang.Exception - */ - @AfterClass - public static void tearDownAfterClass() { - } - - /** - * Test method for {@link org.exist.security.realm.ldap.LDAPRealm#authenticate(java.lang.String, java.lang.Object)}. - */ - @Ignore - @Test - public void testAuthenticate() { - Account account = null; - try { - account = realm.authenticate("admin", "passwd"); - } catch (AuthenticationException e) { - fail(e.getMessage()); - } - - assertNotNull(account); - } - -} diff --git a/extensions/security/ldap/src/test/resources-filtered/conf.xml b/extensions/security/ldap/src/test/resources-filtered/conf.xml new file mode 100644 index 0000000000..e9949d1b9b --- /dev/null +++ b/extensions/security/ldap/src/test/resources-filtered/conf.xml @@ -0,0 +1,791 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/security/ldap/src/test/resources/ldap-example-1-iqdu.config.xml b/extensions/security/ldap/src/test/resources/ldap-example-1-iqdu.config.xml new file mode 100644 index 0000000000..4b0c05d8eb --- /dev/null +++ b/extensions/security/ldap/src/test/resources/ldap-example-1-iqdu.config.xml @@ -0,0 +1,77 @@ + + + + /authentication/login + + + simple + false + ldap://localhost:389 + gb.myorg.com + + dc=gb,dc=myorg,dc=com + + + user1,ou=Users,dc=gb,dc=myorg,dc=com + user1 + + objectClass=inetOrgPerson + uidNumber + uid + uid + gidNumber + + givenName + sn + displayName + preferredLanguage + mail + + + + objectClass=posixGroup + gidNumber + cn + memberUid + + description + + + group1 + group2 + + + + + + ldap-users + + + + + + \ No newline at end of file diff --git a/extensions/security/ldap/src/test/resources/ldap-example-1-iudu.config.xml b/extensions/security/ldap/src/test/resources/ldap-example-1-iudu.config.xml new file mode 100644 index 0000000000..7875605a65 --- /dev/null +++ b/extensions/security/ldap/src/test/resources/ldap-example-1-iudu.config.xml @@ -0,0 +1,77 @@ + + + + /authentication/login + + + simple + false + ldap://localhost:389 + gb.myorg.com + + dc=gb,dc=myorg,dc=com + + + no-such-username + no-such-password + + objectClass=inetOrgPerson + uidNumber + uid + uid + gidNumber + + givenName + sn + displayName + preferredLanguage + mail + + + + objectClass=posixGroup + gidNumber + cn + memberUid + + description + + + group1 + group2 + + + + + + ldap-users + + + + + + \ No newline at end of file diff --git a/extensions/security/ldap/src/test/resources/ldap-example-1-pp-iqdu.config.xml b/extensions/security/ldap/src/test/resources/ldap-example-1-pp-iqdu.config.xml new file mode 100644 index 0000000000..f0fef382b1 --- /dev/null +++ b/extensions/security/ldap/src/test/resources/ldap-example-1-pp-iqdu.config.xml @@ -0,0 +1,80 @@ + + + + /authentication/login + + + simple + false + ldap://localhost:389 + gb.myorg.com + uid={0},ou=Users,dc=gb,dc=myorg,dc=com + + dc=gb,dc=myorg,dc=com + + + user1,ou=Users,dc=gb,dc=myorg,dc=com + + user1 + + objectClass=inetOrgPerson + uidNumber + uid + uid + gidNumber + + givenName + sn + displayName + preferredLanguage + mail + + + + objectClass=posixGroup + gidNumber + cn + memberUid + + description + + + group1 + group2 + + + + + + ldap-users + + + + + + \ No newline at end of file diff --git a/extensions/security/ldap/src/test/resources/ldap-example-1-pp-iudu.config.xml b/extensions/security/ldap/src/test/resources/ldap-example-1-pp-iudu.config.xml new file mode 100644 index 0000000000..a3c5d81ece --- /dev/null +++ b/extensions/security/ldap/src/test/resources/ldap-example-1-pp-iudu.config.xml @@ -0,0 +1,79 @@ + + + + /authentication/login + + + simple + false + ldap://localhost:389 + gb.myorg.com + uid={0},ou=Users,dc=gb,dc=myorg,dc=com + + dc=gb,dc=myorg,dc=com + + + no-such-username + no-such-password + + objectClass=inetOrgPerson + uidNumber + uid + uid + gidNumber + + givenName + sn + displayName + preferredLanguage + mail + + + + objectClass=posixGroup + gidNumber + cn + memberUid + + description + + + group1 + group2 + + + + + + ldap-users + + + + + + \ No newline at end of file diff --git a/extensions/security/ldap/src/test/resources/ldap-example-1-pp-vqdu.config.xml b/extensions/security/ldap/src/test/resources/ldap-example-1-pp-vqdu.config.xml new file mode 100644 index 0000000000..f7b9eb2245 --- /dev/null +++ b/extensions/security/ldap/src/test/resources/ldap-example-1-pp-vqdu.config.xml @@ -0,0 +1,78 @@ + + + + /authentication/login + + + simple + false + ldap://localhost:389 + gb.myorg.com + uid={0},ou=Users,dc=gb,dc=myorg,dc=com + + dc=gb,dc=myorg,dc=com + + uid=user1,ou=Users,dc=gb,dc=myorg,dc=com + user1 + + objectClass=inetOrgPerson + uidNumber + uid + uid + gidNumber + + givenName + sn + displayName + preferredLanguage + mail + + + + objectClass=posixGroup + gidNumber + cn + memberUid + + description + + + group1 + group2 + + + + + + ldap-users + + + + + + \ No newline at end of file diff --git a/extensions/security/ldap/src/test/resources/ldap-example-1-pp-vudu.config.xml b/extensions/security/ldap/src/test/resources/ldap-example-1-pp-vudu.config.xml new file mode 100644 index 0000000000..8f6c6ea061 --- /dev/null +++ b/extensions/security/ldap/src/test/resources/ldap-example-1-pp-vudu.config.xml @@ -0,0 +1,77 @@ + + + + /authentication/login + + + simple + false + ldap://localhost:389 + gb.myorg.com + uid={0},ou=Users,dc=gb,dc=myorg,dc=com + + dc=gb,dc=myorg,dc=com + user1 + user1 + + objectClass=inetOrgPerson + uidNumber + uid + uid + gidNumber + + givenName + sn + displayName + preferredLanguage + mail + + + + objectClass=posixGroup + gidNumber + cn + memberUid + + description + + + group1 + group2 + + + + + + ldap-users + + + + + + \ No newline at end of file diff --git a/extensions/security/ldap/src/test/resources/ldap-example-1-vqdu.config.xml b/extensions/security/ldap/src/test/resources/ldap-example-1-vqdu.config.xml new file mode 100644 index 0000000000..c41b18b95c --- /dev/null +++ b/extensions/security/ldap/src/test/resources/ldap-example-1-vqdu.config.xml @@ -0,0 +1,75 @@ + + + + /authentication/login + + + simple + false + ldap://localhost:389 + gb.myorg.com + + dc=gb,dc=myorg,dc=com + uid=user1,ou=Users,dc=gb,dc=myorg,dc=com + user1 + + objectClass=inetOrgPerson + uidNumber + uid + uid + gidNumber + + givenName + sn + displayName + preferredLanguage + mail + + + + objectClass=posixGroup + gidNumber + cn + memberUid + + description + + + group1 + group2 + + + + + + ldap-users + + + + + + \ No newline at end of file diff --git a/extensions/security/ldap/src/test/resources/ldap-example-1-vudu.config.xml b/extensions/security/ldap/src/test/resources/ldap-example-1-vudu.config.xml new file mode 100644 index 0000000000..a1062ab2ca --- /dev/null +++ b/extensions/security/ldap/src/test/resources/ldap-example-1-vudu.config.xml @@ -0,0 +1,75 @@ + + + + /authentication/login + + + simple + false + ldap://localhost:389 + gb.myorg.com + + dc=gb,dc=myorg,dc=com + user1 + user1 + + objectClass=inetOrgPerson + uidNumber + uid + uid + gidNumber + + givenName + sn + displayName + preferredLanguage + mail + + + + objectClass=posixGroup + gidNumber + cn + memberUid + + description + + + group1 + group2 + + + + + + ldap-users + + + + + + \ No newline at end of file diff --git a/extensions/security/ldap/src/test/resources/ldap-example-1.ldif b/extensions/security/ldap/src/test/resources/ldap-example-1.ldif new file mode 100644 index 0000000000..4550efa81b --- /dev/null +++ b/extensions/security/ldap/src/test/resources/ldap-example-1.ldif @@ -0,0 +1,109 @@ +# +# Elemental +# Copyright (C) 2024, Evolved Binary Ltd +# +# admin@evolvedbinary.com +# https://www.evolvedbinary.com | https://www.elemental.xyz +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; version 2.1. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# + +version: 1 +dn: dc=gb,dc=myorg,dc=com +objectClass: domain +objectClass: top +dc: gb + +dn: ou=Users,dc=gb,dc=myorg,dc=com +objectClass: organizationalUnit +objectClass: top +ou: Users + +dn: ou=Groups,dc=gb,dc=myorg,dc=com +objectClass: organizationalUnit +objectClass: top +ou: Groups + +dn: cn=group1,ou=Groups,dc=gb,dc=myorg,dc=com +objectClass: top +objectClass: posixGroup +cn: group1 +description: Group 1 +gidNumber: 10001 +memberUid: user1 +memberUid: user2 + +dn: cn=group2,ou=Groups,dc=gb,dc=myorg,dc=com +objectClass: top +objectClass: posixGroup +cn: group2 +description: Group 2 +gidNumber: 20001 +memberUid: user2 + +dn: cn=group3,ou=Groups,dc=gb,dc=myorg,dc=com +objectClass: top +objectClass: posixGroup +cn: group3 +description: Group 3 +gidNumber: 30001 +memberUid: user3 + +dn: uid=user1,ou=Users,dc=gb,dc=myorg,dc=com +objectClass: inetOrgPerson +objectClass: posixAccount +cn: User 1 +givenName: First name 1 +sn: Last name 1 +displayName: First Last 1 +preferredLanguage: en_GB +mail: user1@mail.com +homeDirectory: /home/user1 +uid: user1 +userPassword: user1 +uidNumber: 10001 +# primary group = group1 +gidNumber: 10001 + +dn: uid=user2,ou=Users,dc=gb,dc=myorg,dc=com +objectClass: inetOrgPerson +objectClass: posixAccount +cn: User 2 +givenName: First name 2 +sn: Last name 2 +displayName: First Last 2 +preferredLanguage: es +mail: user2@mail.com +homeDirectory: /home/user2 +uid: user2 +userPassword: user2 +uidNumber: 10002 +# primary group = group1 +gidNumber: 10001 + +dn: uid=user3,ou=Users,dc=gb,dc=myorg,dc=com +objectClass: inetOrgPerson +objectClass: posixAccount +cn: User 3 +givenName: First name 3 +sn: Last name 3 +displayName: First Last 3 +preferredLanguage: it +mail: user3@mail.com +homeDirectory: /home/user3 +uid: user3 +userPassword: user3 +uidNumber: 10003 +# primary group = group3 +gidNumber: 30001 diff --git a/extensions/security/activedirectory/src/test/resources/log4j2.xml b/extensions/security/ldap/src/test/resources/log4j2.xml similarity index 93% rename from extensions/security/activedirectory/src/test/resources/log4j2.xml rename to extensions/security/ldap/src/test/resources/log4j2.xml index f061d56ce8..8bcd14d56d 100644 --- a/extensions/security/activedirectory/src/test/resources/log4j2.xml +++ b/extensions/security/ldap/src/test/resources/log4j2.xml @@ -50,12 +50,9 @@ ${log4j:configParentLocation}/../../target/test-logs-${date:yyyyMMddHHmmssSSS} 10MB - 0 0 0 * * ? 14 %d{yyyyMMddHHmmss} - %d{yyyy-MM-dd} - %d [%t] %-5p (%F [%M]:%L) - %m%n - %m%n + %d [%t] %-5p (%F [%M]:%L) - %m %n @@ -123,15 +120,7 @@ - - - - - - - - - + @@ -175,7 +164,7 @@ - + @@ -209,10 +198,6 @@ - - - - diff --git a/extensions/security/pom.xml b/extensions/security/pom.xml index 2eea488d68..1b7e8d871e 100644 --- a/extensions/security/pom.xml +++ b/extensions/security/pom.xml @@ -70,7 +70,6 @@ - activedirectory iprange ldap @@ -126,4 +125,4 @@ - \ No newline at end of file +