From 678d30b42737b8635f2e7295da63b9395bed87db Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Thu, 12 Dec 2024 20:29:04 +0100 Subject: [PATCH 01/14] OF-2924: Reduce duplicated code in User multi-providers This moves mostly duplicated code from HybridUserProvider and MappedUserProvider in their common superclass, UserMultiProvider Slight functional changes have been introduces, which intent to make behavior more consistent. --- .../openfire/user/HybridUserProvider.java | 141 ++---------------- .../openfire/user/MappedUserProvider.java | 51 +------ .../openfire/user/UserMultiProvider.java | 102 ++++++++++++- 3 files changed, 113 insertions(+), 181 deletions(-) diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/user/HybridUserProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/user/HybridUserProvider.java index 69f09023a7..f37d0e4f24 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/user/HybridUserProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/user/HybridUserProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 IgniteRealtime.org, 2018-2019 Ignite Realtime Foundation. All rights reserved + * Copyright (C) 2016 IgniteRealtime.org, 2018-2024 Ignite Realtime Foundation. All rights reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -79,57 +79,6 @@ protected List getUserProviders() return userProviders; } - /** - * Creates a new user in the first non-read-only provider. - * - * @param username the username. - * @param password the plain-text password. - * @param name the user's name, which can be {@code null}, unless isNameRequired is set to true. - * @param email the user's email address, which can be {@code null}, unless isEmailRequired is set to true. - * @return The user that was created. - * @throws UserAlreadyExistsException if the user already exists - */ - @Override - public User createUser( String username, String password, String name, String email ) throws UserAlreadyExistsException - { - // create the user (first writable provider wins) - for ( final UserProvider provider : getUserProviders() ) - { - if ( provider.isReadOnly() ) - { - continue; - } - return provider.createUser( username, password, name, email ); - } - - // all providers are read-only - throw new UnsupportedOperationException(); - } - - /** - * Removes a user from all non-read-only providers. - * - * @param username the username to delete. - */ - @Override - public void deleteUser( String username ) - { - // all providers are read-only - if ( isReadOnly() ) - { - throw new UnsupportedOperationException(); - } - - for ( final UserProvider provider : getUserProviders() ) - { - if ( provider.isReadOnly() ) - { - continue; - } - provider.deleteUser( username ); - } - } - /** * Returns the first provider that contains the user, or the first provider that is not read-only when the user * does not exist in any provider. @@ -137,25 +86,22 @@ public void deleteUser( String username ) * @param username the username (cannot be null or empty). * @return The user provider (never null) */ - public UserProvider getUserProvider( String username ) + @Override + public UserProvider getUserProvider(String username) { UserProvider nonReadOnly = null; - for ( final UserProvider provider : getUserProviders() ) + for (final UserProvider provider : getUserProviders()) { try { - provider.loadUser( username ); + provider.loadUser(username); return provider; } - catch ( UserNotFoundException unfe ) + catch (UserNotFoundException unfe) { - if ( Log.isDebugEnabled() ) - { - Log.debug( "User {} not found by UserProvider {}", username, provider.getClass().getName() ); - } + Log.debug( "User {} not found by UserProvider {}", username, provider.getClass().getName() ); - if ( nonReadOnly == null && !provider.isReadOnly() ) - { + if (nonReadOnly == null && !provider.isReadOnly()) { nonReadOnly = provider; } } @@ -180,78 +126,17 @@ public UserProvider getUserProvider( String username ) @Override public User loadUser( String username ) throws UserNotFoundException { - for ( UserProvider provider : userProviders ) + // Override the implementation in the superclass to prevent obtaining the user twice. + for (UserProvider provider : userProviders) { - try - { + try { return provider.loadUser( username ); - } - catch ( UserNotFoundException unfe ) - { - if ( Log.isDebugEnabled() ) - { - Log.debug( "User {} not found by UserProvider {}", username, provider.getClass().getName() ); - } + } catch ( UserNotFoundException unfe ) { + Log.debug("User {} not found by UserProvider {}", username, provider.getClass().getName()); } } //if we get this far, no provider was able to load the user throw new UserNotFoundException(); } - - /** - * Changes the creation date of a user in the first provider that contains the user. - * - * @param username the username. - * @param creationDate the date the user was created. - * @throws UserNotFoundException when the user was not found in any provider. - * @throws UnsupportedOperationException when the provider is read-only. - */ - @Override - public void setCreationDate( String username, Date creationDate ) throws UserNotFoundException - { - getUserProvider( username ).setCreationDate( username, creationDate ); - } - - /** - * Changes the modification date of a user in the first provider that contains the user. - * - * @param username the username. - * @param modificationDate the date the user was (last) modified. - * @throws UserNotFoundException when the user was not found in any provider. - * @throws UnsupportedOperationException when the provider is read-only. - */ - @Override - public void setModificationDate( String username, Date modificationDate ) throws UserNotFoundException - { - getUserProvider( username ).setCreationDate( username, modificationDate ); - } - - /** - * Changes the full name of a user in the first provider that contains the user. - * - * @param username the username. - * @param name the new full name a user. - * @throws UserNotFoundException when the user was not found in any provider. - * @throws UnsupportedOperationException when the provider is read-only. - */ - @Override - public void setName( String username, String name ) throws UserNotFoundException - { - getUserProvider( username ).setEmail( username, name ); - } - - /** - * Changes the email address of a user in the first provider that contains the user. - * - * @param username the username. - * @param email the new email address of a user. - * @throws UserNotFoundException when the user was not found in any provider. - * @throws UnsupportedOperationException when the provider is read-only. - */ - @Override - public void setEmail( String username, String email ) throws UserNotFoundException - { - getUserProvider( username ).setEmail( username, email ); - } } diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/user/MappedUserProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/user/MappedUserProvider.java index 79caa1e3f4..7de961b73d 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/user/MappedUserProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/user/MappedUserProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 IgniteRealtime.org, 2018-2019 Ignite Realtime Foundation. All rights reserved + * Copyright (C) 2016 IgniteRealtime.org, 2018-2024 Ignite Realtime Foundation. All rights reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import org.jivesoftware.util.JiveGlobals; import java.util.Collection; -import java.util.Date; /** * A {@link UserProvider} that delegates to a user-specific UserProvider. @@ -90,52 +89,4 @@ public UserProvider getUserProvider( String username ) { return mapper.getUserProvider( username ); } - - @Override - public User loadUser( String username ) throws UserNotFoundException - { - final UserProvider userProvider; - try{ - userProvider = getUserProvider( username ); - } catch (RuntimeException e){ - throw new UserNotFoundException("Unable to identify user provider for username "+username, e); - } - return userProvider.loadUser( username ); - } - - @Override - public User createUser( String username, String password, String name, String email ) throws UserAlreadyExistsException - { - return getUserProvider( username ).createUser( username, password, name, email ); - } - - @Override - public void deleteUser( String username ) - { - getUserProvider( username ).deleteUser( username ); - } - - @Override - public void setName( String username, String name ) throws UserNotFoundException - { - getUserProvider( username ).setName( username, name ); - } - - @Override - public void setEmail( String username, String email ) throws UserNotFoundException - { - getUserProvider( username ).setEmail( username, email ); - } - - @Override - public void setCreationDate( String username, Date creationDate ) throws UserNotFoundException - { - getUserProvider( username ).setCreationDate( username, creationDate ); - } - - @Override - public void setModificationDate( String username, Date modificationDate ) throws UserNotFoundException - { - getUserProvider( username ).setModificationDate( username, modificationDate ); - } } diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/user/UserMultiProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/user/UserMultiProvider.java index 14c1c22d48..76be5e58f0 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/user/UserMultiProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/user/UserMultiProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 IgniteRealtime.org, 2018 Ignite Realtime Foundation. All rights reserved + * Copyright (C) 2016 IgniteRealtime.org, 2018-2024 Ignite Realtime Foundation. All rights reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ /** * A {@link UserProvider} that delegates to one or more 'backing' UserProvider. * - * @author GUus der Kinderen, guus@goodbytes.nl + * @author Guus der Kinderen, guus@goodbytes.nl */ public abstract class UserMultiProvider implements UserProvider { @@ -39,7 +39,7 @@ public abstract class UserMultiProvider implements UserProvider * * UserProvider classes are required to have a public, no-argument constructor. * - * @param propertyName A property name (cannot ben ull). + * @param propertyName A property name (cannot be null). * @return A user provider (can be null). */ public static UserProvider instantiate( String propertyName ) @@ -370,4 +370,100 @@ public boolean isEmailRequired() return true; } + + @Override + public User loadUser(String username) throws UserNotFoundException + { + final UserProvider userProvider; + try { + userProvider = getUserProvider( username ); + } catch (RuntimeException e){ + throw new UserNotFoundException("Unable to identify user provider for username " + username, e); + } + return userProvider.loadUser( username ); + } + + @Override + public User createUser(String username, String password, String name, String email) throws UserAlreadyExistsException + { + return getUserProvider(username).createUser(username, password, name, email); + } + + /** + * Removes a user from all non-read-only providers. + * + * @param username the username to delete. + */ + @Override + public void deleteUser(String username) + { + // all providers are read-only + if (isReadOnly()) { + throw new UnsupportedOperationException(); + } + + for (final UserProvider provider : getUserProviders()) + { + if (provider.isReadOnly()) { + continue; + } + provider.deleteUser(username); + } + } + + /** + * Changes the creation date of a user in the first provider that contains the user. + * + * @param username the identifier of the user. + * @param creationDate the date the user was created. + * @throws UserNotFoundException when the user was not found in any provider. + * @throws UnsupportedOperationException when the provider is read-only. + */ + @Override + public void setCreationDate(String username, Date creationDate) throws UserNotFoundException + { + getUserProvider(username).setCreationDate(username, creationDate); + } + + /** + * Changes the modification date of a user in the first provider that contains the user. + * + * @param username the identifier of the user. + * @param modificationDate the date the user was (last) modified. + * @throws UserNotFoundException when the user was not found in any provider. + * @throws UnsupportedOperationException when the provider is read-only. + */ + @Override + public void setModificationDate(String username, Date modificationDate) throws UserNotFoundException + { + getUserProvider(username).setModificationDate(username, modificationDate); + } + + /** + * Changes the full name of a user in the first provider that contains the user. + * + * @param username the identifier of the user. + * @param name the new full name a user. + * @throws UserNotFoundException when the user was not found in any provider. + * @throws UnsupportedOperationException when the provider is read-only. + */ + @Override + public void setName(String username, String name) throws UserNotFoundException + { + getUserProvider(username).setEmail(username, name); + } + + /** + * Changes the email address of a user in the first provider that contains the user. + * + * @param username the identifier of the user. + * @param email the new email address of a user. + * @throws UserNotFoundException when the user was not found in any provider. + * @throws UnsupportedOperationException when the provider is read-only. + */ + @Override + public void setEmail(String username, String email) throws UserNotFoundException + { + getUserProvider(username).setEmail(username, email); + } } From 8bc64c8edf1efae855c543089a1b521ba73cbdd1 Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Thu, 12 Dec 2024 21:06:17 +0100 Subject: [PATCH 02/14] OF-2924: Reduce duplicated code in User Property multi-providers This moves mostly duplicated code from HybridUserPropertyProvider and MappedUserPropertyProvider in a new, common superclass, UserPropertyMultiProvider Slight functional changes have been introduces, which intent to make behavior more consistent. --- .../property/HybridUserPropertyProvider.java | 190 ++---------------- .../property/MappedUserPropertyProvider.java | 92 +-------- .../property/UserPropertyMultiProvider.java | 173 ++++++++++++++++ 3 files changed, 200 insertions(+), 255 deletions(-) create mode 100644 xmppserver/src/main/java/org/jivesoftware/openfire/user/property/UserPropertyMultiProvider.java diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/HybridUserPropertyProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/HybridUserPropertyProvider.java index 70a2057aad..284f5e9a16 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/HybridUserPropertyProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/HybridUserPropertyProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2019 Ignite Realtime Foundation. All rights reserved + * Copyright (C) 2017-2024 Ignite Realtime Foundation. All rights reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,9 +21,7 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Map; /** * Delegate UserPropertyProvider operations among up to three configurable provider implementation classes. @@ -50,7 +48,7 @@ * * @author Guus der Kinderen, guus.der.kinderen@gmail.com */ -public class HybridUserPropertyProvider implements UserPropertyProvider +public class HybridUserPropertyProvider extends UserPropertyMultiProvider { private static final Logger Log = LoggerFactory.getLogger( HybridUserPropertyProvider.class ); @@ -87,194 +85,44 @@ public HybridUserPropertyProvider() } } - /** - * Returns the properties from the first provider that returns a non-empty collection. - * - * When none of the providers provide properties an empty collection is returned. - * - * @param username The identifier of the user (cannot be null or empty). - * @return A collection, possibly empty, never null. - */ - @Override - public Map loadProperties( String username ) - { - for ( final UserPropertyProvider provider : providers ) - { - try - { - final Map properties = provider.loadProperties( username ); - if ( !properties.isEmpty() ) - { - return properties; - } - } - catch ( UserNotFoundException e ) - { - // User not in this provider. Try other providers; - } - } - return Collections.emptyMap(); - } - - /** - * Returns a property from the first provider that returns a non-null value. - * - * This method will return null when the desired property was not defined in any provider. - * - * @param username The identifier of the user (cannot be null or empty). - * @param propName The property name (cannot be null or empty). - * @return The property value (possibly null). - */ @Override - public String loadProperty( String username, String propName ) + protected List getUserPropertyProviders() { - for ( final UserPropertyProvider provider : providers ) - { - try - { - final String property = provider.loadProperty( username, propName ); - if ( property != null ) - { - return property; - } - } - catch ( UserNotFoundException e ) - { - // User not in this provider. Try other providers; - } - } - return null; - } - - /** - * Adds a new property, updating a previous property value if one already exists. - * - * Note that the implementation of this method is equal to that of {@link #updateProperty(String, String, String)}. - * - * First, tries to find a provider that has the property for the provided user. If that provider is read-only, an - * UnsupportedOperationException is thrown. If the provider is not read-only, the existing property value will be - * updated. - * - * When the property is not defined in any provider, it will be added in the first non-read-only provider. - * - * When all providers are read-only, an UnsupportedOperationException is thrown. - * - * @param username The identifier of the user (cannot be null or empty). - * @param propName The property name (cannot be null or empty). - * @param propValue The property value (cannot be null). - */ - @Override - public void insertProperty( String username, String propName, String propValue ) throws UnsupportedOperationException - { - updateProperty( username, propName, propValue ); + return providers; } /** - * Updates a property (or adds a new property when the property does not exist). - * - * Note that the implementation of this method is equal to that of {@link #insertProperty(String, String, String)}. + * Returns the first provider that contains the user, or the first provider that is not read-only when the user + * does not exist in any provider. * - * First, tries to find a provider that has the property for the provided user. If that provider is read-only, an - * UnsupportedOperationException is thrown. If the provider is not read-only, the existing property value will be - * updated. - * - * When the property is not defined in any provider, it will be added in the first non-read-only provider. - * - * When all providers are read-only, an UnsupportedOperationException is thrown. - * - * @param username The identifier of the user (cannot be null or empty). - * @param propName The property name (cannot be null or empty). - * @param propValue The property value (cannot be null). + * @param username the username (cannot be null or empty). + * @return The user property provider (never null) */ @Override - public void updateProperty( String username, String propName, String propValue ) throws UnsupportedOperationException + public UserPropertyProvider getUserPropertyProvider(String username) { - for ( final UserPropertyProvider provider : providers ) + UserPropertyProvider nonReadOnly = null; + for (final UserPropertyProvider provider : getUserPropertyProviders()) { try { - if ( provider.loadProperty( username, propName ) != null ) - { - provider.updateProperty( username, propName, propValue ); - return; - } + provider.loadProperties(username); } - catch ( UserNotFoundException e ) + catch (UserNotFoundException unfe) { - // User not in this provider. Try other providers; - } - } + Log.debug("User {} not found by UserPropertyProvider {}", username, provider.getClass().getName()); - for ( final UserPropertyProvider provider : providers ) - { - try - { - if ( !provider.isReadOnly() ) - { - provider.insertProperty( username, propName, propValue ); - return; + if (nonReadOnly == null && !provider.isReadOnly()) { + nonReadOnly = provider; } } - catch ( UserNotFoundException e ) - { - // User not in this provider. Try other providers; - } } - throw new UnsupportedOperationException(); - } - /** - * Removes a property from all non-read-only providers. - * - * @param username The identifier of the user (cannot be null or empty). - * @param propName The property name (cannot be null or empty). - */ - @Override - public void deleteProperty( String username, String propName ) throws UnsupportedOperationException - { - // all providers are read-only - if ( isReadOnly() ) - { + // User does not exist. Return a provider suitable for creating user properties. + if (nonReadOnly == null) { throw new UnsupportedOperationException(); } - for ( final UserPropertyProvider provider : providers ) - { - if ( provider.isReadOnly() ) - { - continue; - } - - try - { - provider.deleteProperty( username, propName ); - } - catch ( UserNotFoundException e ) - { - // User not in this provider. Try other providers; - } - } - } - - /** - * Returns whether all backing providers are read-only. When read-only, properties can not be created, - * deleted, or modified. If at least one provider is not read-only, this method returns false. - * - * @return true when all backing providers are read-only, otherwise false. - */ - @Override - public boolean isReadOnly() - { - // TODO Make calls concurrent for improved throughput. - for ( final UserPropertyProvider provider : providers ) - { - // If at least one provider is not readonly, neither is this proxy. - if ( !provider.isReadOnly() ) - { - return false; - } - } - - return true; + return nonReadOnly; } } diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/MappedUserPropertyProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/MappedUserPropertyProvider.java index f6bee00dc9..c81fff7f39 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/MappedUserPropertyProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/MappedUserPropertyProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2017-2019 Ignite Realtime Foundation. All rights reserved + * Copyright (C) 2017-2024 Ignite Realtime Foundation. All rights reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,13 +15,10 @@ */ package org.jivesoftware.openfire.user.property; -import org.jivesoftware.openfire.user.UserNotFoundException; import org.jivesoftware.util.ClassUtils; import org.jivesoftware.util.JiveGlobals; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import java.util.Map; +import java.util.Collection; /** * A {@link UserPropertyProvider} that delegates to a user-specific UserPropertyProvider. @@ -46,14 +43,14 @@ * * @author Guus der Kinderen, guus@goodbytes.nl */ -public class MappedUserPropertyProvider implements UserPropertyProvider +public class MappedUserPropertyProvider extends UserPropertyMultiProvider { /** * Name of the property of which the value is expected to be the classname of the UserPropertyProviderMapper * instance to be used by instances of this class. */ public static final String PROPERTY_MAPPER_CLASSNAME = "mappedUserPropertyProvider.mapper.className"; - private static final Logger Log = LoggerFactory.getLogger( MappedUserPropertyProvider.class ); + /** * Used to determine what provider is to be used to operate on a particular user. */ @@ -82,88 +79,15 @@ public MappedUserPropertyProvider() } } - /** - * Instantiates a UserPropertyProvider based on a property value (that is expected to be a class name). When the - * property is not set, this method returns null. When the property is set, but an exception occurs while - * instantiating the class, this method logs the error and returns null. - * - * UserProvider classes are required to have a public, no-argument constructor. - * - * @param propertyName A property name (cannot ben ull). - * @return A user provider (can be null). - */ - public static UserPropertyProvider instantiate( String propertyName ) - { - final String className = JiveGlobals.getProperty( propertyName ); - if ( className == null ) - { - Log.debug( "Property '{}' is undefined. Skipping.", propertyName ); - return null; - } - Log.debug( "About to to instantiate an UserPropertyProvider '{}' based on the value of property '{}'.", className, propertyName ); - try - { - final Class c = ClassUtils.forName( className ); - final UserPropertyProvider provider = (UserPropertyProvider) c.newInstance(); - Log.debug( "Instantiated UserPropertyProvider '{}'", className ); - return provider; - } - catch ( Exception e ) - { - Log.error( "Unable to load UserPropertyProvider '{}'. Users in this provider will be disabled.", className, e ); - return null; - } - } - - @Override - public Map loadProperties( String username ) throws UserNotFoundException - { - return mapper.getUserPropertyProvider( username ).loadProperties( username ); - } - @Override - public String loadProperty( String username, String propName ) throws UserNotFoundException + Collection getUserPropertyProviders() { - return mapper.getUserPropertyProvider( username ).loadProperty( username, propName ); + return mapper.getUserPropertyProviders(); } @Override - public void insertProperty( String username, String propName, String propValue ) throws UserNotFoundException + UserPropertyProvider getUserPropertyProvider(final String username) { - mapper.getUserPropertyProvider( username ).insertProperty( username, propName, propValue ); - } - - @Override - public void updateProperty( String username, String propName, String propValue ) throws UserNotFoundException - { - mapper.getUserPropertyProvider( username ).updateProperty( username, propName, propValue ); - } - - @Override - public void deleteProperty( String username, String propName ) throws UserNotFoundException - { - mapper.getUserPropertyProvider( username ).deleteProperty( username, propName ); - } - - /** - * Returns whether all backing providers are read-only. When read-only, properties can not be created, - * deleted, or modified. If at least one provider is not read-only, this method returns false. - * - * @return true when all backing providers are read-only, otherwise false. - */ - @Override - public boolean isReadOnly() - { - // TODO Make calls concurrent for improved throughput. - for ( final UserPropertyProvider provider : mapper.getUserPropertyProviders() ) - { - // If at least one provider is not readonly, neither is this proxy. - if ( !provider.isReadOnly() ) - { - return false; - } - } - - return true; + return mapper.getUserPropertyProvider(username); } } diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/UserPropertyMultiProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/UserPropertyMultiProvider.java new file mode 100644 index 0000000000..f1dd42bf4e --- /dev/null +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/UserPropertyMultiProvider.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.user.property; + +import org.jivesoftware.openfire.user.UserNotFoundException; +import org.jivesoftware.util.ClassUtils; +import org.jivesoftware.util.JiveGlobals; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.Map; + +/** + * A {@link UserPropertyProvider} that delegates to one or more 'backing' UserProvider. + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public abstract class UserPropertyMultiProvider implements UserPropertyProvider +{ + private final static Logger Log = LoggerFactory.getLogger(UserPropertyMultiProvider.class); + + /** + * Instantiates a UserPropertyProvider based on a property value (that is expected to be a class name). When the + * property is not set, this method returns null. When the property is set, but an exception occurs while + * instantiating the class, this method logs the error and returns null. + * + * UserPropertyProvider classes are required to have a public, no-argument constructor. + * + * @param propertyName A property name (cannot be null). + * @return A user proerty provider (can be null). + */ + public static UserPropertyProvider instantiate(String propertyName) + { + final String className = JiveGlobals.getProperty( propertyName ); + if ( className == null ) + { + Log.debug( "Property '{}' is undefined. Skipping.", propertyName ); + return null; + } + Log.debug( "About to to instantiate an UserPropertyProvider '{}' based on the value of property '{}'.", className, propertyName ); + try + { + final Class c = ClassUtils.forName( className ); + final UserPropertyProvider provider = (UserPropertyProvider) c.newInstance(); + Log.debug( "Instantiated UserPropertyProvider '{}'", className ); + return provider; + } + catch ( Exception e ) + { + Log.error( "Unable to load UserPropertyProvider '{}'. Users in this provider will be disabled.", className, e ); + return null; + } + } + + /** + * Returns all UserPropertyProvider instances that serve as 'backing' providers. + * + * @return A collection of providers (never null). + */ + abstract Collection getUserPropertyProviders(); + + /** + * Returns the 'backing' provider that serves the provided user. Note that the user need not exist. + * + * Finds a suitable UserPropertyProvider for the user. + * + * Note that the provided username need not reflect a pre-existing user (the instance might be used to determine in + * which provider a new user is to be created). + * + * Implementations are expected to be able to find a UserPropertyProvider for any username. If an implementation + * fails to do so, such a failure is assumed to be the result of a problem in implementation or configuration. + * + * @param username A user identifier (cannot be null or empty). + * @return A UserPropertyProvider for the user (never null). + */ + abstract UserPropertyProvider getUserPropertyProvider(final String username); + + /** + * Returns whether all backing providers are read-only. When read-only, properties can not be created, + * deleted, or modified. If at least one provider is not read-only, this method returns false. + * + * @return true when all backing providers are read-only, otherwise false. + */ + @Override + public boolean isReadOnly() + { + // TODO Make calls concurrent for improved throughput. + for ( final UserPropertyProvider provider : getUserPropertyProviders() ) + { + // If at least one provider is not readonly, neither is this proxy. + if ( !provider.isReadOnly() ) + { + return false; + } + } + + return true; + } + + @Override + public Map loadProperties(String username) throws UserNotFoundException + { + return getUserPropertyProvider(username).loadProperties( username ); + } + + @Override + public String loadProperty(String username, String propName) throws UserNotFoundException + { + return getUserPropertyProvider(username).loadProperty( username, propName ); + } + + @Override + public void insertProperty(String username, String propName, String propValue) throws UserNotFoundException + { + // all providers are read-only + if (isReadOnly()) { + throw new UnsupportedOperationException(); + } + getUserPropertyProvider(username).insertProperty(username, propName, propValue); + } + + @Override + public void updateProperty(String username, String propName, String propValue) throws UserNotFoundException + { + if (isReadOnly()) { + throw new UnsupportedOperationException(); + } + getUserPropertyProvider(username).updateProperty(username, propName, propValue); + } + + /** + * Removes a property from all non-read-only providers. + * + * @param username The identifier of the user (cannot be null or empty). + * @param propName The property name (cannot be null or empty). + */ + @Override + public void deleteProperty( String username, String propName ) throws UnsupportedOperationException + { + // all providers are read-only + if (isReadOnly()) { + throw new UnsupportedOperationException(); + } + + for (final UserPropertyProvider provider : getUserPropertyProviders()) + { + if ( provider.isReadOnly() ) { + continue; + } + + try { + provider.deleteProperty(username, propName); + } + catch (UserNotFoundException e) { + // User not in this provider. Try other providers; + } + } + } +} From 3c3c1669483653b52a5c653c0c5f2659b321f561 Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Thu, 12 Dec 2024 22:05:56 +0100 Subject: [PATCH 03/14] OF-2924: Reduce duplicated code in Auth multi-providers This moves mostly duplicated code from HybridAuthProvider and MappedAuthProvider in a new, common superclass, AuthMultiProvider Slight functional changes have been introduces, which intent to make behavior more consistent. --- .../openfire/auth/AuthMultiProvider.java | 161 +++++++++++ .../openfire/auth/HybridAuthProvider.java | 268 ++++++++++++------ .../openfire/auth/MappedAuthProvider.java | 113 +------- 3 files changed, 353 insertions(+), 189 deletions(-) create mode 100644 xmppserver/src/main/java/org/jivesoftware/openfire/auth/AuthMultiProvider.java diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/auth/AuthMultiProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/auth/AuthMultiProvider.java new file mode 100644 index 0000000000..56c1da839c --- /dev/null +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/auth/AuthMultiProvider.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.auth; + +import org.jivesoftware.openfire.user.UserNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; + +/** + * An {@link AuthProvider} that delegates to one or more 'backing' AuthProviders. + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public abstract class AuthMultiProvider implements AuthProvider +{ + private final static Logger Log = LoggerFactory.getLogger(AuthMultiProvider.class); + + /** + * Returns all AuthProvider instances that serve as 'backing' providers. + * + * @return A collection of providers (never null). + */ + abstract Collection getAuthProviders(); + + /** + * Returns the 'backing' provider that serves the provided user. + * + * Finds a suitable AuthProvider for the user. + * + * Unlike other MultiProvider interfaces, this interface does not require this method to return a non-null value. + * + * @param username A user identifier (cannot be null or empty). + * @return A AuthProvider for the user (possibly null). + */ + abstract AuthProvider getAuthProvider(String username); + + @Override + public boolean supportsPasswordRetrieval() + { + // TODO Make calls concurrent for improved throughput. + for (final AuthProvider provider : getAuthProviders()) + { + // If at least one provider supports password retrieval, so does this proxy. + if (provider.supportsPasswordRetrieval()) { + return true; + } + } + + return false; + } + + @Override + public boolean isScramSupported() + { + // TODO Make calls concurrent for improved throughput. + for (final AuthProvider provider : getAuthProviders()) + { + // If at least one provider supports SCRAM, so does this proxy. + if ( provider.isScramSupported() ) + { + return true; + } + } + + return false; + } + + @Override + public void authenticate(String username, String password) throws UnauthorizedException, ConnectionException, InternalUnauthenticatedException + { + final AuthProvider provider = getAuthProvider(username); + if (provider == null) + { + throw new UnauthorizedException(); + } + provider.authenticate(username, password); + } + + @Override + public String getPassword(String username) throws UserNotFoundException, UnsupportedOperationException + { + if (!supportsPasswordRetrieval()) { + throw new UnsupportedOperationException(); + } + + final AuthProvider provider = getAuthProvider(username); + if (provider == null) + { + throw new UserNotFoundException(); + } + return provider.getPassword( username ); + } + + @Override + public void setPassword(String username, String password) throws UserNotFoundException, UnsupportedOperationException + { + final AuthProvider provider = getAuthProvider(username); + if (provider == null) + { + throw new UserNotFoundException(); + } + provider.setPassword( username, password ); + } + + @Override + public String getSalt(String username) throws UserNotFoundException + { + final AuthProvider provider = getAuthProvider(username); + if (provider == null) + { + throw new UserNotFoundException(); + } + return provider.getSalt( username ); + } + + @Override + public int getIterations(String username) throws UserNotFoundException + { + final AuthProvider provider = getAuthProvider(username); + if (provider == null) + { + throw new UserNotFoundException(); + } + return provider.getIterations(username); + } + + @Override + public String getServerKey(String username) throws UserNotFoundException + { + final AuthProvider provider = getAuthProvider(username); + if (provider == null) { + throw new UserNotFoundException(); + } + return provider.getServerKey(username); + } + + @Override + public String getStoredKey(String username) throws UserNotFoundException + { + final AuthProvider provider = getAuthProvider(username); + if (provider == null) { + throw new UserNotFoundException(); + } + return provider.getStoredKey(username); + } +} diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/auth/HybridAuthProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/auth/HybridAuthProvider.java index eb78b6068b..7869ac7194 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/auth/HybridAuthProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/auth/HybridAuthProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2008 Jive Software, 2016-2022 Ignite Realtime Foundation. All rights reserved. + * Copyright (C) 2005-2008 Jive Software, 2016-2024 Ignite Realtime Foundation. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,15 +16,14 @@ package org.jivesoftware.openfire.auth; -import java.util.HashSet; -import java.util.Set; - import org.jivesoftware.openfire.user.UserNotFoundException; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.SystemProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.*; + /** * The hybrid auth provider allows up to three AuthProvider implementations to * be strung together to do chained authentication checking. The algorithm is @@ -76,7 +75,7 @@ * * @author Matt Tucker */ -public class HybridAuthProvider implements AuthProvider { +public class HybridAuthProvider extends AuthMultiProvider { private static final Logger Log = LoggerFactory.getLogger(HybridAuthProvider.class); private static final SystemProperty PRIMARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) @@ -100,9 +99,9 @@ public class HybridAuthProvider implements AuthProvider { private AuthProvider secondaryProvider; private AuthProvider tertiaryProvider; - private Set primaryOverrides = new HashSet<>(); - private Set secondaryOverrides = new HashSet<>(); - private Set tertiaryOverrides = new HashSet<>(); + private final Set primaryOverrides = new HashSet<>(); + private final Set secondaryOverrides = new HashSet<>(); + private final Set tertiaryOverrides = new HashSet<>(); public HybridAuthProvider() { // Convert XML based provider setup to Database based @@ -176,49 +175,99 @@ public HybridAuthProvider() { } @Override - public void authenticate(String username, String password) throws UnauthorizedException, ConnectionException, InternalUnauthenticatedException { - // Check overrides first. + Collection getAuthProviders() { + final List result = new ArrayList<>(); + if (primaryProvider != null) { + result.add(primaryProvider); + } + if (secondaryProvider != null) { + result.add(secondaryProvider); + } + if (tertiaryProvider != null) { + result.add(tertiaryProvider); + } + + return result; + } + + /** + * Returns an auth provider for the provided username, but only if the username is in the 'override' list for that + * provider. + * + * When this method returns null, all providers should be attempted. When this method returns a non-null value, then + * only the returned provider is to be used for the provided user. + * + * @param username A user identifier (cannot be null or empty). + * @return an auth provider that MUST be used for this user. + */ + @Override + AuthProvider getAuthProvider(String username) + { if (primaryOverrides.contains(username.toLowerCase())) { - primaryProvider.authenticate(username, password); - return; + return primaryProvider; } - else if (secondaryOverrides != null && secondaryOverrides.contains(username.toLowerCase())) { - secondaryProvider.authenticate(username, password); - return; + if (secondaryOverrides.contains(username.toLowerCase())) { + return secondaryProvider; } - else if (tertiaryOverrides != null && tertiaryOverrides.contains(username.toLowerCase())) { - tertiaryProvider.authenticate(username, password); - return; + if (tertiaryOverrides.contains(username.toLowerCase())) { + return tertiaryProvider; } + return null; + } + + boolean hasOverride(String username) { + return getAuthProvider(username) != null; + } - // Now perform normal + @Override + public void authenticate(String username, String password) throws UnauthorizedException, ConnectionException, InternalUnauthenticatedException { + // Check overrides first. try { - primaryProvider.authenticate(username, password); - } - catch (UnauthorizedException ue) { - if (secondaryProvider != null) { - try { - secondaryProvider.authenticate(username, password); - } - catch (UnauthorizedException ue2) { - if (tertiaryProvider != null) { - tertiaryProvider.authenticate(username, password); - } - else { - throw ue2; - } - } + super.authenticate(username, password); + return; + } catch (UnauthorizedException e) { + if (hasOverride(username)) { + // An override was used. Must not try other providers. + throw e; } - else { - throw ue; + } + + // When there's no override, try all providers in order. + for (final AuthProvider provider: getAuthProviders()) { + try { + provider.authenticate(username, password); + return; + } catch (UnauthorizedException e) { + Log.trace("Could not authenticate user {} with auth provider {}. Will try remaining providers (if any)", username, provider.getClass().getName(), e); } } + throw new UnauthorizedException(); } @Override public String getPassword(String username) throws UserNotFoundException, UnsupportedOperationException { + // Check overrides first. + try { + return super.getPassword(username); + } catch (UserNotFoundException e) { + if (hasOverride(username)) { + // An override was used. Must not try other providers. + throw e; + } + } + + // When there's no override, try all providers in order. + for (final AuthProvider provider: getAuthProviders()) { + try { + if (provider.supportsPasswordRetrieval()) { + return provider.getPassword(username); + } + } catch (UserNotFoundException | UnsupportedOperationException e) { + Log.trace("Could find user {} with auth provider {}. Will try remaining providers (if any)", username, provider.getClass().getName(), e); + } + } throw new UnsupportedOperationException(); } @@ -226,72 +275,121 @@ public String getPassword(String username) public void setPassword(String username, String password) throws UserNotFoundException, UnsupportedOperationException { - // Check overrides first. - if (primaryOverrides.contains(username.toLowerCase())) { - primaryProvider.setPassword(username, password); - return; - } - else if (secondaryOverrides.contains(username.toLowerCase())) { - secondaryProvider.setPassword(username, password); - return; - } - else if (tertiaryOverrides.contains(username.toLowerCase())) { - tertiaryProvider.setPassword(username, password); - return; - } - - // Now perform normal - try { - primaryProvider.setPassword(username, password); - } - catch (UserNotFoundException | UnsupportedOperationException ue) { - if (secondaryProvider != null) { - try { - secondaryProvider.setPassword(username, password); - } - catch (UserNotFoundException | UnsupportedOperationException ue2) { - if (tertiaryProvider != null) { - tertiaryProvider.setPassword(username, password); - } - else { - throw ue2; - } - } - } - else { - throw ue; - } - } - } + // Check overrides first. + try { + super.setPassword(username, password); + return; + } catch (UserNotFoundException e) { + if (hasOverride(username)) { + // An override was used. Must not try other providers. + throw e; + } + } - @Override - public boolean supportsPasswordRetrieval() { - return false; + // When there's no override, try all providers in order. + for (final AuthProvider provider: getAuthProviders()) { + try { + provider.setPassword(username, password); + } catch (UserNotFoundException | UnsupportedOperationException e) { + Log.trace("Could set password for user {} with auth provider {}. Will try remaining providers (if any)", username, provider.getClass().getName(), e); + } + } + throw new UnsupportedOperationException(); } @Override - public boolean isScramSupported() { - // TODO Auto-generated method stub - return false; - } + public String getSalt(String username) throws UnsupportedOperationException, UserNotFoundException + { + // Check overrides first. + try { + return super.getSalt(username); + } catch (UserNotFoundException e) { + if (hasOverride(username)) { + // An override was used. Must not try other providers. + throw e; + } + } - @Override - public String getSalt(String username) throws UnsupportedOperationException, UserNotFoundException { + // When there's no override, try all providers in order. + for (final AuthProvider provider: getAuthProviders()) { + try { + provider.getSalt(username); + } catch (UserNotFoundException | UnsupportedOperationException e) { + Log.trace("Could get salt for user {} with auth provider {}. Will try remaining providers (if any)", username, provider.getClass().getName(), e); + } + } throw new UnsupportedOperationException(); } @Override - public int getIterations(String username) throws UnsupportedOperationException, UserNotFoundException { + public int getIterations(String username) throws UnsupportedOperationException, UserNotFoundException + { + // Check overrides first. + try { + return super.getIterations(username); + } catch (UserNotFoundException e) { + if (hasOverride(username)) { + // An override was used. Must not try other providers. + throw e; + } + } + + // When there's no override, try all providers in order. + for (final AuthProvider provider: getAuthProviders()) { + try { + provider.getIterations(username); + } catch (UserNotFoundException | UnsupportedOperationException e) { + Log.trace("Could get iterations for user {} with auth provider {}. Will try remaining providers (if any)", username, provider.getClass().getName(), e); + } + } throw new UnsupportedOperationException(); } @Override - public String getServerKey(String username) throws UnsupportedOperationException, UserNotFoundException { + public String getServerKey(String username) throws UnsupportedOperationException, UserNotFoundException + { + // Check overrides first. + try { + return super.getServerKey(username); + } catch (UserNotFoundException e) { + if (hasOverride(username)) { + // An override was used. Must not try other providers. + throw e; + } + } + + // When there's no override, try all providers in order. + for (final AuthProvider provider: getAuthProviders()) { + try { + provider.getServerKey(username); + } catch (UserNotFoundException | UnsupportedOperationException e) { + Log.trace("Could get serverkey for user {} with auth provider {}. Will try remaining providers (if any)", username, provider.getClass().getName(), e); + } + } throw new UnsupportedOperationException(); } @Override - public String getStoredKey(String username) throws UnsupportedOperationException, UserNotFoundException { + public String getStoredKey(String username) throws UnsupportedOperationException, UserNotFoundException + { + // Check overrides first. + try { + return super.getStoredKey(username); + } catch (UserNotFoundException e) { + if (hasOverride(username)) { + // An override was used. Must not try other providers. + throw e; + } + } + + // When there's no override, try all providers in order. + for (final AuthProvider provider: getAuthProviders()) { + try { + provider.getStoredKey(username); + } catch (UserNotFoundException | UnsupportedOperationException e) { + Log.trace("Could get storedkey for user {} with auth provider {}. Will try remaining providers (if any)", username, provider.getClass().getName(), e); + } + } throw new UnsupportedOperationException(); } diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/auth/MappedAuthProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/auth/MappedAuthProvider.java index d84f5c2024..fea40dc8fe 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/auth/MappedAuthProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/auth/MappedAuthProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 IgniteRealtime.org, 2016-2019 Ignite Realtime Foundation. All rights reserved + * Copyright (C) 2016 IgniteRealtime.org, 2016-2024 Ignite Realtime Foundation. All rights reserved * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,9 @@ import org.jivesoftware.util.ClassUtils; import org.jivesoftware.util.JiveGlobals; +import java.util.Collection; +import java.util.List; + /** * A {@link AuthProvider} that delegates to a user-specific AuthProvider. * @@ -39,7 +42,7 @@ * * @author Guus der Kinderen, guus@goodbytes.nl */ -public class MappedAuthProvider implements AuthProvider +public class MappedAuthProvider extends AuthMultiProvider { /** * Name of the property of which the value is expected to be the classname of the AuthProviderMapper instance to be @@ -76,111 +79,13 @@ public MappedAuthProvider() } @Override - public void authenticate( String username, String password ) throws UnauthorizedException, ConnectionException, InternalUnauthenticatedException - { - final AuthProvider provider = mapper.getAuthProvider( username ); - if ( provider == null ) - { - throw new UnauthorizedException(); - } - provider.authenticate( username, password ); - } - - @Override - public String getPassword( String username ) throws UserNotFoundException, UnsupportedOperationException - { - final AuthProvider provider = mapper.getAuthProvider( username ); - if ( provider == null ) - { - throw new UserNotFoundException(); - } - return provider.getPassword( username ); - } - - @Override - public void setPassword( String username, String password ) throws UserNotFoundException, UnsupportedOperationException - { - final AuthProvider provider = mapper.getAuthProvider( username ); - if ( provider == null ) - { - throw new UserNotFoundException(); - } - provider.setPassword( username, password ); - } - - @Override - public boolean supportsPasswordRetrieval() - { - // TODO Make calls concurrent for improved throughput. - for ( final AuthProvider provider : mapper.getAuthProviders() ) - { - // If at least one provider supports password retrieval, so does this proxy. - if ( provider.supportsPasswordRetrieval() ) - { - return true; - } - } - - return false; - } - - @Override - public boolean isScramSupported() - { - // TODO Make calls concurrent for improved throughput. - for ( final AuthProvider provider : mapper.getAuthProviders() ) - { - // If at least one provider supports SCRAM, so does this proxy. - if ( provider.isScramSupported() ) - { - return true; - } - } - - return false; + Collection getAuthProviders() { + return mapper.getAuthProviders(); } @Override - public String getSalt(String username) throws UserNotFoundException + AuthProvider getAuthProvider(String username) { - final AuthProvider provider = mapper.getAuthProvider( username ); - if ( provider == null ) - { - throw new UserNotFoundException(); - } - return provider.getSalt( username ); - } - - @Override - public int getIterations(String username) throws UserNotFoundException - { - final AuthProvider provider = mapper.getAuthProvider( username ); - if ( provider == null ) - { - throw new UserNotFoundException(); - } - return provider.getIterations( username ); - } - - @Override - public String getServerKey(String username) throws UserNotFoundException - { - final AuthProvider provider = mapper.getAuthProvider( username ); - if ( provider == null ) - { - throw new UserNotFoundException(); - } - return provider.getServerKey( username ); - } - - @Override - public String getStoredKey(String username) throws UserNotFoundException - { - final AuthProvider provider = mapper.getAuthProvider( username ); - if ( provider == null ) - { - throw new UserNotFoundException(); - } - return provider.getStoredKey( username ); + return mapper.getAuthProvider(username); } } From 881ac141d2f2383b00bc4773ce3833b57e2e80ec Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Thu, 12 Dec 2024 22:32:48 +0100 Subject: [PATCH 04/14] OF-2925: Have 'multi' providers for Groups As are available for Users, UserProperties and Auth, this commit now introduces a Hybrid and Mapped provider for Groups. With these providers, groups can be obtained from more than one external system. This change is a prerequisite for OF-2923. As both issues were developed at the same time, some concepts related to both issues are applied to this commit. This foreshadows more, similar changes related to OF-2923. --- .../main/resources/openfire_i18n.properties | 6 + .../openfire/group/GroupMultiProvider.java | 523 ++++++++++++++++++ .../openfire/group/GroupProviderMapper.java | 56 ++ .../openfire/group/HybridGroupProvider.java | 171 ++++++ .../openfire/group/MappedGroupProvider.java | 91 +++ 5 files changed, 847 insertions(+) create mode 100644 xmppserver/src/main/java/org/jivesoftware/openfire/group/GroupMultiProvider.java create mode 100644 xmppserver/src/main/java/org/jivesoftware/openfire/group/GroupProviderMapper.java create mode 100644 xmppserver/src/main/java/org/jivesoftware/openfire/group/HybridGroupProvider.java create mode 100644 xmppserver/src/main/java/org/jivesoftware/openfire/group/MappedGroupProvider.java diff --git a/i18n/src/main/resources/openfire_i18n.properties b/i18n/src/main/resources/openfire_i18n.properties index 91f18ef5b0..61249798ec 100644 --- a/i18n/src/main/resources/openfire_i18n.properties +++ b/i18n/src/main/resources/openfire_i18n.properties @@ -1234,6 +1234,12 @@ system_property.provider.auth.className=The class to use to authenticate users system_property.hybridAuthProvider.primaryProvider.className=The first class the HybridAuthProvider should to use to authenticate users system_property.hybridAuthProvider.secondaryProvider.className=The second class the HybridAuthProvider should to use to authenticate users system_property.hybridAuthProvider.tertiaryProvider.className=The third class the HybridAuthProvider should to use to authenticate users +system_property.hybridGroupProvider.primaryProvider.className=The first class the HybridGroupProvider should use to get groups from. +system_property.hybridGroupProvider.primaryProvider.config=Configuration value for the first class used by the HybridGroupProvider. +system_property.hybridGroupProvider.secondaryProvider.className=The second class the HybridGroupProvider should use to get groups from. +system_property.hybridGroupProvider.secondaryProvider.config=Configuration value for the second class used by the HybridGroupProvider. +system_property.hybridGroupProvider.tertiaryProvider.className=The third class the HybridGroupProvider should use to get group from. +system_property.hybridGroupProvider.tertiaryProvider.config=Configuration value for the third class used by the HybridGroupProvider. system_property.admin.authorizedJIDs=The bare JID of every admin user for the DefaultAdminProvider system_property.xmpp.auth.ssl.context_protocol=The TLS protocol to use for encryption context initialization, overriding the Java default. system_property.xmpp.parser.buffer.size=Maximum size of an XMPP stanza. Larger stanzas will cause a connection to be closed. diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/group/GroupMultiProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/group/GroupMultiProvider.java new file mode 100644 index 0000000000..32c1da6df0 --- /dev/null +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/group/GroupMultiProvider.java @@ -0,0 +1,523 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.group; + +import org.jivesoftware.util.PersistableMap; +import org.jivesoftware.util.SystemProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xmpp.packet.JID; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * A {@link GroupProvider} that delegates to one or more 'backing' GroupProvider. + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public abstract class GroupMultiProvider implements GroupProvider +{ + private final static Logger Log = LoggerFactory.getLogger( GroupMultiProvider.class ); + + /** + * Instantiates a GroupProvider based on Class-based system property. When the property is not set, this + * method returns null. When the property is set, but an exception occurs while instantiating the class, this method + * logs the error and returns null. + * + * GroupProvider classes are required to have a public, no-argument constructor. + * + * @param implementationProperty A property that defines the class of the instance to be returned. + * @return A group provider (can be null). + */ + public static GroupProvider instantiate(@Nonnull final SystemProperty implementationProperty) + { + return instantiate(implementationProperty, null); + } + + /** + * Instantiates a GroupProvider based on Class-based system property. When the property is not set, this + * method returns null. When the property is set, but an exception occurs while instantiating the class, this method + * logs the error and returns null. + * + * GroupProvider classes are required to have a public, no-argument constructor, but can have an optional additional + * constructor that takes a single String argument. If such constructor is defined, then it is invoked with the + * value of the second argument of this method. This is typically used to (but needs not) identify a property + * (by name) that holds additional configuration for the to be instantiated GroupProvider. This + * implementation will pass on any non-empty value to the constructor. When a configuration argument is provided, + * but no constructor exists in the implementation that accepts a single String value, this method will log a + * warning and attempt to return an instance based on the no-arg constructor of the class. + * + * @param implementationProperty A property that defines the class of the instance to be returned. + * @param configProperty an opaque string value passed to the constructor. + * @return A group provider (can be null). + */ + public static GroupProvider instantiate(@Nonnull final SystemProperty implementationProperty, @Nullable final SystemProperty configProperty) + { + final Class implementationClass = implementationProperty.getValue(); + if (implementationClass == null) { + Log.debug( "Property '{}' is undefined or has no value. Skipping.", implementationProperty.getKey() ); + return null; + } + Log.debug("About to to instantiate an GroupProvider '{}' based on the value of property '{}'.", implementationClass, implementationProperty.getKey()); + + try { + if (configProperty != null && configProperty.getValue() != null && !configProperty.getValue().isEmpty()) { + try { + final Constructor constructor = implementationClass.getConstructor(String.class); + final GroupProvider result = constructor.newInstance(configProperty.getValue()); + Log.debug("Instantiated GroupProvider '{}' with configuration: '{}'", implementationClass.getName(), configProperty.getValue()); + return result; + } catch (NoSuchMethodException e) { + Log.warn("Custom configuration is defined for the a provider but the configured class ('{}') does not provide a constructor that takes a String argument. Custom configuration will be ignored. Ignored configuration: '{}'", implementationProperty.getValue().getName(), configProperty); + } + } + + final GroupProvider result = implementationClass.getDeclaredConstructor().newInstance(); + Log.debug("Instantiated GroupProvider '{}'", implementationClass.getName()); + return result; + } catch (Exception e) { + Log.error("Unable to load GroupProvider '{}'. Data from this provider will not be available.", implementationClass.getName(), e); + return null; + } + } + + /** + * Returns all GroupProvider instances that serve as 'backing' providers. + * + * @return A collection of providers (never null). + */ + abstract Collection getGroupProviders(); + + /** + * Returns the 'backing' provider that serves the provided group. Note that the group need not exist. + * + * Finds a suitable GroupProvider for the group. + * + * Note that the provided group name need not reflect a pre-existing group (the instance might be used to determine in + * which provider a new group is to be created). + * + * Implementations are expected to be able to find a GroupProvider for any group name. If an implementation fails to do + * so, such a failure is assumed to be the result of a problem in implementation or configuration. + * + * @param groupName A group identifier (cannot be null or empty). + * @return A GroupProvider for the group (never null). + */ + abstract GroupProvider getGroupProvider(String groupName); + + /** + * Returns the number of groups in the system, calculated as the sum of groups in each provider. + * + * @return the number of groups in the system. + */ + @Override + public int getGroupCount() + { + return getGroupProviders().parallelStream() + .map(GroupProvider::getGroupCount) + .reduce(0, Integer::sum); + } + + /** + * Returns the Collection of all group names in each of the providers. + * + * @return the Collection of all groups. + */ + @Override + public Collection getGroupNames() + { + return getGroupProviders().parallelStream() + .map(GroupProvider::getGroupNames) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + + /** + * Returns true if at least one provider allows group sharing. Shared groups + * enable roster sharing. + * + * @return true if group sharing is supported. + */ + @Override + public boolean isSharingSupported() + { + return getGroupProviders().parallelStream() + .anyMatch(GroupProvider::isSharingSupported); + } + + /** + * Returns an unmodifiable Collection of all shared groups from each provider that supports group sharing. + * + * @return unmodifiable Collection of all shared groups in the system. + */ + @Override + public Collection getSharedGroupNames() + { + return getGroupProviders().parallelStream() + .filter(GroupProvider::isSharingSupported) + .map(GroupProvider::getSharedGroupNames) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + + /** + * Returns an unmodifiable Collection of all shared groups in the system for a given user. + * + * Implementations should use the bare JID representation of the JID passed as an argument to this method. + * + * @param user The bare JID for the user (node@domain) + * @return unmodifiable Collection of all shared groups in the system for a given user. + */ + @Override + public Collection getSharedGroupNames(JID user) + { + return getGroupProviders().parallelStream() + .filter(GroupProvider::isSharingSupported) + .map(groupProvider -> groupProvider.getSharedGroupNames(user)) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + + /** + * Returns an unmodifiable Collection of all public shared groups by each of the providers that support sharing. + * + * @return unmodifiable Collection of all public shared groups in the system. + */ + @Override + public Collection getPublicSharedGroupNames() + { + return getGroupProviders().parallelStream() + .filter(GroupProvider::isSharingSupported) + .map(GroupProvider::getPublicSharedGroupNames) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + + /** + * Returns an unmodifiable Collection of groups that are visible to the members of the given group. + * + * @param userGroup The given group + * @return unmodifiable Collection of group names that are visible to the given group. + */ + @Override + public Collection getVisibleGroupNames(String userGroup) + { + return getGroupProviders().parallelStream() + .map(groupProvider -> groupProvider.getVisibleGroupNames(userGroup)) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + + /** + * Returns the Collection of all groups provided by each of the providers, ordered by provider-order. + * + * @param startIndex start index in results. + * @param numResults number of results to return. + * @return the Collection of all group names given the {@code startIndex} and {@code numResults}. + */ + @Override + public Collection getGroupNames(int startIndex, int numResults) + { + final List result = new ArrayList<>(); + int totalCount = 0; + + for ( final GroupProvider provider : getGroupProviders() ) + { + final int providerStartIndex = Math.max( ( startIndex - totalCount ), 0 ); + totalCount += provider.getGroupCount(); + if ( startIndex >= totalCount ) + { + continue; + } + final int providerResultMax = numResults - result.size(); + result.addAll( provider.getGroupNames( providerStartIndex, providerResultMax ) ); + if ( result.size() >= numResults ) + { + break; + } + } + return result; + } + + /** + * Returns the Collection of group names that an entity belongs to. + * + * Implementations should use the bare JID representation of the JID passed as an argument to this method. + * + * @param user the (bare) JID of the entity. + * @return the Collection of group names that the user belongs to. + */ + @Override + public Collection getGroupNames(JID user) + { + return getGroupProviders().parallelStream() + .map(groupProvider -> groupProvider.getGroupNames(user)) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + + /** + * Returns whether all backing providers are read-only. When read-only, groups can not be created, deleted, + * or modified. If at least one provider is not read-only, this method returns false. + * + * @return true when all backing providers are read-only, otherwise false. + */ + @Override + public boolean isReadOnly() + { + // If at least one provider is not readonly, neither is this proxy. + return getGroupProviders().parallelStream() + .allMatch(GroupProvider::isReadOnly); + } + + @Override + public Collection search(String query) + { + return getGroupProviders().parallelStream() + .filter(groupProvider -> isSearchSupported()) + .map(groupProvider -> groupProvider.search(query)) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + + /** + * Returns the group names that match a search. The search is over group names and implicitly uses wildcard matching + * (although the exact search semantics are left up to each provider implementation). For example, a search for "HR" + * should match the groups "HR", "HR Department", and "The HR People".

+ * + * Before searching or showing a search UI, use the {@link #isSearchSupported} method to ensure that searching is + * supported. + * + * This method throws an UnsupportedOperationException when none of the backing providers support search. + * + * @param query the search string for group names. + * @return all groups that match the search. + * @throws UnsupportedOperationException When none of the providers support search. + */ + @Override + public Collection search(String query, int startIndex, int numResults) + { + if (!isSearchSupported()) { + throw new UnsupportedOperationException("None of the backing providers support this operation."); + } + + // TODO improve the performance and efficiency of this! Do not collect _all_ results before returning the page! + final List allResults = new ArrayList<>(); + for ( final GroupProvider provider : getGroupProviders() ) + { + if ( provider.isSearchSupported()) { + allResults.addAll(provider.search(query)); + } + } + return allResults.subList(startIndex, Math.min(allResults.size(), startIndex + numResults)); + } + + /** + * Returns the names of groups that have a property matching the given key/value pair. This provides a simple + * extensible search mechanism for providers with differing property sets and storage models. + * + * The semantics of the key/value matching (wildcard support, scoping, etc.) are unspecified by the interface and + * may vary for each implementation. + * + * When a key/value combination ia provided that are not supported by a particular provider, this is ignored by that + * provider (but can still be used by other providers). + * + * Before searching or showing a search UI, use the {@link #isSearchSupported} method to ensure that searching is + * supported. + * + * @param key The name of a group property (e.g. "sharedRoster.showInRoster") + * @param value The value to match for the given property + * @return unmodifiable Collection of group names that match the given key/value pair. + * @throws UnsupportedOperationException When none of the providers support search. + */ + @Override + public Collection search(String key, String value) + { + if (!isSearchSupported()) { + throw new UnsupportedOperationException("None of the backing providers support this operation."); + } + + return getGroupProviders().parallelStream() + .filter(groupProvider -> isSearchSupported()) + .map(groupProvider -> groupProvider.search(key, value)) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + + /** + * Returns true if group searching is supported by at least one of the providers. + * + * @return true if searching is supported. + */ + @Override + public boolean isSearchSupported() + { + // If at least one provider is supports search, so does this proxy. + return getGroupProviders().parallelStream() + .anyMatch(GroupProvider::isSearchSupported); + } + + /** + * Loads a group from the first provider that contains the group. + * + * @param name the name of the group (cannot be null or empty). + * @return The group (never null). + * @throws GroupNotFoundException When none of the providers contains the group. + */ + @Override + public Group getGroup(String name) throws GroupNotFoundException + { + final GroupProvider groupProvider; + try { + groupProvider = getGroupProvider(name); + } catch (RuntimeException e){ + throw new GroupNotFoundException("Unable to identify group provider for group name " + name, e); + } + return groupProvider.getGroup(name); + } + + /** + * Creates a group with the given name in the first provider that is not read-only. + * + * @param name name of the group. + * @return the newly created group. + * @throws GroupAlreadyExistsException if a group with the same name already exists. + * @throws UnsupportedOperationException if the provider does not support the operation. + */ + @Override + public Group createGroup(String name) throws GroupAlreadyExistsException, GroupNameInvalidException + { + return getGroupProvider(name).createGroup(name); + } + + /** + * Removes a group from all non-read-only providers. + * + * @param name the name of the group to delete. + */ + @Override + public void deleteGroup(String name) throws GroupNotFoundException + { + // all providers are read-only + if (isReadOnly()) { + throw new UnsupportedOperationException(); + } + + for (final GroupProvider provider : getGroupProviders()) { + if (provider.isReadOnly()) { + continue; + } + provider.deleteGroup(name); + } + } + + /** + * Sets the name of a group to a new name in the provider that is used for this group. + * + * @param oldName the current name of the group. + * @param newName the desired new name of the group. + * @throws GroupAlreadyExistsException if a group with the same name already exists. + * @throws UnsupportedOperationException if the provider does not support the operation. + */ + @Override + public void setName(String oldName, String newName) throws GroupAlreadyExistsException, GroupNameInvalidException, GroupNotFoundException + { + getGroupProvider(oldName).setName(oldName, newName); + } + + /** + * Updates the group's description in the provider that is used for this group. + * + * @param name the group name. + * @param description the group description. + * @throws GroupNotFoundException if no existing group could be found to update. + */ + @Override + public void setDescription(String name, String description) throws GroupNotFoundException + { + getGroupProvider(name).setDescription(name, description); + } + + /** + * Adds an entity to a group (optional operation). + * + * Implementations should use the bare JID representation of the JID passed as an argument to this method. + * + * @param groupName the group to add the member to + * @param user the (bare) JID of the entity to add + * @param administrator True if the member is an administrator of the group + * @throws UnsupportedOperationException if the provider does not support the operation. + */ + @Override + public void addMember(String groupName, JID user, boolean administrator) throws GroupNotFoundException + { + getGroupProvider(groupName).addMember(groupName, user, administrator); + } + + /** + * Updates the privileges of an entity in a group. + * + * Implementations should use the bare JID representation of the JID passed as an argument to this method. + * + * @param groupName the group where the change happened + * @param user the (bare) JID of the entity with new privileges + * @param administrator True if the member is an administrator of the group + * @throws UnsupportedOperationException if the provider does not support the operation. + */ + @Override + public void updateMember(String groupName, JID user, boolean administrator) throws GroupNotFoundException + { + getGroupProvider(groupName).updateMember(groupName, user, administrator); + } + + /** + * Deletes an entity from a group (optional operation). + * + * Implementations should use the bare JID representation of the JID passed as an argument to this method. + * + * @param groupName the group name. + * @param user the (bare) JID of the entity to delete. + * @throws UnsupportedOperationException if the provider does not support the operation. + */ + @Override + public void deleteMember(String groupName, JID user) + { + getGroupProvider(groupName).deleteMember(groupName, user); + } + + /** + * Loads the group properties (if any) from the backend data store. If the properties can be changed, the provider + * implementation must ensure that updates to the resulting {@link Map} are persisted to the backend data store. + * Otherwise, if a mutator method is called, the implementation should throw an {@link UnsupportedOperationException}. + * + * If there are no corresponding properties for the given group, or if the provider does not support group + * properties, this method should return an empty Map rather than null. + * + * @param group The target group + * @return The properties for the given group + */ + @Override + public PersistableMap loadProperties(Group group) + { + return getGroupProvider(group.getName()).loadProperties(group); + } +} diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/group/GroupProviderMapper.java b/xmppserver/src/main/java/org/jivesoftware/openfire/group/GroupProviderMapper.java new file mode 100644 index 0000000000..e00a3d1e57 --- /dev/null +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/group/GroupProviderMapper.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.openfire.group; + +import java.util.Set; + +/** + * Implementations are used to determine what GroupProvider is to be used for a particular group name. + * + * Note that the provided group name need not reflect a pre-existing group (the instance might be used to determine in + * which provider a new group is to be created). + * + * Implementation must have a no-argument constructor. + * + * @author Guus der Kinderen, guus@goodbytes.nl + * @see MappedGroupProvider + */ +public interface GroupProviderMapper +{ + /** + * Finds a suitable GroupProvider for the group. + * + * Note that the provided group name need not reflect a pre-existing group (the instance might be used to determine in + * which provider a new group is to be created). + * + * Implementations are expected to be able to find a GroupProvider for any group name. If an implementation fails to do + * so, such a failure is assumed to be the result of a problem in implementation or configuration. + * + * @param groupname A group identifier (cannot be null or empty). + * @return A GroupProvider for the group (never null). + */ + GroupProvider getGroupProvider(String groupname); + + /** + * Returns all providers that are used by this instance. + * + * The returned collection should have a consistent, predictable iteration order. + * + * @return all providers (never null). + */ + Set getGroupProviders(); +} diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/group/HybridGroupProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/group/HybridGroupProvider.java new file mode 100644 index 0000000000..5b101937be --- /dev/null +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/group/HybridGroupProvider.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.group; + +import org.jivesoftware.util.JiveGlobals; +import org.jivesoftware.util.PersistableMap; +import org.jivesoftware.util.SystemProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.xmpp.packet.JID; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Delegate GroupProvider operations among up to three configurable provider implementation classes. + * + * This class related to, but is distinct from {@link MappedGroupProvider}. The Hybrid variant of the provider iterates + * over providers, operating on the first applicable instance. The Mapped variant, however, maps each group to exactly + * one provider. + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public class HybridGroupProvider extends GroupMultiProvider +{ + private static final Logger Log = LoggerFactory.getLogger(HybridGroupProvider.class); + + private static final SystemProperty PRIMARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + .setKey("hybridGroupProvider.primaryProvider.className") + .setBaseClass(GroupProvider.class) + .setDynamic(false) + .build(); + + public static final SystemProperty PRIMARY_PROVIDER_CONFIG = SystemProperty.Builder.ofType(String.class) + .setKey("hybridGroupProvider.primaryProvider.config") + .setDynamic(false) + .build(); + + private static final SystemProperty SECONDARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + .setKey("hybridGroupProvider.secondaryProvider.className") + .setBaseClass(GroupProvider.class) + .setDynamic(false) + .build(); + + public static final SystemProperty SECONDARY_PROVIDER_CONFIG = SystemProperty.Builder.ofType(String.class) + .setKey("hybridGroupProvider.secondaryProvider.config") + .setDynamic(false) + .build(); + + private static final SystemProperty TERTIARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + .setKey("hybridGroupProvider.tertiaryProvider.className") + .setBaseClass(GroupProvider.class) + .setDynamic(false) + .build(); + + public static final SystemProperty TERTIARY_PROVIDER_CONFIG = SystemProperty.Builder.ofType(String.class) + .setKey("hybridGroupProvider.tertiaryProvider.config") + .setDynamic(false) + .build(); + + private final List groupProviders = new ArrayList<>(); + + public HybridGroupProvider() + { + // Migrate group provider properties + JiveGlobals.migrateProperty(PRIMARY_PROVIDER.getKey()); + JiveGlobals.migrateProperty(SECONDARY_PROVIDER.getKey()); + JiveGlobals.migrateProperty(TERTIARY_PROVIDER.getKey()); + + // Load primary, secondary, and tertiary group providers. + final GroupProvider primary = instantiate(PRIMARY_PROVIDER, PRIMARY_PROVIDER_CONFIG); + if (primary != null) { + groupProviders.add(primary); + } + final GroupProvider secondary = instantiate(SECONDARY_PROVIDER, SECONDARY_PROVIDER_CONFIG); + if (secondary != null) { + groupProviders.add(secondary); + } + final GroupProvider tertiary = instantiate(TERTIARY_PROVIDER, TERTIARY_PROVIDER_CONFIG); + if (tertiary != null) { + groupProviders.add(tertiary); + } + + // Verify that there's at least one provider available. + if ( groupProviders.isEmpty() ) + { + Log.error( "At least one GroupProvider must be specified via openfire.xml or the system properties!" ); + } + } + + /** + * Returns all GroupProvider instances that serve as 'backing' providers. + * + * @return A collection of providers (never null). + */ + @Override + Collection getGroupProviders() + { + return groupProviders; + } + + /** + * Returns the first provider that contains the group, or the first provider that is not read-only when the group + * does not exist in any provider. + * + * @param groupName the name of the group (cannot be null or empty). + * @return The group provider (never null) + * @throws UnsupportedOperationException When the group is not found and no provider can be used to create the group. + */ + @Override + GroupProvider getGroupProvider(String groupName) + { + GroupProvider nonReadOnly = null; + for (final GroupProvider provider : getGroupProviders()) { + try { + provider.getGroup(groupName); + return provider; + } catch (GroupNotFoundException ex) { + Log.debug( "Group {} not found in GroupProvider {}", groupName, provider.getClass().getName() ); + + if (nonReadOnly == null && !provider.isReadOnly()) { + nonReadOnly = provider; + } + } + } + + // Group does not exist. Return a provider suitable for creating groups. + if (nonReadOnly == null) { + throw new UnsupportedOperationException(); + } + + return nonReadOnly; + } + + /** + * Loads a group from the first provider that contains the group. + * + * @param name the name of the group (cannot be null or empty). + * @return The group (never null). + * @throws GroupNotFoundException When none of the providers contains the group. + */ + @Override + public Group getGroup(String name) throws GroupNotFoundException + { + // Override the implementation in the superclass to prevent obtaining the griyo twice. + for (final GroupProvider provider : groupProviders) { + try { + return provider.getGroup(name); + } catch (GroupNotFoundException ex) { + Log.debug( "Group {} not found in GroupProvider {}", name, provider.getClass().getName() ); + } + } + // If we get this far, no provider was able to load the group + throw new GroupNotFoundException(); + } +} diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/group/MappedGroupProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/group/MappedGroupProvider.java new file mode 100644 index 0000000000..ff02919127 --- /dev/null +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/group/MappedGroupProvider.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jivesoftware.openfire.group; + +import org.jivesoftware.util.ClassUtils; +import org.jivesoftware.util.JiveGlobals; + +import java.util.Collection; + +/** + * A {@link GroupProvider} that delegates to a group-specific GroupProvider. + * + * This class related to, but is distinct from {@link HybridGroupProvider}. The Hybrid variant of the provider iterates + * over providers, operating on the first applicable instance. This Mapped variant, however, maps each group to exactly + * one provider. + * + * To use this provider, use the following system property definition: + * + *

    + *
  • {@code provider.group.className = org.jivesoftware.openfire.group.MappedGroupProvider}
  • + *
+ * + * To be usable, a {@link GroupProviderMapper} must be configured using the {@code mappedGroupProvider.mapper.className} + * system property. It is of importance to note that most GroupProviderMapper implementations will require additional + * configuration. + * + * @author Guus der Kinderen, guus@goodbytes.nl + * @see org.jivesoftware.openfire.group.GroupProviderMapper + */ +public class MappedGroupProvider extends GroupMultiProvider +{ + /** + * Name of the property of which the value is expected to be the classname of the GroupProviderMapper instance to be + * used by instances of this class. + */ + public static final String PROPERTY_MAPPER_CLASSNAME = "mappedGroupProvider.mapper.className"; + + /** + * Used to determine what provider is to be used to operate on a particular group. + */ + protected final GroupProviderMapper mapper; + + public MappedGroupProvider() + { + // Migrate properties. + JiveGlobals.migrateProperty( PROPERTY_MAPPER_CLASSNAME ); + + // Instantiate mapper. + final String mapperClass = JiveGlobals.getProperty( PROPERTY_MAPPER_CLASSNAME ); + if ( mapperClass == null ) + { + throw new IllegalStateException( "A mapper must be specified via openfire.xml or the system properties." ); + } + + try + { + final Class c = ClassUtils.forName( mapperClass ); + mapper = (GroupProviderMapper) c.newInstance(); + } + catch ( Exception e ) + { + throw new IllegalStateException( "Unable to create new instance of GroupProviderMapper class: " + mapperClass, e ); + } + } + + @Override + public Collection getGroupProviders() + { + return mapper.getGroupProviders(); + } + + @Override + public GroupProvider getGroupProvider( String groupname ) + { + return mapper.getGroupProvider( groupname ); + } +} From d0f8b6232b56a199f1af2483923205c29a9e82ca Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Thu, 12 Dec 2024 22:48:53 +0100 Subject: [PATCH 05/14] OF-2923: Allow for more than one LDAP connection This change replaces the singleton LDAP Manager (where LDAP connectivity information is stored) with a solution in which multiple, named, LDAP Managers can exist. Having more than one LDAP configuration allows Openfire (through Mapped or Hybrid providers) to connect to more than one LDAP service. This can be used to combine data from multiple services. The various Hybrid and Mapped providers that pre-exist have received modifications that allow them to configure providers with an additional string (which is used in the LDAP manager to differentiate between the configuration of different LDAP servers). --- .../main/resources/openfire_i18n.properties | 25 +- .../openfire/admin/DefaultAdminProvider.java | 13 +- .../openfire/auth/AuthMultiProvider.java | 66 ++++ .../openfire/auth/HybridAuthProvider.java | 106 +++---- .../openfire/ldap/LdapAuthProvider.java | 17 +- .../openfire/ldap/LdapGroupProvider.java | 16 +- .../openfire/ldap/LdapManager.java | 296 +++++++++++------- .../openfire/ldap/LdapUserProvider.java | 16 +- .../openfire/user/HybridUserProvider.java | 62 ++-- .../openfire/user/UserMultiProvider.java | 68 ++++ .../property/HybridUserPropertyProvider.java | 46 ++- .../property/UserPropertyMultiProvider.java | 72 ++++- .../java/org/jivesoftware/util/LDAPTest.java | 8 +- 13 files changed, 575 insertions(+), 236 deletions(-) diff --git a/i18n/src/main/resources/openfire_i18n.properties b/i18n/src/main/resources/openfire_i18n.properties index 61249798ec..5c3df6adda 100644 --- a/i18n/src/main/resources/openfire_i18n.properties +++ b/i18n/src/main/resources/openfire_i18n.properties @@ -1231,15 +1231,30 @@ system_property.update.proxy.port=The port on the proxy to use, or -1 if no prox system_property.xmpp.session.conflict-limit=-1 to never kick off existing sessions when another session with the same \ full JID joins, otherwise the number of login attempts before the existing session is kicked system_property.provider.auth.className=The class to use to authenticate users -system_property.hybridAuthProvider.primaryProvider.className=The first class the HybridAuthProvider should to use to authenticate users -system_property.hybridAuthProvider.secondaryProvider.className=The second class the HybridAuthProvider should to use to authenticate users -system_property.hybridAuthProvider.tertiaryProvider.className=The third class the HybridAuthProvider should to use to authenticate users +system_property.hybridAuthProvider.primaryProvider.className=The first class the HybridAuthProvider should use to authenticate users. +system_property.hybridAuthProvider.primaryProvider.config=Configuration value for the first class used by the HybridAuthProvider. +system_property.hybridAuthProvider.secondaryProvider.className=The second class the HybridAuthProvider should use to authenticate users. +system_property.hybridAuthProvider.secondaryProvider.config=Configuration value for the second class used by the HybridAuthProvider. +system_property.hybridAuthProvider.tertiaryProvider.className=The third class the HybridAuthProvider should use to authenticate users. +system_property.hybridAuthProvider.tertiaryProvider.config=Configuration value for the third class used by the HybridAuthProvider. system_property.hybridGroupProvider.primaryProvider.className=The first class the HybridGroupProvider should use to get groups from. system_property.hybridGroupProvider.primaryProvider.config=Configuration value for the first class used by the HybridGroupProvider. system_property.hybridGroupProvider.secondaryProvider.className=The second class the HybridGroupProvider should use to get groups from. system_property.hybridGroupProvider.secondaryProvider.config=Configuration value for the second class used by the HybridGroupProvider. system_property.hybridGroupProvider.tertiaryProvider.className=The third class the HybridGroupProvider should use to get group from. system_property.hybridGroupProvider.tertiaryProvider.config=Configuration value for the third class used by the HybridGroupProvider. +system_property.hybridUserPropertyProvider.primaryProvider.className=The first class the HybridUserPropertyProvider should use to get user properties from. +system_property.hybridUserPropertyProvider.primaryProvider.config=Configuration value for the first class used by the HybridUserPropertyProvider. +system_property.hybridUserPropertyProvider.secondaryProvider.className=The second class the HybridUserPropertyProvider should use to get user properties from. +system_property.hybridUserPropertyProvider.secondaryProvider.config=Configuration value for the second class used by the HybridUserPropertyProvider. +system_property.hybridUserPropertyProvider.tertiaryProvider.className=The third class the HybridUserPropertyProvider should use to get user properties from. +system_property.hybridUserPropertyProvider.tertiaryProvider.config=Configuration value for the third class used by the HybridUserPropertyProvider. +system_property.hybridUserProvider.primaryProvider.className=The first class the HybridUserProvider should use to get users from. +system_property.hybridUserProvider.primaryProvider.config=Configuration value for the first class used by the HybridUserProvider. +system_property.hybridUserProvider.secondaryProvider.className=The second class the HybridUserProvider should use to get users from. +system_property.hybridUserProvider.secondaryProvider.config=Configuration value for the second class used by the HybridUserProvider. +system_property.hybridUserProvider.tertiaryProvider.className=The third class the HybridUserProvider should use to get users from. +system_property.hybridUserProvider.tertiaryProvider.config=Configuration value for the third class used by the HybridUserProvider. system_property.admin.authorizedJIDs=The bare JID of every admin user for the DefaultAdminProvider system_property.xmpp.auth.ssl.context_protocol=The TLS protocol to use for encryption context initialization, overriding the Java default. system_property.xmpp.parser.buffer.size=Maximum size of an XMPP stanza. Larger stanzas will cause a connection to be closed. @@ -1326,6 +1341,7 @@ system_property.adminConsole.perUsernameAttemptResetInterval=Time frame before A system_property.xmpp.muc.muclumbus.v1-0.enabled=Determine is the multi-user chat "muclumbus" (v1.0) search feature is enabled. system_property.xmpp.muc.join.presence=Setting the presence send of participants joining in MUC rooms. system_property.xmpp.muc.join.self-presence-timeout=Maximum duration to wait for presence to be broadcast while joining a MUC room. +system_property.ldap.authorizeField=Name of attribute in user's LDAP object used by the LDAP authorization policy. system_property.ldap.pagedResultsSize=The maximum number of records to retrieve from LDAP in a single page. \ The default value of -1 means rely on the paging of the LDAP server itself. \ Note that if using ActiveDirectory, this should not be left at the default, and should not be set to more than the value of the ActiveDirectory MaxPageSize; 1,000 by default. @@ -2032,6 +2048,9 @@ setup.ldap.server.basedn=Base DN setup.ldap.server.basedn_help=The starting DN that contains all user accounts. The entire subtree \ under the base DN will be searched for user accounts (unless subtree searching is disabled). setup.ldap.server.basedn_error=Enter a valid LDAP base DN. +setup.ldap.server.alternatebasedn=Alternate Base DN +setup.ldap.server.alternatebasedn_help=Provides data that complements that of the Base DN. +setup.ldap.server.alternatebasedn_error=Enter a valid LDAP alternate base DN (or leave it empty). setup.ldap.server.auth=Authentication setup.ldap.server.admindn=Administrator DN setup.ldap.server.admindn_help=The full DN of a directory administrator. All directory operations will be \ diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/admin/DefaultAdminProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/admin/DefaultAdminProvider.java index d542d9a58f..ef69d1987d 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/admin/DefaultAdminProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/admin/DefaultAdminProvider.java @@ -15,11 +15,6 @@ */ package org.jivesoftware.openfire.admin; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.StringTokenizer; - import org.jivesoftware.openfire.XMPPServer; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.SystemProperty; @@ -27,6 +22,11 @@ import org.slf4j.LoggerFactory; import org.xmpp.packet.JID; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.StringTokenizer; + /** * Handles default management of admin users, which stores the list if accounts as a system property. * @@ -39,7 +39,7 @@ public class DefaultAdminProvider implements AdminProvider { .setDefaultValue(Collections.emptyList()) .setSorted(true) .setDynamic(true) - .addListener(jids -> AdminManager.getInstance().refreshAdminAccounts()) + .addListener(jids -> { if (AdminManager.getAdminProvider() != null) AdminManager.getInstance().refreshAdminAccounts(); }) .buildList(JID.class); private static final Logger Log = LoggerFactory.getLogger(DefaultAdminProvider.class); @@ -47,7 +47,6 @@ public class DefaultAdminProvider implements AdminProvider { * Constructs a new DefaultAdminProvider */ public DefaultAdminProvider() { - // Convert old openfire.xml style to new provider style, if necessary. Log.debug("DefaultAdminProvider: Convert XML to provider."); convertXMLToProvider(); diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/auth/AuthMultiProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/auth/AuthMultiProvider.java index 56c1da839c..f64524a79f 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/auth/AuthMultiProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/auth/AuthMultiProvider.java @@ -16,9 +16,13 @@ package org.jivesoftware.openfire.auth; import org.jivesoftware.openfire.user.UserNotFoundException; +import org.jivesoftware.util.SystemProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.lang.reflect.Constructor; import java.util.Collection; /** @@ -30,6 +34,68 @@ public abstract class AuthMultiProvider implements AuthProvider { private final static Logger Log = LoggerFactory.getLogger(AuthMultiProvider.class); + /** + * Instantiates a AuthProvider based on Class-based system property. When the property is not set, this + * method returns null. When the property is set, but an exception occurs while instantiating the class, this method + * logs the error and returns null. + * + * AuthProvider classes are required to have a public, no-argument constructor. + * + * @param implementationProperty A property that defines the class of the instance to be returned. + * @return A user provider (can be null). + */ + public static AuthProvider instantiate(@Nonnull final SystemProperty implementationProperty) + { + return instantiate(implementationProperty, null); + } + + /** + * Instantiates a AuthProvider based on Class-based system property. When the property is not set, this + * method returns null. When the property is set, but an exception occurs while instantiating the class, this method + * logs the error and returns null. + * + * AuthProvider classes are required to have a public, no-argument constructor, but can have an optional + * additional constructor that takes a single String argument. If such constructor is defined, then it is invoked + * with the value of the second argument of this method. This is typically used to (but needs not) identify a + * property (by name) that holds additional configuration for the to be instantiated AuthProvider. This + * implementation will pass on any non-empty value to the constructor. When a configuration argument is provided, + * but no constructor exists in the implementation that accepts a single String value, this method will log a + * warning and attempt to return an instance based on the no-arg constructor of the class. + * + * @param implementationProperty A property that defines the class of the instance to be returned. + * @param configProperty A property that holds an opaque configuration string value passed to the constructor. + * @return A user provider (can be null). + */ + public static AuthProvider instantiate(@Nonnull final SystemProperty implementationProperty, @Nullable final SystemProperty configProperty) + { + final Class implementationClass = implementationProperty.getValue(); + if (implementationClass == null) { + Log.debug( "Property '{}' is undefined or has no value. Skipping.", implementationProperty.getKey() ); + return null; + } + Log.debug("About to to instantiate an AuthProvider '{}' based on the value of property '{}'.", implementationClass, implementationProperty.getKey()); + + try { + if (configProperty != null && configProperty.getValue() != null && !configProperty.getValue().isEmpty()) { + try { + final Constructor constructor = implementationClass.getConstructor(String.class); + final AuthProvider result = constructor.newInstance(configProperty.getValue()); + Log.debug("Instantiated AuthProvider '{}' with configuration: '{}'", implementationClass.getName(), configProperty.getValue()); + return result; + } catch (NoSuchMethodException e) { + Log.warn("Custom configuration is defined for the a provider but the configured class ('{}') does not provide a constructor that takes a String argument. Custom configuration will be ignored. Ignored configuration: '{}'", implementationProperty.getValue().getName(), configProperty); + } + } + + final AuthProvider result = implementationClass.getDeclaredConstructor().newInstance(); + Log.debug("Instantiated AuthProvider '{}'", implementationClass.getName()); + return result; + } catch (Exception e) { + Log.error("Unable to load AuthProvider '{}'. Data from this provider will not be available.", implementationClass.getName(), e); + return null; + } + } + /** * Returns all AuthProvider instances that serve as 'backing' providers. * diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/auth/HybridAuthProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/auth/HybridAuthProvider.java index 7869ac7194..95560630f6 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/auth/HybridAuthProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/auth/HybridAuthProvider.java @@ -54,101 +54,89 @@ * an override list, authentication will only be attempted with the associated provider * (bypassing the chaining logic).

* + * The primary provider is required, but all other properties are optional. Each provider should be configured as it is + * normally, using whatever XML configuration options it specifies. + * + * When using multiple providers of the same type, it typically is desirable to have distinct configuration for each + * provider. To do so, a property with the name 'config' can be used. If used, the value of this property is passed as a + * string to the constructor of the provider (for this to work, the provider must have a constructor that takes exactly + * one argument: a string). Typically, this value is used to reference another property name that the provider can use + * to obtain its information for, but the value is treated as an opaque string by this implementation. + * * The full list of properties: *

    - *
  • {@code hybridAuthProvider.primaryProvider.className} (required) -- the class name - * of the auth provider. - *
  • {@code hybridAuthProvider.primaryProvider.overrideList} -- a comma-delimitted list - * of usernames for which authentication will only be tried with this provider. - *
  • {@code hybridAuthProvider.secondaryProvider.className} -- the class name - * of the auth provider. - *
  • {@code hybridAuthProvider.secondaryProvider.overrideList} -- a comma-delimitted list - * of usernames for which authentication will only be tried with this provider. - *
  • {@code hybridAuthProvider.tertiaryProvider.className} -- the class name - * of the auth provider. - *
  • {@code hybridAuthProvider.tertiaryProvider.overrideList} -- a comma-delimitted list - * of usernames for which authentication will only be tried with this provider. + *
  • {@code hybridAuthProvider.primaryProvider.className} (required) -- the class name of the auth provider. + *
  • {@code hybridAuthProvider.primaryProvider.config} -- A value used by the auth provider for configuration (typically the name of another property). + *
  • {@code hybridAuthProvider.primaryProvider.overrideList} -- a comma-delimited list of usernames for which authentication will only be tried with this provider. + *
  • {@code hybridAuthProvider.secondaryProvider.className} -- the class name of the auth provider. + *
  • {@code hybridAuthProvider.secondaryProvider.config} -- A value used by the auth provider for configuration (typically the name of another property). + *
  • {@code hybridAuthProvider.secondaryProvider.overrideList} -- a comma-delimited list of usernames for which authentication will only be tried with this provider. + *
  • {@code hybridAuthProvider.tertiaryProvider.className} -- the class name of the auth provider. + *
  • {@code hybridAuthProvider.tertiaryProvider.config} -- A value used by the auth provider for configuration (typically the name of another property). + *
  • {@code hybridAuthProvider.tertiaryProvider.overrideList} -- a comma-delimited list of usernames for which authentication will only be tried with this provider. *
* - * The primary provider is required, but all other properties are optional. Each provider - * should be configured as it is normally, using whatever XML configuration options it specifies. - * * @author Matt Tucker */ public class HybridAuthProvider extends AuthMultiProvider { private static final Logger Log = LoggerFactory.getLogger(HybridAuthProvider.class); - private static final SystemProperty PRIMARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + public static final SystemProperty PRIMARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) .setKey("hybridAuthProvider.primaryProvider.className") .setBaseClass(AuthProvider.class) .setDefaultValue(DefaultAuthProvider.class) .setDynamic(false) .build(); - private static final SystemProperty SECONDARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + + public static final SystemProperty PRIMARY_PROVIDER_CONFIG = SystemProperty.Builder.ofType(String.class) + .setKey("hybridAuthProvider.primaryProvider.config") + .setDynamic(false) + .build(); + + public static final SystemProperty SECONDARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) .setKey("hybridAuthProvider.secondaryProvider.className") .setBaseClass(AuthProvider.class) .setDynamic(false) .build(); - private static final SystemProperty TERTIARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + + public static final SystemProperty SECONDARY_PROVIDER_CONFIG = SystemProperty.Builder.ofType(String.class) + .setKey("hybridAuthProvider.secondaryProvider.config") + .setDynamic(false) + .build(); + + public static final SystemProperty TERTIARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) .setKey("hybridAuthProvider.tertiaryProvider.className") .setBaseClass(AuthProvider.class) .setDynamic(false) .build(); - private AuthProvider primaryProvider; - private AuthProvider secondaryProvider; - private AuthProvider tertiaryProvider; + public static final SystemProperty TERTIARY_PROVIDER_CONFIG = SystemProperty.Builder.ofType(String.class) + .setKey("hybridAuthProvider.tertiaryProvider.config") + .setDynamic(false) + .build(); + + private final AuthProvider primaryProvider; + private final AuthProvider secondaryProvider; + private final AuthProvider tertiaryProvider; private final Set primaryOverrides = new HashSet<>(); private final Set secondaryOverrides = new HashSet<>(); private final Set tertiaryOverrides = new HashSet<>(); public HybridAuthProvider() { - // Convert XML based provider setup to Database based - JiveGlobals.migrateProperty(PRIMARY_PROVIDER.getKey()); - JiveGlobals.migrateProperty(SECONDARY_PROVIDER.getKey()); - JiveGlobals.migrateProperty(TERTIARY_PROVIDER.getKey()); JiveGlobals.migrateProperty("hybridAuthProvider.primaryProvider.overrideList"); JiveGlobals.migrateProperty("hybridAuthProvider.secondaryProvider.overrideList"); JiveGlobals.migrateProperty("hybridAuthProvider.tertiaryProvider.overrideList"); // Load primary, secondary, and tertiary auth providers. - final Class primaryClass = PRIMARY_PROVIDER.getValue(); - if (primaryClass == null) { + primaryProvider = instantiate(PRIMARY_PROVIDER, PRIMARY_PROVIDER_CONFIG); + secondaryProvider = instantiate(SECONDARY_PROVIDER, SECONDARY_PROVIDER_CONFIG); + tertiaryProvider = instantiate(TERTIARY_PROVIDER, TERTIARY_PROVIDER_CONFIG); + + if (primaryProvider == null) { Log.error("A primary AuthProvider must be specified. Authentication will be disabled."); return; } - try { - primaryProvider = (AuthProvider)primaryClass.newInstance(); - Log.debug("Primary auth provider: " + primaryClass.getName()); - } - catch (Exception e) { - Log.error("Unable to load primary auth provider: " + primaryClass.getName() + - ". Authentication will be disabled.", e); - return; - } - - final Class secondaryClass = SECONDARY_PROVIDER.getValue(); - if (secondaryClass != null) { - try { - secondaryProvider = (AuthProvider)secondaryClass.newInstance(); - Log.debug("Secondary auth provider: " + secondaryClass.getName()); - } - catch (Exception e) { - Log.error("Unable to load secondary auth provider: " + secondaryClass.getName(), e); - } - } - - final Class tertiaryClass = TERTIARY_PROVIDER.getValue(); - if (tertiaryClass != null) { - try { - tertiaryProvider = (AuthProvider)tertiaryClass.newInstance(); - Log.debug("Tertiary auth provider: " + tertiaryClass.getName()); - } - catch (Exception e) { - Log.error("Unable to load tertiary auth provider: " + tertiaryClass.getName(), e); - } - } // Now, load any overrides. String overrideList = JiveGlobals.getProperty( @@ -246,7 +234,7 @@ public void authenticate(String username, String password) throws UnauthorizedEx @Override public String getPassword(String username) - throws UserNotFoundException, UnsupportedOperationException + throws UserNotFoundException, UnsupportedOperationException { // Check overrides first. try { diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapAuthProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapAuthProvider.java index a97cc55bb0..2d01b912c3 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapAuthProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapAuthProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2004-2008 Jive Software, 2016-2019 Ignite Realtime Foundation. All rights reserved. + * Copyright (C) 2004-2008 Jive Software, 2016-2024 Ignite Realtime Foundation. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -44,7 +44,7 @@ *
  • {@code ldap.authCache.size} -- size in bytes of the auth cache. If property is * not set, the default value is 524288 (512 K).
  • *
  • {@code ldap.authCache.maxLifetime} -- maximum amount of time a hashed password - * can be cached in milleseconds. If property is not set, the default value is + * can be cached in milliseconds. If property is not set, the default value is * 7200000 (2 hours).
  • * * @@ -52,18 +52,23 @@ */ public class LdapAuthProvider implements AuthProvider { - private static final Logger Log = LoggerFactory.getLogger(LdapAuthProvider.class); + private final Logger Log; - private LdapManager manager; + private final LdapManager manager; private Cache authCache = null; public LdapAuthProvider() { + this(null); // Convert XML based provider setup to Database based JiveGlobals.migrateProperty("ldap.authCache.enabled"); + } + + public LdapAuthProvider(final String ldapConfigPropertyName) { + Log = LoggerFactory.getLogger(LdapAuthProvider.class.getName() + (ldapConfigPropertyName == null ? "" : ( "[" + ldapConfigPropertyName + "]" ))); - manager = LdapManager.getInstance(); + manager = LdapManager.getInstance(ldapConfigPropertyName); if (JiveGlobals.getBooleanProperty("ldap.authCache.enabled", false)) { - String cacheName = "LDAP Authentication"; + String cacheName = "LDAP Authentication" + (ldapConfigPropertyName == null ? "" : ( " (" + ldapConfigPropertyName + ")" ) ); authCache = CacheFactory.createCache(cacheName); } } diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapGroupProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapGroupProvider.java index ddb8966ea7..8f1e3ee83e 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapGroupProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapGroupProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2005-2008 Jive Software, 2017-2023 Ignite Realtime Foundation. All rights reserved. + * Copyright (C) 2005-2008 Jive Software, 2017-2024 Ignite Realtime Foundation. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import org.jivesoftware.openfire.group.GroupNotFoundException; import org.jivesoftware.openfire.user.UserManager; import org.jivesoftware.openfire.user.UserNotFoundException; +import org.jivesoftware.util.SystemProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.packet.JID; @@ -44,8 +45,6 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -import org.jivesoftware.util.SystemProperty; - /** * LDAP implementation of the GroupProvider interface. All data in the directory is treated as * read-only so any set operations will result in an exception. @@ -54,7 +53,7 @@ */ public class LdapGroupProvider extends AbstractGroupProvider { - private static final Logger Log = LoggerFactory.getLogger(LdapGroupProvider.class); + private final Logger Log; private LdapManager manager; private UserManager userManager; @@ -72,8 +71,15 @@ public class LdapGroupProvider extends AbstractGroupProvider { * Constructs a new LDAP group provider. */ public LdapGroupProvider() { + this(null); + } + + public LdapGroupProvider(String ldapConfigPropertyName) + { super(); - manager = LdapManager.getInstance(); + Log = LoggerFactory.getLogger(LdapGroupProvider.class.getName() + (ldapConfigPropertyName == null ? "" : ( "[" + ldapConfigPropertyName + "]" ))); + + manager = LdapManager.getInstance(ldapConfigPropertyName); userManager = UserManager.getInstance(); standardAttributes = new String[3]; standardAttributes[0] = manager.getGroupNameField(); diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapManager.java b/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapManager.java index b22f7433e2..dffed31c1b 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapManager.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapManager.java @@ -27,6 +27,7 @@ import org.xmpp.packet.JID; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.naming.*; import javax.naming.directory.*; import javax.naming.ldap.*; @@ -42,7 +43,7 @@ /** * Centralized administration of LDAP connections. The {@link #getInstance()} method - * should be used to get an instace. The following properties configure this manager: + * should be used to get an instance. The following properties configure this manager: * *
      *
    • ldap.host
    • @@ -94,7 +95,7 @@ */ public class LdapManager { - private static final Logger Log = LoggerFactory.getLogger(LdapManager.class); + private final Logger Log; private static final String DEFAULT_LDAP_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory"; public static final SystemProperty LDAP_PAGE_SIZE = SystemProperty.Builder.ofType(Integer.class) .setKey("ldap.pagedResultsSize") @@ -115,83 +116,8 @@ public class LdapManager { public static Instant lastUnencryptedWarning = Instant.EPOCH; - private static LdapManager instance; - static { - // Create a special Map implementation to wrap XMLProperties. We only implement - // the get, put, and remove operations, since those are the only ones used. Using a Map - // makes it easier to perform LdapManager testing. - Map properties = new Map() { - - @Override - public String get(Object key) { - return JiveGlobals.getProperty((String)key); - } - - @Override - public String put(String key, String value) { - JiveGlobals.setProperty(key, value); - // Always return null since XMLProperties doesn't support the normal semantics. - return null; - } - - @Override - public String remove(Object key) { - JiveGlobals.deleteProperty((String)key); - // Always return null since XMLProperties doesn't support the normal semantics. - return null; - } - - - @Override - public int size() { - return 0; - } - - @Override - public boolean isEmpty() { - return false; - } - - @Override - public boolean containsKey(Object key) { - return false; - } - - @Override - public boolean containsValue(Object value) { - return false; - } - - @Override - public void putAll(Map t) { - } - - @Override - public void clear() { - } - - @Override - public Set keySet() { - return null; - } - - @Override - public Collection values() { - return null; - } - - @Override - public Set> entrySet() { - return null; - } - }; - instance = new LdapManager(properties); - } - - /** Exposed for test use only */ - public static void setInstance(LdapManager instance) { - LdapManager.instance = instance; - } + private static final Map instances = new HashMap<>(); + private final String propertyPrefix; private Collection hosts = new ArrayList<>(); private int port; @@ -233,9 +159,120 @@ public static void setInstance(LdapManager instance) { * @return an LdapManager instance. */ public static LdapManager getInstance() { - return instance; + return getInstance(null); + } + + /** + * Provides singleton access to a named instance of the LdapManager class. + * + * This constructor is useful when several, different LDAP configurations are used (eg: A Hybrid Provider that + * uses more than one LDAP server). + * + * @param name The name of the ldap instance. + * @return an LdapManager instance. + */ + public synchronized static LdapManager getInstance(final String name) { + if (instances.containsKey(name)) { + return instances.get(name); + } else { + // the get, put, and remove operations, since those are the only ones used. Using a Map + // makes it easier to perform LdapManager testing. + Map properties = new Map() + { + String getKey(Object key) { + return getPrefixedPropertyName(name, (String) key); + } + + @Override + public String get(Object key) + { + return JiveGlobals.getProperty(getKey(key)); + } + + @Override + public String put(String key, String value) + { + JiveGlobals.setProperty(getKey(key), value); + // Always return null since XMLProperties doesn't support the normal semantics. + return null; + } + + @Override + public String remove(Object key) + { + JiveGlobals.deleteProperty(getKey(key)); + // Always return null since XMLProperties doesn't support the normal semantics. + return null; + } + + + @Override + public int size() + { + return 0; + } + + @Override + public boolean isEmpty() + { + return false; + } + + @Override + public boolean containsKey(Object key) + { + return false; + } + + @Override + public boolean containsValue(Object value) + { + return false; + } + + @Override + public void putAll(Map t) + { + } + + @Override + public void clear() + { + } + + @Override + public Set keySet() + { + return null; + } + + @Override + public Collection values() + { + return null; + } + + @Override + public Set> entrySet() + { + return null; + } + }; + LdapManager instance = new LdapManager(properties, name); + instances.put(name, instance); + return instance; + } + } + + static String getPrefixedPropertyName(@Nullable final String prefix, @Nonnull final String propertyName) { + return prefix == null ? propertyName : prefix + "." + propertyName.substring("ldap.".length()); } + String getPrefixedPropertyName(@Nonnull final String propertyName) { + return propertyPrefix == null ? propertyName : propertyPrefix + "." + propertyName.substring("ldap.".length()); + } + + /** * Constructs a new LdapManager instance. Typically, {@link #getInstance()} should be * called instead of this method. LdapManager instances should only be created directly @@ -245,44 +282,50 @@ public static LdapManager getInstance() { * LDAP host and base DN. */ public LdapManager(Map properties) { + this(properties, null); + } + + public LdapManager(Map properties, String propertyPrefix) { + this.propertyPrefix = propertyPrefix; + Log = LoggerFactory.getLogger(LdapManager.class.getName() + (propertyPrefix == null ? "" : ("["+propertyPrefix+"]"))); this.properties = properties; // Convert XML based provider setup to Database based - JiveGlobals.migrateProperty("ldap.host"); - JiveGlobals.migrateProperty("ldap.port"); - JiveGlobals.migrateProperty("ldap.readTimeout"); - JiveGlobals.migrateProperty("ldap.usernameField"); - JiveGlobals.migrateProperty("ldap.usernameSuffix"); - JiveGlobals.migrateProperty("ldap.baseDN"); - JiveGlobals.migrateProperty("ldap.alternateBaseDN"); - JiveGlobals.migrateProperty("ldap.nameField"); - JiveGlobals.migrateProperty("ldap.emailField"); - JiveGlobals.migrateProperty("ldap.connectionPoolEnabled"); - JiveGlobals.migrateProperty("ldap.searchFilter"); - JiveGlobals.migrateProperty("ldap.subTreeSearch"); - JiveGlobals.migrateProperty("ldap.groupNameField"); - JiveGlobals.migrateProperty("ldap.groupMemberField"); - JiveGlobals.migrateProperty("ldap.groupDescriptionField"); - JiveGlobals.migrateProperty("ldap.posixMode"); - JiveGlobals.migrateProperty("ldap.groupSearchFilter"); - JiveGlobals.migrateProperty("ldap.flattenNestedGroups"); - JiveGlobals.migrateProperty("ldap.adminDN"); - JiveGlobals.migrateProperty("ldap.adminPassword"); - JiveGlobals.migrateProperty("ldap.debugEnabled"); - JiveGlobals.migrateProperty("ldap.sslEnabled"); - JiveGlobals.migrateProperty("ldap.startTlsEnabled"); - JiveGlobals.migrateProperty("ldap.autoFollowReferrals"); - JiveGlobals.migrateProperty("ldap.autoFollowAliasReferrals"); - JiveGlobals.migrateProperty("ldap.encloseUserDN"); - JiveGlobals.migrateProperty("ldap.encloseGroupDN"); - JiveGlobals.migrateProperty("ldap.encloseDNs"); - JiveGlobals.migrateProperty("ldap.initialContextFactory"); - JiveGlobals.migrateProperty("ldap.clientSideSorting"); - JiveGlobals.migrateProperty("ldap.ldapDebugEnabled"); - JiveGlobals.migrateProperty("ldap.encodeMultibyteCharacters"); - - if (JiveGlobals.getBooleanProperty("ldap.userDNCache.enabled", true)) { - String cacheName = "LDAP UserDN"; + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.host")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.port")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.readTimeout")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.usernameField")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.usernameSuffix")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.baseDN")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.alternateBaseDN")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.nameField")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.emailField")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.connectionPoolEnabled")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.searchFilter")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.subTreeSearch")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.groupNameField")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.groupMemberField")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.groupDescriptionField")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.posixMode")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.groupSearchFilter")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.flattenNestedGroups")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.adminDN")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.adminPassword")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.debugEnabled")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.sslEnabled")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.startTlsEnabled")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.autoFollowReferrals")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.autoFollowAliasReferrals")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.encloseUserDN")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.encloseGroupDN")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.encloseDNs")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.initialContextFactory")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.clientSideSorting")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.ldapDebugEnabled")); + JiveGlobals.migrateProperty(getPrefixedPropertyName("ldap.encodeMultibyteCharacters")); + + if (JiveGlobals.getBooleanProperty(getPrefixedPropertyName("ldap.userDNCache.enabled"), true)) { + String cacheName = "LDAP UserDN" + (propertyPrefix == null ? "" : (" (" + propertyPrefix + ")")); userDNCache = CacheFactory.createCache( cacheName ); } @@ -295,6 +338,10 @@ public LdapManager(Map properties) { hosts.add(st.nextToken()); } } + if (host == null || host.isEmpty()) { + Log.warn("No host value found in property '{}'", getPrefixedPropertyName("ldap.host")); + } + String portStr = properties.get("ldap.port"); port = 389; if (portStr != null) { @@ -533,7 +580,7 @@ public LdapName parseAsLdapNameOrLog( String value ) * @return A relative distinguished name from the answer. * @throws NamingException When the search result value cannot be used to form a valid RDN value. */ - public static Rdn[] getRelativeDNFromResult( SearchResult answer ) throws NamingException + public Rdn[] getRelativeDNFromResult( SearchResult answer ) throws NamingException { // All other methods assume that UserDN is a relative distinguished name, // not a (full) distinguished name. However if a referral was followed, @@ -1400,8 +1447,23 @@ String getProviderURL(LdapName baseDN) throws NamingException { for ( String host : hosts ) { + // If a host-definition contains a port (eg: example.org:389 vs example.org) then use that port, rather than the port that's configured in ldap.port + final String[] split = host.split(":"); + String specificHost = host; + int specificPort = this.port; + if (split.length == 2) { + try { + final int number = Integer.parseInt(split[1]); + if (number > 0 && number < 65535) { + specificHost = split[0]; + specificPort = number; + } + } catch (NumberFormatException e) { + Log.trace("Unable to determine port number from value '{}'. Expected format: 'hostname' or 'hostname:port'", host); + } + } // Create a correctly-encoded ldap URL for the PROVIDER_URL - final URI uri = new URI(sslEnabled ? "ldaps" : "ldap", null, host, port, "/" + baseDN.toString(), null, null); + final URI uri = new URI(sslEnabled ? "ldaps" : "ldap", null, specificHost, specificPort, "/" + baseDN.toString(), null, null); ldapURL.append(uri.toASCIIString()); ldapURL.append(" "); } diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapUserProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapUserProvider.java index 46d8518cd3..697b7570f3 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapUserProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapUserProvider.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2004-2008 Jive Software, 2016-2020 Ignite Realtime Foundation. All rights reserved. + * Copyright (C) 2004-2008 Jive Software, 2016-2024 Ignite Realtime Foundation. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -46,7 +46,7 @@ */ public class LdapUserProvider implements UserProvider { - private static final Logger Log = LoggerFactory.getLogger(LdapUserProvider.class); + private final Logger Log; // LDAP date format parser. private static final SimpleDateFormat ldapDateFormat = new SimpleDateFormat("yyyyMMddHHmmss"); @@ -59,13 +59,19 @@ public class LdapUserProvider implements UserProvider { private Collection allUsers = null; public LdapUserProvider() { + this(null); + } + + public LdapUserProvider(String ldapConfigPropertyName) { + Log = LoggerFactory.getLogger(LdapUserProvider.class.getName() + (ldapConfigPropertyName == null ? "" : ( "[" + ldapConfigPropertyName + "]" ))); + // Convert XML based provider setup to Database based JiveGlobals.migrateProperty("ldap.searchFields"); - manager = LdapManager.getInstance(); + manager = LdapManager.getInstance(ldapConfigPropertyName); searchFields = new LinkedHashMap<>(); String fieldList = JiveGlobals.getProperty("ldap.searchFields"); - // If the value isn't present, default to to username, name, and email. + // If the value isn't present, default to the username, name, and email. if (fieldList == null) { searchFields.put("Username", manager.getUsernameField()); int i = 0; @@ -412,7 +418,7 @@ private static Date parseLDAPDate(String dateText) { date = ldapDateFormat.parse(dateText); } catch (Exception e) { - Log.error(e.getMessage(), e); + LoggerFactory.getLogger(LdapUserProvider.class).error(e.getMessage(), e); } return date; } diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/user/HybridUserProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/user/HybridUserProvider.java index f37d0e4f24..0df00da22b 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/user/HybridUserProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/user/HybridUserProvider.java @@ -16,12 +16,11 @@ package org.jivesoftware.openfire.user; -import org.jivesoftware.util.JiveGlobals; +import org.jivesoftware.util.SystemProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; -import java.util.Date; import java.util.List; /** @@ -40,30 +39,55 @@ public class HybridUserProvider extends UserMultiProvider { private static final Logger Log = LoggerFactory.getLogger( HybridUserProvider.class ); + private static final SystemProperty PRIMARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + .setKey("hybridUserProvider.primaryProvider.className") + .setBaseClass(UserProvider.class) + .setDynamic(false) + .build(); + + public static final SystemProperty PRIMARY_PROVIDER_CONFIG = SystemProperty.Builder.ofType(String.class) + .setKey("hybridUserProvider.primaryProvider.config") + .setDynamic(false) + .build(); + + private static final SystemProperty SECONDARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + .setKey("hybridUserProvider.secondaryProvider.className") + .setBaseClass(UserProvider.class) + .setDynamic(false) + .build(); + + public static final SystemProperty SECONDARY_PROVIDER_CONFIG = SystemProperty.Builder.ofType(String.class) + .setKey("hybridUserProvider.secondaryProvider.config") + .setDynamic(false) + .build(); + + private static final SystemProperty TERTIARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + .setKey("hybridUserProvider.tertiaryProvider.className") + .setBaseClass(UserProvider.class) + .setDynamic(false) + .build(); + + public static final SystemProperty TERTIARY_PROVIDER_CONFIG = SystemProperty.Builder.ofType(String.class) + .setKey("hybridUserProvider.tertiaryProvider.config") + .setDynamic(false) + .build(); + private final List userProviders = new ArrayList<>(); public HybridUserProvider() { - // Migrate user provider properties - JiveGlobals.migrateProperty( "hybridUserProvider.primaryProvider.className" ); - JiveGlobals.migrateProperty( "hybridUserProvider.secondaryProvider.className" ); - JiveGlobals.migrateProperty( "hybridUserProvider.tertiaryProvider.className" ); - // Load primary, secondary, and tertiary user providers. - final UserProvider primary = instantiate( "hybridUserProvider.primaryProvider.className" ); - if ( primary != null ) - { - userProviders.add( primary ); + final UserProvider primary = instantiate(PRIMARY_PROVIDER, PRIMARY_PROVIDER_CONFIG); + if (primary != null) { + userProviders.add(primary); } - final UserProvider secondary = instantiate( "hybridUserProvider.secondaryProvider.className" ); - if ( secondary != null ) - { - userProviders.add( secondary ); + final UserProvider secondary = instantiate(SECONDARY_PROVIDER, SECONDARY_PROVIDER_CONFIG); + if (secondary != null) { + userProviders.add(secondary); } - final UserProvider tertiary = instantiate( "hybridUserProvider.tertiaryProvider.className" ); - if ( tertiary != null ) - { - userProviders.add( tertiary ); + final UserProvider tertiary = instantiate(TERTIARY_PROVIDER, TERTIARY_PROVIDER_CONFIG); + if (tertiary != null) { + userProviders.add(tertiary); } // Verify that there's at least one provider available. diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/user/UserMultiProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/user/UserMultiProvider.java index 76be5e58f0..661cb1d712 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/user/UserMultiProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/user/UserMultiProvider.java @@ -18,9 +18,13 @@ import org.jivesoftware.util.ClassUtils; import org.jivesoftware.util.JiveGlobals; +import org.jivesoftware.util.SystemProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.lang.reflect.Constructor; import java.util.*; /** @@ -41,7 +45,9 @@ public abstract class UserMultiProvider implements UserProvider * * @param propertyName A property name (cannot be null). * @return A user provider (can be null). + * @deprecated Use {@link #instantiate(SystemProperty)} or {@link #instantiate(SystemProperty, SystemProperty)} instead. */ + @Deprecated(forRemoval = true, since = "5.0.0") // TODO Remove in or after Openfire 5.1.0 public static UserProvider instantiate( String propertyName ) { final String className = JiveGlobals.getProperty( propertyName ); @@ -65,6 +71,68 @@ public static UserProvider instantiate( String propertyName ) } } + /** + * Instantiates a UserProvider based on Class-based system property. When the property is not set, this + * method returns null. When the property is set, but an exception occurs while instantiating the class, this method + * logs the error and returns null. + * + * UserProvider classes are required to have a public, no-argument constructor. + * + * @param implementationProperty A property that defines the class of the instance to be returned. + * @return A user provider (can be null). + */ + public static UserProvider instantiate(@Nonnull final SystemProperty implementationProperty) + { + return instantiate(implementationProperty, null); + } + + /** + * Instantiates a UserProvider based on Class-based system property. When the property is not set, this + * method returns null. When the property is set, but an exception occurs while instantiating the class, this method + * logs the error and returns null. + * + * UserProvider classes are required to have a public, no-argument constructor, but can have an optional additional + * constructor that takes a single String argument. If such constructor is defined, then it is invoked with the + * value of the second argument of this method. This is typically used to (but needs not) identify a property + * (by name) that holds additional configuration for the to be instantiated UserProvider. This + * implementation will pass on any non-empty value to the constructor. When a configuration argument is provided, + * but no constructor exists in the implementation that accepts a single String value, this method will log a + * warning and attempt to return an instance based on the no-arg constructor of the class. + * + * @param implementationProperty A property that defines the class of the instance to be returned. + * @param configProperty an opaque string value passed to the constructor. + * @return A user provider (can be null). + */ + public static UserProvider instantiate(@Nonnull final SystemProperty implementationProperty, @Nullable final SystemProperty configProperty) + { + final Class implementationClass = implementationProperty.getValue(); + if (implementationClass == null) { + Log.debug( "Property '{}' is undefined or has no value. Skipping.", implementationProperty.getKey() ); + return null; + } + Log.debug("About to to instantiate an UserProvider '{}' based on the value of property '{}'.", implementationClass, implementationProperty.getKey()); + + try { + if (configProperty != null && configProperty.getValue() != null && !configProperty.getValue().isEmpty()) { + try { + final Constructor constructor = implementationClass.getConstructor(String.class); + final UserProvider result = constructor.newInstance(configProperty.getValue()); + Log.debug("Instantiated UserProvider '{}' with configuration: '{}'", implementationClass.getName(), configProperty.getValue()); + return result; + } catch (NoSuchMethodException e) { + Log.warn("Custom configuration is defined for the a provider but the configured class ('{}') does not provide a constructor that takes a String argument. Custom configuration will be ignored. Ignored configuration: '{}'", implementationProperty.getValue().getName(), configProperty); + } + } + + final UserProvider result = implementationClass.getDeclaredConstructor().newInstance(); + Log.debug("Instantiated UserProvider '{}'", implementationClass.getName()); + return result; + } catch (Exception e) { + Log.error("Unable to load UserProvider '{}'. Data from this provider will not be available.", implementationClass.getName(), e); + return null; + } + } + /** * Returns all UserProvider instances that serve as 'backing' providers. * diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/HybridUserPropertyProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/HybridUserPropertyProvider.java index 284f5e9a16..0d69c9c887 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/HybridUserPropertyProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/HybridUserPropertyProvider.java @@ -16,7 +16,7 @@ package org.jivesoftware.openfire.user.property; import org.jivesoftware.openfire.user.UserNotFoundException; -import org.jivesoftware.util.JiveGlobals; +import org.jivesoftware.util.SystemProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,27 +52,55 @@ public class HybridUserPropertyProvider extends UserPropertyMultiProvider { private static final Logger Log = LoggerFactory.getLogger( HybridUserPropertyProvider.class ); + private static final SystemProperty PRIMARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + .setKey("hybridUserPropertyProvider.primaryProvider.className") + .setBaseClass(UserPropertyProvider.class) + .setDynamic(false) + .build(); + + public static final SystemProperty PRIMARY_PROVIDER_CONFIG = SystemProperty.Builder.ofType(String.class) + .setKey("hybridUserPropertyProvider.primaryProvider.config") + .setDynamic(false) + .build(); + + private static final SystemProperty SECONDARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + .setKey("hybridUserPropertyProvider.secondaryProvider.className") + .setBaseClass(UserPropertyProvider.class) + .setDynamic(false) + .build(); + + public static final SystemProperty SECONDARY_PROVIDER_CONFIG = SystemProperty.Builder.ofType(String.class) + .setKey("hybridUserPropertyProvider.secondaryProvider.config") + .setDynamic(false) + .build(); + + private static final SystemProperty TERTIARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + .setKey("hybridUserPropertyProvider.tertiaryProvider.className") + .setBaseClass(UserPropertyProvider.class) + .setDynamic(false) + .build(); + + public static final SystemProperty TERTIARY_PROVIDER_CONFIG = SystemProperty.Builder.ofType(String.class) + .setKey("hybridUserPropertyProvider.tertiaryProvider.config") + .setDynamic(false) + .build(); + private final List providers = new ArrayList<>(); public HybridUserPropertyProvider() { - // Migrate user provider properties - JiveGlobals.migrateProperty( "hybridUserPropertyProvider.primaryProvider.className" ); - JiveGlobals.migrateProperty( "hybridUserPropertyProvider.secondaryProvider.className" ); - JiveGlobals.migrateProperty( "hybridUserPropertyProvider.tertiaryProvider.className" ); - // Load primary, secondary, and tertiary user providers. - final UserPropertyProvider primary = MappedUserPropertyProvider.instantiate( "hybridUserPropertyProvider.primaryProvider.className" ); + final UserPropertyProvider primary = MappedUserPropertyProvider.instantiate(PRIMARY_PROVIDER, PRIMARY_PROVIDER_CONFIG); if ( primary != null ) { providers.add( primary ); } - final UserPropertyProvider secondary = MappedUserPropertyProvider.instantiate( "hybridUserPropertyProvider.secondaryProvider.className" ); + final UserPropertyProvider secondary = MappedUserPropertyProvider.instantiate(SECONDARY_PROVIDER, SECONDARY_PROVIDER_CONFIG); if ( secondary != null ) { providers.add( secondary ); } - final UserPropertyProvider tertiary = MappedUserPropertyProvider.instantiate( "hybridUserPropertyProvider.tertiaryProvider.className" ); + final UserPropertyProvider tertiary = MappedUserPropertyProvider.instantiate(TERTIARY_PROVIDER, TERTIARY_PROVIDER_CONFIG); if ( tertiary != null ) { providers.add( tertiary ); diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/UserPropertyMultiProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/UserPropertyMultiProvider.java index f1dd42bf4e..f0837058c0 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/UserPropertyMultiProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/UserPropertyMultiProvider.java @@ -18,9 +18,13 @@ import org.jivesoftware.openfire.user.UserNotFoundException; import org.jivesoftware.util.ClassUtils; import org.jivesoftware.util.JiveGlobals; +import org.jivesoftware.util.SystemProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.lang.reflect.Constructor; import java.util.Collection; import java.util.Map; @@ -41,9 +45,11 @@ public abstract class UserPropertyMultiProvider implements UserPropertyProvider * UserPropertyProvider classes are required to have a public, no-argument constructor. * * @param propertyName A property name (cannot be null). - * @return A user proerty provider (can be null). + * @return A user provider (can be null). + * @deprecated Use {@link #instantiate(SystemProperty)} or {@link #instantiate(SystemProperty, SystemProperty)} instead. */ - public static UserPropertyProvider instantiate(String propertyName) + @Deprecated(forRemoval = true, since = "5.0.0") // TODO Remove in or after Openfire 5.1.0 + public static UserPropertyProvider instantiate( String propertyName ) { final String className = JiveGlobals.getProperty( propertyName ); if ( className == null ) @@ -66,6 +72,68 @@ public static UserPropertyProvider instantiate(String propertyName) } } + /** + * Instantiates a UserPropertyProvider based on Class-based system property. When the property is not set, this + * method returns null. When the property is set, but an exception occurs while instantiating the class, this method + * logs the error and returns null. + * + * UserPropertyProvider classes are required to have a public, no-argument constructor. + * + * @param implementationProperty A property that defines the class of the instance to be returned. + * @return A user provider (can be null). + */ + public static UserPropertyProvider instantiate(@Nonnull final SystemProperty implementationProperty) + { + return instantiate(implementationProperty, null); + } + + /** + * Instantiates a UserPropertyProvider based on Class-based system property. When the property is not set, this + * method returns null. When the property is set, but an exception occurs while instantiating the class, this method + * logs the error and returns null. + * + * UserPropertyProvider classes are required to have a public, no-argument constructor, but can have an optional + * additional constructor that takes a single String argument. If such constructor is defined, then it is invoked + * with the value of the second argument of this method. This is typically used to (but needs not) identify a + * property (by name) that holds additional configuration for the to be instantiated UserPropertyProvider. This + * implementation will pass on any non-empty value to the constructor. When a configuration argument is provided, + * but no constructor exists in the implementation that accepts a single String value, this method will log a + * warning and attempt to return an instance based on the no-arg constructor of the class. + * + * @param implementationProperty A property that defines the class of the instance to be returned. + * @param configProperty A property that holds an opaque configuration string value passed to the constructor. + * @return A user provider (can be null). + */ + public static UserPropertyProvider instantiate(@Nonnull final SystemProperty implementationProperty, @Nullable final SystemProperty configProperty) + { + final Class implementationClass = implementationProperty.getValue(); + if (implementationClass == null) { + Log.debug( "Property '{}' is undefined or has no value. Skipping.", implementationProperty.getKey() ); + return null; + } + Log.debug("About to to instantiate an UserPropertyProvider '{}' based on the value of property '{}'.", implementationClass, implementationProperty.getKey()); + + try { + if (configProperty != null && configProperty.getValue() != null && !configProperty.getValue().isEmpty()) { + try { + final Constructor constructor = implementationClass.getConstructor(String.class); + final UserPropertyProvider result = constructor.newInstance(configProperty.getValue()); + Log.debug("Instantiated UserPropertyProvider '{}' with configuration: '{}'", implementationClass.getName(), configProperty.getValue()); + return result; + } catch (NoSuchMethodException e) { + Log.warn("Custom configuration is defined for the a provider but the configured class ('{}') does not provide a constructor that takes a String argument. Custom configuration will be ignored. Ignored configuration: '{}'", implementationProperty.getValue().getName(), configProperty); + } + } + + final UserPropertyProvider result = implementationClass.getDeclaredConstructor().newInstance(); + Log.debug("Instantiated UserPropertyProvider '{}'", implementationClass.getName()); + return result; + } catch (Exception e) { + Log.error("Unable to load UserPropertyProvider '{}'. Data from this provider will not be available.", implementationClass.getName(), e); + return null; + } + } + /** * Returns all UserPropertyProvider instances that serve as 'backing' providers. * diff --git a/xmppserver/src/test/java/org/jivesoftware/util/LDAPTest.java b/xmppserver/src/test/java/org/jivesoftware/util/LDAPTest.java index 3689fc02f0..d3f554e588 100644 --- a/xmppserver/src/test/java/org/jivesoftware/util/LDAPTest.java +++ b/xmppserver/src/test/java/org/jivesoftware/util/LDAPTest.java @@ -25,7 +25,7 @@ import javax.naming.directory.SearchResult; import javax.naming.ldap.Rdn; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * @author Daniel Henninger @@ -128,7 +128,7 @@ public void testGetRelativeDNFromResultSingleValue() throws Exception final SearchResult input = new SearchResult( "cn=bender", null, new BasicAttributes(), true ); // Execute system under test. - final Rdn[] result = LdapManager.getRelativeDNFromResult( input ); + final Rdn[] result = LdapManager.getInstance().getRelativeDNFromResult( input ); // Verify result. assertEquals( 1, result.length ); @@ -147,7 +147,7 @@ public void testGetRelativeDNFromResultMultiValue() throws Exception final SearchResult input = new SearchResult( "cn=bender,ou=people", null, new BasicAttributes(), true ); // Execute system under test. - final Rdn[] result = LdapManager.getRelativeDNFromResult( input ); + final Rdn[] result = LdapManager.getInstance().getRelativeDNFromResult( input ); // Verify result. assertEquals( 2, result.length ); @@ -170,7 +170,7 @@ public void testGetRelativeDNFromResultQuoted() throws Exception final SearchResult input = new SearchResult( "\"cn=ship crew/cooks\"", null, new BasicAttributes(), true ); // Execute system under test. - final Rdn[] result = LdapManager.getRelativeDNFromResult( input ); + final Rdn[] result = LdapManager.getInstance().getRelativeDNFromResult( input ); // Verify result. assertEquals( 1, result.length ); From fbd1d71cc0e004769c3918bd7cca104c38558a9a Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Thu, 12 Dec 2024 22:52:40 +0100 Subject: [PATCH 06/14] OF-2926: Implement LdapAuthProvider's documented cache config options This commit fulfills the promise expressed in documentation, by adding the configuration options that are defined for the cache used by LdapAuthProvider. --- .../main/resources/openfire_i18n.properties | 3 ++ .../openfire/ldap/LdapAuthProvider.java | 37 +++++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/i18n/src/main/resources/openfire_i18n.properties b/i18n/src/main/resources/openfire_i18n.properties index 5c3df6adda..3231664fed 100644 --- a/i18n/src/main/resources/openfire_i18n.properties +++ b/i18n/src/main/resources/openfire_i18n.properties @@ -1347,6 +1347,9 @@ system_property.ldap.pagedResultsSize=The maximum number of records to retrieve Note that if using ActiveDirectory, this should not be left at the default, and should not be set to more than the value of the ActiveDirectory MaxPageSize; 1,000 by default. system_property.ldap.useRangeRetrieval=Enable range retrieval for processing of large LDAP groups system_property.ldap.unencrypted-warning-suppression=Openfire will log a warning when interacting with LDAP using an unencrypted connection. To prevent flooding of the logfiles, subsequent warnings are suppressed for the duration configured by this property. +system_property.ldap.authCache.enabled=When enabled, user credentials obtained from a directory service (AD/LDAP) will be cached for a while. +system_property.ldap.authCache.size=Size (in bytes) of the cache in which user credentials obtained from a directory service are cached. +system_property.ldap.authCache.maxLifetime=Maximum time that user credentials obtained from a directory service are kept in cache. system_property.xmpp.iqdiscoinfo.xformsoftwareversion=Set to false to not allow Software Version DataForm on InfoDisco response. system_property.plugins.servlet.allowLocalFileReading=Determines if the plugin servlets can be used to access files outside of Openfire's home directory. system_property.cert.storewatcher.enabled=Automatically reloads certificate stores when they're modified on disk. diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapAuthProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapAuthProvider.java index 2d01b912c3..5a974d4512 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapAuthProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapAuthProvider.java @@ -20,8 +20,8 @@ import org.jivesoftware.openfire.auth.AuthProvider; import org.jivesoftware.openfire.auth.UnauthorizedException; import org.jivesoftware.openfire.user.UserNotFoundException; -import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.StringUtils; +import org.jivesoftware.util.SystemProperty; import org.jivesoftware.util.cache.Cache; import org.jivesoftware.util.cache.CacheFactory; import org.slf4j.Logger; @@ -30,6 +30,8 @@ import javax.naming.CommunicationException; import javax.naming.ldap.Rdn; +import java.time.Duration; +import java.time.temporal.ChronoUnit; /** * Implementation of auth provider interface for LDAP authentication service plug-in. @@ -54,28 +56,49 @@ public class LdapAuthProvider implements AuthProvider { private final Logger Log; + public static final SystemProperty AUTH_CACHE_ENABLED = SystemProperty.Builder.ofType(Boolean.class) + .setKey("ldap.authCache.enabled") + .setDefaultValue(false) + .setDynamic(false) + .build(); + + public static final SystemProperty AUTH_CACHE_SIZE = SystemProperty.Builder.ofType(Long.class) + .setKey("ldap.authCache.size") + .setDefaultValue(524_288L) + .setDynamic(false) + .build(); + + public static final SystemProperty AUTH_CACHE_MAX_LIFETIME = SystemProperty.Builder.ofType(Duration.class) + .setKey("ldap.authCache.maxLifetime") + .setChronoUnit(ChronoUnit.MILLIS) + .setDefaultValue(Duration.ofHours(2)) + .setDynamic(false) + .build(); + private final LdapManager manager; - private Cache authCache = null; + private final Cache authCache; public LdapAuthProvider() { this(null); - // Convert XML based provider setup to Database based - JiveGlobals.migrateProperty("ldap.authCache.enabled"); } public LdapAuthProvider(final String ldapConfigPropertyName) { Log = LoggerFactory.getLogger(LdapAuthProvider.class.getName() + (ldapConfigPropertyName == null ? "" : ( "[" + ldapConfigPropertyName + "]" ))); manager = LdapManager.getInstance(ldapConfigPropertyName); - if (JiveGlobals.getBooleanProperty("ldap.authCache.enabled", false)) { + if (AUTH_CACHE_ENABLED.getValue()) { String cacheName = "LDAP Authentication" + (ldapConfigPropertyName == null ? "" : ( " (" + ldapConfigPropertyName + ")" ) ); authCache = CacheFactory.createCache(cacheName); + authCache.setMaxCacheSize(AUTH_CACHE_SIZE.getValue()); + authCache.setMaxLifetime(AUTH_CACHE_MAX_LIFETIME.getValue().toMillis()); + } else { + authCache = null; } } @Override public void authenticate(String username, String password) throws UnauthorizedException { - if (username == null || password == null || "".equals(password.trim())) { + if (username == null || password == null || password.trim().isEmpty()) { throw new UnauthorizedException(); } @@ -140,7 +163,7 @@ public void authenticate(String username, String password) throws UnauthorizedEx @Override public String getPassword(String username) throws UserNotFoundException, - UnsupportedOperationException + UnsupportedOperationException { throw new UnsupportedOperationException(); } From 92d586a83bc364e95607dcd507a627585b99d528 Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Thu, 12 Dec 2024 22:56:28 +0100 Subject: [PATCH 07/14] OF-2927: Allow LDAP alternateBaseDN to be configured during setup This exposes a pre-existing LDAP property in the setup wizard. --- .../src/main/webapp/setup/ldap-server.jspf | 40 +++++++++++++++++++ .../setup/setup-admin-settings_test.jsp | 1 - 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/xmppserver/src/main/webapp/setup/ldap-server.jspf b/xmppserver/src/main/webapp/setup/ldap-server.jspf index bf85d2a75f..0304460613 100644 --- a/xmppserver/src/main/webapp/setup/ldap-server.jspf +++ b/xmppserver/src/main/webapp/setup/ldap-server.jspf @@ -17,6 +17,7 @@ String host; int port; LdapName baseDN; + LdapName alternateBaseDN; String adminDN; String adminPassword; boolean connectionPoolEnabled; @@ -64,6 +65,16 @@ errors.put("baseDN", LocaleUtils.getLocalizedString("setup.ldap.server.basedn_error")); baseDN = null; } + if (ParamUtils.getParameter(request, "alternatebasedn") != null) { + try { + alternateBaseDN = new LdapName(ParamUtils.getParameter(request, "alternatebasedn")); + } catch (Exception e) { + errors.put("baseDN", LocaleUtils.getLocalizedString("setup.ldap.server.alternatebasedn_error")); + alternateBaseDN = null; + } + } else { + alternateBaseDN = null; + } adminDN = ParamUtils.getParameter(request, "admindn"); adminPassword = ParamUtils.getParameter(request, "adminpwd"); @@ -81,6 +92,11 @@ settings.put("ldap.host", host); settings.put("ldap.port", Integer.toString(port)); settings.put("ldap.baseDN", baseDN.toString()); + if (alternateBaseDN != null) { + settings.put("ldap.alternateBaseDN", alternateBaseDN.toString()); + } else { + settings.remove("ldap.alternateBaseDN"); + } if (adminDN != null) { settings.put("ldap.adminDN", adminDN); } @@ -108,6 +124,11 @@ manager.setHosts(hosts); manager.setPort(port); manager.setBaseDN(baseDN); + if (alternateBaseDN != null) { + manager.setAlternateBaseDN(alternateBaseDN); + } else { + manager.setAlternateBaseDN(null); + } manager.setAdminDN(adminDN); if ( adminPassword != null ) { // Only store a password if it was changed. manager.setAdminPassword( adminPassword ); @@ -124,6 +145,11 @@ xmppSettings.put("ldap.host", host); xmppSettings.put("ldap.port", Integer.toString(port)); xmppSettings.put("ldap.baseDN", baseDN.toString()); + if (alternateBaseDN != null) { + xmppSettings.put("ldap.alternateBaseDN", alternateBaseDN.toString()); + } else { + xmppSettings.remove("ldap.alternateBaseDN"); + } xmppSettings.put("ldap.adminDN", adminDN); if ( adminPassword != null ) { // Only store a password if it was changed. xmppSettings.put( "ldap.adminPassword", adminPassword ); @@ -167,6 +193,7 @@ } port = manager.getPort(); baseDN = manager.getBaseDN(); + alternateBaseDN = manager.getAlternateBaseDN(); adminDN = manager.getAdminDN(); connectionPoolEnabled = manager.isConnectionPoolEnabled(); sslEnabled = manager.isSslEnabled(); @@ -180,6 +207,7 @@ pageContext.setAttribute("host", host); pageContext.setAttribute("port", port); pageContext.setAttribute("baseDN", baseDN); + pageContext.setAttribute("alternateBaseDN", alternateBaseDN); pageContext.setAttribute("adminDN", adminDN ); // Only show password if it was set in this session (used for testing the password). if ( session.getAttribute("ldapSettings") != null ) { @@ -443,6 +471,18 @@ + + + : + + + + + + + + + diff --git a/xmppserver/src/main/webapp/setup/setup-admin-settings_test.jsp b/xmppserver/src/main/webapp/setup/setup-admin-settings_test.jsp index 5710565db2..179366bf41 100644 --- a/xmppserver/src/main/webapp/setup/setup-admin-settings_test.jsp +++ b/xmppserver/src/main/webapp/setup/setup-admin-settings_test.jsp @@ -19,7 +19,6 @@ <%@ page import="org.jivesoftware.util.ParamUtils, org.jivesoftware.openfire.ldap.LdapManager, org.jivesoftware.openfire.user.UserNotFoundException, org.xmpp.packet.JID" %> <%@ page import="java.util.Map" %> <%@ page import="java.net.URLDecoder" %> -<%@ page import="org.jivesoftware.util.CookieUtils" %> <%@ page import="javax.naming.ldap.Rdn" %> <%@ page import="org.jivesoftware.util.StringUtils" %> From 0a4a5a44be7af0c02245656bdbf3d7bb054e7976 Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Thu, 12 Dec 2024 23:22:43 +0100 Subject: [PATCH 08/14] OF-2928: Improve speed of 'multi' providers Where possible, execute operations on providers in parallel. --- .../openfire/auth/AuthMultiProvider.java | 27 +-- .../openfire/user/UserMultiProvider.java | 155 +++++++----------- .../property/UserPropertyMultiProvider.java | 16 +- 3 files changed, 67 insertions(+), 131 deletions(-) diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/auth/AuthMultiProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/auth/AuthMultiProvider.java index f64524a79f..4f53cbf556 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/auth/AuthMultiProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/auth/AuthMultiProvider.java @@ -118,32 +118,17 @@ public static AuthProvider instantiate(@Nonnull final SystemProperty impl @Override public boolean supportsPasswordRetrieval() { - // TODO Make calls concurrent for improved throughput. - for (final AuthProvider provider : getAuthProviders()) - { - // If at least one provider supports password retrieval, so does this proxy. - if (provider.supportsPasswordRetrieval()) { - return true; - } - } - - return false; + // If at least one provider supports password retrieval, so does this proxy. + return getAuthProviders().parallelStream() + .anyMatch(AuthProvider::supportsPasswordRetrieval); } @Override public boolean isScramSupported() { - // TODO Make calls concurrent for improved throughput. - for (final AuthProvider provider : getAuthProviders()) - { - // If at least one provider supports SCRAM, so does this proxy. - if ( provider.isScramSupported() ) - { - return true; - } - } - - return false; + // If at least one provider supports SCRAM, so does this proxy. + return getAuthProviders().parallelStream() + .anyMatch(AuthProvider::isScramSupported); } @Override diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/user/UserMultiProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/user/UserMultiProvider.java index 661cb1d712..267029b9e2 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/user/UserMultiProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/user/UserMultiProvider.java @@ -16,6 +16,7 @@ package org.jivesoftware.openfire.user; +import org.jivesoftware.openfire.group.GroupProvider; import org.jivesoftware.util.ClassUtils; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.SystemProperty; @@ -26,6 +27,8 @@ import javax.annotation.Nullable; import java.lang.reflect.Constructor; import java.util.*; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; /** * A {@link UserProvider} that delegates to one or more 'backing' UserProvider. @@ -159,40 +162,27 @@ public static UserProvider instantiate(@Nonnull final SystemProperty impl @Override public int getUserCount() { - int total = 0; - // TODO Make calls concurrent for improved throughput. - for ( final UserProvider provider : getUserProviders() ) - { - total += provider.getUserCount(); - } - - return total; + return getUserProviders().parallelStream() + .map(UserProvider::getUserCount) + .reduce(0, Integer::sum); } @Override public Collection getUsers() { - final Collection result = new ArrayList<>(); - for ( final UserProvider provider : getUserProviders() ) - { - // TODO Make calls concurrent for improved throughput. - result.addAll( provider.getUsers() ); - } - - return result; + return getUserProviders().parallelStream() + .map(UserProvider::getUsers) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); } @Override public Collection getUsernames() { - final Collection result = new ArrayList<>(); - for ( final UserProvider provider : getUserProviders() ) - { - // TODO Make calls concurrent for improved throughput. - result.addAll( provider.getUsernames() ); - } - - return result; + return getUserProviders().parallelStream() + .map(UserProvider::getUsernames) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); } @Override @@ -237,32 +227,27 @@ public Collection getUsers( int startIndex, int numResults ) @Override public Collection findUsers( Set fields, String query ) throws UnsupportedOperationException { - final List userList = new ArrayList<>(); - int supportSearch = getUserProviders().size(); - - // TODO Make calls concurrent for improved throughput. - for ( final UserProvider provider : getUserProviders() ) - { - try - { - // Use only those fields that are supported by the provider. - final Set supportedFields = new HashSet<>( fields ); - supportedFields.retainAll( provider.getSearchFields() ); - - userList.addAll( provider.findUsers( supportedFields, query ) ); - } - catch ( UnsupportedOperationException uoe ) - { - Log.warn( "UserProvider.findUsers is not supported by this UserProvider: {}. Its users are not returned as part of search queries.", provider.getClass().getName() ); - supportSearch--; - } - } + final AtomicLong supportSearch = new AtomicLong(getUserProviders().size()); + final Set result = getUserProviders().parallelStream() + .map(provider -> { + try { + // Use only those fields that are supported by the provider. + final Set supportedFields = new HashSet<>(fields); + supportedFields.retainAll(provider.getSearchFields()); + return provider.findUsers(supportedFields, query); + } catch (UnsupportedOperationException uoe) { + Log.warn("UserProvider.findUsers is not supported by this UserProvider: {}. Its users are not returned as part of search queries.", provider.getClass().getName()); + supportSearch.decrementAndGet(); + return new HashSet(); + } + }) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); - if ( supportSearch == 0 ) - { - throw new UnsupportedOperationException( "None of the backing providers support this operation." ); + if (supportSearch.longValue() == 0) { + throw new UnsupportedOperationException("None of the backing providers support this operation."); } - return userList; + return result; } /** @@ -349,24 +334,20 @@ public Collection findUsers( Set fields, String query, int startIn @Override public Set getSearchFields() throws UnsupportedOperationException { - int supportSearch = getUserProviders().size(); - final Set result = new HashSet<>(); - - // TODO Make calls concurrent for improved throughput. - for ( final UserProvider provider : getUserProviders() ) - { - try - { - result.addAll( provider.getSearchFields() ); - } - catch ( UnsupportedOperationException uoe ) - { - Log.warn( "getSearchFields is not supported by this UserProvider: " + provider.getClass().getName() ); - supportSearch--; - } - } + final AtomicLong supportSearch = new AtomicLong(getUserProviders().size()); + final Set result = getUserProviders().parallelStream() + .map(userProvider -> { + try { + return userProvider.getSearchFields(); + } catch ( UnsupportedOperationException uoe ) { + supportSearch.decrementAndGet(); + return new HashSet(); + } + }) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); - if ( supportSearch == 0 ) + if (supportSearch.longValue() == 0) { throw new UnsupportedOperationException( "None of the backing providers support this operation." ); } @@ -382,17 +363,9 @@ public Set getSearchFields() throws UnsupportedOperationException @Override public boolean isReadOnly() { - // TODO Make calls concurrent for improved throughput. - for ( final UserProvider provider : getUserProviders() ) - { - // If at least one provider is not readonly, neither is this proxy. - if ( !provider.isReadOnly() ) - { - return false; - } - } - - return true; + // If at least one provider is not readonly, neither is this proxy. + return getUserProviders().parallelStream() + .allMatch(UserProvider::isReadOnly); } /** @@ -404,39 +377,23 @@ public boolean isReadOnly() @Override public boolean isNameRequired() { - // TODO Make calls concurrent for improved throughput. - for ( final UserProvider provider : getUserProviders() ) - { - // If at least one provider does not require a name, neither is this proxy. - if ( !provider.isNameRequired() ) - { - return false; - } - } - - return true; + // If at least one provider does not require a name, neither is this proxy. + return getUserProviders().parallelStream() + .anyMatch(UserProvider::isNameRequired); } /** * Returns whether all backing providers require an email address to be set on User objects. If at least - * one proivder does not, this method returns false. + * one provider does not, this method returns false. * * @return true when all backing providers require an email address to be set on User objects, otherwise false. */ @Override public boolean isEmailRequired() { - // TODO Make calls concurrent for improved throughput. - for ( final UserProvider provider : getUserProviders() ) - { - // If at least one provider does not require an email, neither is this proxy. - if ( !provider.isEmailRequired() ) - { - return false; - } - } - - return true; + // If at least one provider does not require an email, neither is this proxy. + return getUserProviders().parallelStream() + .anyMatch(UserProvider::isEmailRequired); } @Override diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/UserPropertyMultiProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/UserPropertyMultiProvider.java index f0837058c0..c14008c33f 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/UserPropertyMultiProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/UserPropertyMultiProvider.java @@ -15,6 +15,8 @@ */ package org.jivesoftware.openfire.user.property; +import org.jivesoftware.openfire.auth.AuthProvider; +import org.jivesoftware.openfire.group.GroupProvider; import org.jivesoftware.openfire.user.UserNotFoundException; import org.jivesoftware.util.ClassUtils; import org.jivesoftware.util.JiveGlobals; @@ -166,17 +168,9 @@ public static UserPropertyProvider instantiate(@Nonnull final SystemProperty Date: Thu, 12 Dec 2024 23:33:51 +0100 Subject: [PATCH 09/14] Reduce LDAP group log verbosity --- .../jivesoftware/openfire/ldap/LdapGroupProvider.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapGroupProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapGroupProvider.java index 8f1e3ee83e..8ff8f2ec9e 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapGroupProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/ldap/LdapGroupProvider.java @@ -94,7 +94,7 @@ public Group getGroup(String groupName) throws GroupNotFoundException { return getGroupByDN(groupDN, new HashSet<>(Collections.singleton(groupDN.toString()))); } catch (Exception e) { - Log.error("Unable to load group: {}", groupName, e); + Log.debug("Unable to load group: {}", groupName, e); throw new GroupNotFoundException("Group with name " + groupName + " not found.", e); } } @@ -256,7 +256,7 @@ private Group getGroupByDN(LdapName groupDN, Set membersToIgnore) throws } catch (Exception e) { - Log.debug("error while reading the ldap group with range retrival",e); // no next found, cause of missing attribute + Log.debug("error while reading the ldap group with range retrieval",e); // no next found, cause of missing attribute break; } }while (rangehigh!=-1); //The last part was received @@ -331,7 +331,7 @@ public Collection getGroupNames(JID user) { username = relativePart + "," + manager.getUsersBaseDN(username); } catch (Exception e) { - Log.error("Could not find user in LDAP " + username); + Log.debug("Unable to find groups for a user that was not recognized in LDAP: {}", username); return Collections.emptyList(); } } @@ -339,7 +339,7 @@ public Collection getGroupNames(JID user) { username = server.isLocal(user) ? JID.unescapeNode(user.getNode()) : user.toBareJID(); } // Do nothing if the user is empty or null - if (username == null || "".equals(username)) { + if (username == null || username.isEmpty()) { return Collections.emptyList(); } @@ -580,8 +580,7 @@ private Group processGroup(LdapContext ctx, Attributes a, Set membersToI } } catch (Exception e) { - // TODO: A NPE is occuring here - Log.error(e.getMessage(), e); + Log.error("An unexpected exception occurred while processing an LDAP group {}, while iterating over user {}", name, username, e); } } // A search filter may have been defined in the LdapUserProvider. From c0b1962c1af8cdb1f6c3e3123e799c55beeb282a Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Fri, 13 Dec 2024 17:21:45 +0100 Subject: [PATCH 10/14] OF-2924: Unit tests for 'multi' providers This adds unit tests for most of the 'Hybrid' and 'Mapped' providers for User, UserProperty, Auth and Group. One bigger issue introduced by the recent (as of yet unreleased) refactoring was identified through these tests, and was fixed in `HybridUserPropertyProvider` Various smaller issues (mostly with throwing one type of exception while another was expected) have also been addressed by this. --- .../openfire/auth/AuthProvider.java | 2 +- .../openfire/auth/HybridAuthProvider.java | 32 ++- .../openfire/group/HybridGroupProvider.java | 9 +- .../openfire/user/HybridUserProvider.java | 6 +- .../property/HybridUserPropertyProvider.java | 7 +- .../property/UserPropertyMultiProvider.java | 2 - .../openfire/auth/HybridAuthProviderTest.java | 102 ++++++++ .../openfire/auth/MappedAuthProviderTest.java | 95 +++++++ .../openfire/auth/TestAuthProvider.java | 121 +++++++++ .../openfire/auth/TestAuthProviderMapper.java | 52 ++++ .../group/HybridGroupProviderTest.java | 184 ++++++++++++++ .../group/MappedGroupProviderTest.java | 168 +++++++++++++ .../openfire/group/TestGroupProvider.java | 234 ++++++++++++++++++ .../group/TestGroupProviderMapper.java | 52 ++++ .../openfire/user/HybridUserProviderTest.java | 182 ++++++++++++++ .../openfire/user/MappedUserProviderTest.java | 166 +++++++++++++ .../openfire/user/TestUserProvider.java | 173 +++++++++++++ .../openfire/user/TestUserProviderMapper.java | 52 ++++ .../HybridUserPropertyProviderTest.java | 167 +++++++++++++ .../MappedUserPropertyProviderTest.java | 153 ++++++++++++ .../property/TestUserPropertyProvider.java | 111 +++++++++ .../TestUserPropertyProviderMapper.java | 52 ++++ 22 files changed, 2101 insertions(+), 21 deletions(-) create mode 100644 xmppserver/src/test/java/org/jivesoftware/openfire/auth/HybridAuthProviderTest.java create mode 100644 xmppserver/src/test/java/org/jivesoftware/openfire/auth/MappedAuthProviderTest.java create mode 100644 xmppserver/src/test/java/org/jivesoftware/openfire/auth/TestAuthProvider.java create mode 100644 xmppserver/src/test/java/org/jivesoftware/openfire/auth/TestAuthProviderMapper.java create mode 100644 xmppserver/src/test/java/org/jivesoftware/openfire/group/HybridGroupProviderTest.java create mode 100644 xmppserver/src/test/java/org/jivesoftware/openfire/group/MappedGroupProviderTest.java create mode 100644 xmppserver/src/test/java/org/jivesoftware/openfire/group/TestGroupProvider.java create mode 100644 xmppserver/src/test/java/org/jivesoftware/openfire/group/TestGroupProviderMapper.java create mode 100644 xmppserver/src/test/java/org/jivesoftware/openfire/user/HybridUserProviderTest.java create mode 100644 xmppserver/src/test/java/org/jivesoftware/openfire/user/MappedUserProviderTest.java create mode 100644 xmppserver/src/test/java/org/jivesoftware/openfire/user/TestUserProvider.java create mode 100644 xmppserver/src/test/java/org/jivesoftware/openfire/user/TestUserProviderMapper.java create mode 100644 xmppserver/src/test/java/org/jivesoftware/openfire/user/property/HybridUserPropertyProviderTest.java create mode 100644 xmppserver/src/test/java/org/jivesoftware/openfire/user/property/MappedUserPropertyProviderTest.java create mode 100644 xmppserver/src/test/java/org/jivesoftware/openfire/user/property/TestUserPropertyProvider.java create mode 100644 xmppserver/src/test/java/org/jivesoftware/openfire/user/property/TestUserPropertyProviderMapper.java diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/auth/AuthProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/auth/AuthProvider.java index dfaff4d0a6..b8f948f393 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/auth/AuthProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/auth/AuthProvider.java @@ -63,7 +63,7 @@ String getPassword( String username ) throws UserNotFoundException, UnsupportedOperationException; /** - * Sets the users's password. This method should throw an UnsupportedOperationException + * Sets the user's password. This method should throw an UnsupportedOperationException * if this operation is not supported by the backend user store. * * @param username the username of the user. diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/auth/HybridAuthProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/auth/HybridAuthProvider.java index 95560630f6..fbc4bbcad2 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/auth/HybridAuthProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/auth/HybridAuthProvider.java @@ -236,6 +236,10 @@ public void authenticate(String username, String password) throws UnauthorizedEx public String getPassword(String username) throws UserNotFoundException, UnsupportedOperationException { + if (!supportsPasswordRetrieval()) { + throw new UnsupportedOperationException(); + } + // Check overrides first. try { return super.getPassword(username); @@ -256,7 +260,7 @@ public String getPassword(String username) Log.trace("Could find user {} with auth provider {}. Will try remaining providers (if any)", username, provider.getClass().getName(), e); } } - throw new UnsupportedOperationException(); + throw new UserNotFoundException(); } @Override @@ -282,12 +286,16 @@ public void setPassword(String username, String password) Log.trace("Could set password for user {} with auth provider {}. Will try remaining providers (if any)", username, provider.getClass().getName(), e); } } - throw new UnsupportedOperationException(); + throw new UserNotFoundException(); } @Override public String getSalt(String username) throws UnsupportedOperationException, UserNotFoundException { + if (!isScramSupported()) { + throw new UnsupportedOperationException(); + } + // Check overrides first. try { return super.getSalt(username); @@ -306,12 +314,16 @@ public String getSalt(String username) throws UnsupportedOperationException, Use Log.trace("Could get salt for user {} with auth provider {}. Will try remaining providers (if any)", username, provider.getClass().getName(), e); } } - throw new UnsupportedOperationException(); + throw new UserNotFoundException(); } @Override public int getIterations(String username) throws UnsupportedOperationException, UserNotFoundException { + if (!isScramSupported()) { + throw new UnsupportedOperationException(); + } + // Check overrides first. try { return super.getIterations(username); @@ -330,12 +342,16 @@ public int getIterations(String username) throws UnsupportedOperationException, Log.trace("Could get iterations for user {} with auth provider {}. Will try remaining providers (if any)", username, provider.getClass().getName(), e); } } - throw new UnsupportedOperationException(); + throw new UserNotFoundException(); } @Override public String getServerKey(String username) throws UnsupportedOperationException, UserNotFoundException { + if (!isScramSupported()) { + throw new UnsupportedOperationException(); + } + // Check overrides first. try { return super.getServerKey(username); @@ -354,12 +370,16 @@ public String getServerKey(String username) throws UnsupportedOperationException Log.trace("Could get serverkey for user {} with auth provider {}. Will try remaining providers (if any)", username, provider.getClass().getName(), e); } } - throw new UnsupportedOperationException(); + throw new UserNotFoundException(); } @Override public String getStoredKey(String username) throws UnsupportedOperationException, UserNotFoundException { + if (!isScramSupported()) { + throw new UnsupportedOperationException(); + } + // Check overrides first. try { return super.getStoredKey(username); @@ -378,7 +398,7 @@ public String getStoredKey(String username) throws UnsupportedOperationException Log.trace("Could get storedkey for user {} with auth provider {}. Will try remaining providers (if any)", username, provider.getClass().getName(), e); } } - throw new UnsupportedOperationException(); + throw new UserNotFoundException(); } boolean isProvider(final Class clazz) { diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/group/HybridGroupProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/group/HybridGroupProvider.java index 5b101937be..c38fcb2686 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/group/HybridGroupProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/group/HybridGroupProvider.java @@ -16,16 +16,13 @@ package org.jivesoftware.openfire.group; import org.jivesoftware.util.JiveGlobals; -import org.jivesoftware.util.PersistableMap; import org.jivesoftware.util.SystemProperty; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.xmpp.packet.JID; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Map; /** * Delegate GroupProvider operations among up to three configurable provider implementation classes. @@ -40,7 +37,7 @@ public class HybridGroupProvider extends GroupMultiProvider { private static final Logger Log = LoggerFactory.getLogger(HybridGroupProvider.class); - private static final SystemProperty PRIMARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + public static final SystemProperty PRIMARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) .setKey("hybridGroupProvider.primaryProvider.className") .setBaseClass(GroupProvider.class) .setDynamic(false) @@ -51,7 +48,7 @@ public class HybridGroupProvider extends GroupMultiProvider .setDynamic(false) .build(); - private static final SystemProperty SECONDARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + public static final SystemProperty SECONDARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) .setKey("hybridGroupProvider.secondaryProvider.className") .setBaseClass(GroupProvider.class) .setDynamic(false) @@ -62,7 +59,7 @@ public class HybridGroupProvider extends GroupMultiProvider .setDynamic(false) .build(); - private static final SystemProperty TERTIARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + public static final SystemProperty TERTIARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) .setKey("hybridGroupProvider.tertiaryProvider.className") .setBaseClass(GroupProvider.class) .setDynamic(false) diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/user/HybridUserProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/user/HybridUserProvider.java index 0df00da22b..02dd6157d4 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/user/HybridUserProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/user/HybridUserProvider.java @@ -39,7 +39,7 @@ public class HybridUserProvider extends UserMultiProvider { private static final Logger Log = LoggerFactory.getLogger( HybridUserProvider.class ); - private static final SystemProperty PRIMARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + public static final SystemProperty PRIMARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) .setKey("hybridUserProvider.primaryProvider.className") .setBaseClass(UserProvider.class) .setDynamic(false) @@ -50,7 +50,7 @@ public class HybridUserProvider extends UserMultiProvider .setDynamic(false) .build(); - private static final SystemProperty SECONDARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + public static final SystemProperty SECONDARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) .setKey("hybridUserProvider.secondaryProvider.className") .setBaseClass(UserProvider.class) .setDynamic(false) @@ -61,7 +61,7 @@ public class HybridUserProvider extends UserMultiProvider .setDynamic(false) .build(); - private static final SystemProperty TERTIARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + public static final SystemProperty TERTIARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) .setKey("hybridUserProvider.tertiaryProvider.className") .setBaseClass(UserProvider.class) .setDynamic(false) diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/HybridUserPropertyProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/HybridUserPropertyProvider.java index 0d69c9c887..d523d2ddca 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/HybridUserPropertyProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/HybridUserPropertyProvider.java @@ -52,7 +52,7 @@ public class HybridUserPropertyProvider extends UserPropertyMultiProvider { private static final Logger Log = LoggerFactory.getLogger( HybridUserPropertyProvider.class ); - private static final SystemProperty PRIMARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + public static final SystemProperty PRIMARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) .setKey("hybridUserPropertyProvider.primaryProvider.className") .setBaseClass(UserPropertyProvider.class) .setDynamic(false) @@ -63,7 +63,7 @@ public class HybridUserPropertyProvider extends UserPropertyMultiProvider .setDynamic(false) .build(); - private static final SystemProperty SECONDARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + public static final SystemProperty SECONDARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) .setKey("hybridUserPropertyProvider.secondaryProvider.className") .setBaseClass(UserPropertyProvider.class) .setDynamic(false) @@ -74,7 +74,7 @@ public class HybridUserPropertyProvider extends UserPropertyMultiProvider .setDynamic(false) .build(); - private static final SystemProperty TERTIARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) + public static final SystemProperty TERTIARY_PROVIDER = SystemProperty.Builder.ofType(Class.class) .setKey("hybridUserPropertyProvider.tertiaryProvider.className") .setBaseClass(UserPropertyProvider.class) .setDynamic(false) @@ -135,6 +135,7 @@ public UserPropertyProvider getUserPropertyProvider(String username) try { provider.loadProperties(username); + return provider; } catch (UserNotFoundException unfe) { diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/UserPropertyMultiProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/UserPropertyMultiProvider.java index c14008c33f..a07ecc01d7 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/UserPropertyMultiProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/user/property/UserPropertyMultiProvider.java @@ -15,8 +15,6 @@ */ package org.jivesoftware.openfire.user.property; -import org.jivesoftware.openfire.auth.AuthProvider; -import org.jivesoftware.openfire.group.GroupProvider; import org.jivesoftware.openfire.user.UserNotFoundException; import org.jivesoftware.util.ClassUtils; import org.jivesoftware.util.JiveGlobals; diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/auth/HybridAuthProviderTest.java b/xmppserver/src/test/java/org/jivesoftware/openfire/auth/HybridAuthProviderTest.java new file mode 100644 index 0000000000..1a16f2ec77 --- /dev/null +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/auth/HybridAuthProviderTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.auth; + +import org.jivesoftware.Fixtures; +import org.jivesoftware.openfire.user.UserNotFoundException; +import org.jivesoftware.util.JiveGlobals; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests that verify the implementation of {@link HybridAuthProvider} + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public class HybridAuthProviderTest +{ + @BeforeAll + public static void setup() throws Exception { + Fixtures.reconfigureOpenfireHome(); + Fixtures.disableDatabasePersistence(); + } + + @BeforeEach + @AfterEach + public void unsetProperties() { + JiveGlobals.deleteProperty(HybridAuthProvider.PRIMARY_PROVIDER.getKey()); + JiveGlobals.deleteProperty(HybridAuthProvider.SECONDARY_PROVIDER.getKey()); + JiveGlobals.deleteProperty(HybridAuthProvider.TERTIARY_PROVIDER.getKey()); + } + + /** + * Verifies that a password from user 'jane' can be retrieved (from the secondary provider) + */ + @Test + public void testGetJanePassword() throws Exception + { + // Setup test fixture. + HybridAuthProvider.PRIMARY_PROVIDER.setValue(TestAuthProvider.NoAuthProvider.class); + HybridAuthProvider.SECONDARY_PROVIDER.setValue(TestAuthProvider.JaneAuthProvider.class); + HybridAuthProvider.TERTIARY_PROVIDER.setValue(TestAuthProvider.JohnAuthProvider.class); + final HybridAuthProvider provider = new HybridAuthProvider(); + + // Execute system under test. + final String result = provider.getPassword("jane"); + + // Verify results. + assertEquals("secret", result); + } + + /** + * Verifies that a password from user 'john' can be retrieved (from the tertiary provider) + */ + @Test + public void testGetJohnPassword() throws Exception + { + // Setup test fixture. + HybridAuthProvider.PRIMARY_PROVIDER.setValue(TestAuthProvider.NoAuthProvider.class); + HybridAuthProvider.SECONDARY_PROVIDER.setValue(TestAuthProvider.JaneAuthProvider.class); + HybridAuthProvider.TERTIARY_PROVIDER.setValue(TestAuthProvider.JohnAuthProvider.class); + final HybridAuthProvider provider = new HybridAuthProvider(); + + // Execute system under test. + final String result = provider.getPassword("john"); + + // Verify results. + assertEquals("secret", result); + } + + /** + * Verifies that an exception is thrown when a password for a non-existing user is being requested. + */ + @Test() + public void testGetNonExistingAuth() throws Exception + { + // Setup test fixture. + HybridAuthProvider.PRIMARY_PROVIDER.setValue(TestAuthProvider.NoAuthProvider.class); + HybridAuthProvider.SECONDARY_PROVIDER.setValue(TestAuthProvider.JaneAuthProvider.class); + HybridAuthProvider.TERTIARY_PROVIDER.setValue(TestAuthProvider.JohnAuthProvider.class); + final HybridAuthProvider provider = new HybridAuthProvider(); + + // Execute system under test & Verify results. + assertThrows(UserNotFoundException.class, () -> provider.getPassword("non-existing-user")); + } +} diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/auth/MappedAuthProviderTest.java b/xmppserver/src/test/java/org/jivesoftware/openfire/auth/MappedAuthProviderTest.java new file mode 100644 index 0000000000..76a2fd7921 --- /dev/null +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/auth/MappedAuthProviderTest.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.auth; + +import org.jivesoftware.Fixtures; +import org.jivesoftware.openfire.user.UserNotFoundException; +import org.jivesoftware.util.JiveGlobals; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Unit tests that verify the implementation of {@link MappedAuthProvider} + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public class MappedAuthProviderTest +{ + @BeforeAll + public static void setup() throws Exception { + Fixtures.reconfigureOpenfireHome(); + Fixtures.disableDatabasePersistence(); + } + + @BeforeEach + @AfterEach + public void unsetProperties() { + JiveGlobals.deleteProperty(MappedAuthProvider.PROPERTY_MAPPER_CLASSNAME); + } + + /** + * Verifies that a password from user 'jane' can be retrieved (from the secondary provider) + */ + @Test + public void testGetJanePassword() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedAuthProvider.PROPERTY_MAPPER_CLASSNAME, TestAuthProviderMapper.class.getName()); + final MappedAuthProvider provider = new MappedAuthProvider(); + + // Execute system under test. + final String result = provider.getPassword("jane"); + + // Verify results. + assertEquals("secret", result); + } + + /** + * Verifies that a password from user 'john' can be retrieved (from the tertiary provider) + */ + @Test + public void testGetJohnPassword() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedAuthProvider.PROPERTY_MAPPER_CLASSNAME, TestAuthProviderMapper.class.getName()); + final MappedAuthProvider provider = new MappedAuthProvider(); + + // Execute system under test. + final String result = provider.getPassword("john"); + + // Verify results. + assertEquals("secret", result); + } + + /** + * Verifies that an exception is thrown when a password for a non-existing user is being requested. + */ + @Test() + public void testGetNonExistingAuth() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedAuthProvider.PROPERTY_MAPPER_CLASSNAME, TestAuthProviderMapper.class.getName()); + final MappedAuthProvider provider = new MappedAuthProvider(); + + // Execute system under test & Verify results. + assertThrows(UserNotFoundException.class, () -> provider.getPassword("non-existing-user")); + } +} diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/auth/TestAuthProvider.java b/xmppserver/src/test/java/org/jivesoftware/openfire/auth/TestAuthProvider.java new file mode 100644 index 0000000000..7ad114a13a --- /dev/null +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/auth/TestAuthProvider.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.auth; + +import org.jivesoftware.openfire.user.UserNotFoundException; + +import java.util.*; + +/** + * A very basic implementation of a AuthProvider, that retains data in-memory. + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public class TestAuthProvider implements AuthProvider +{ + final Map data = new HashMap<>(); + + @Override + public void authenticate(String username, String password) throws UnauthorizedException, ConnectionException, InternalUnauthenticatedException + { + final String storedPassword = data.get(username); + if (storedPassword == null || !storedPassword.equals(password)) { + throw new UnauthorizedException(); + } + } + + @Override + public String getPassword(String username) throws UserNotFoundException, UnsupportedOperationException + { + final String storedPassword = data.get(username); + if (storedPassword == null) { + throw new UserNotFoundException(); + } + return storedPassword; + } + + @Override + public void setPassword(String username, String password) throws UserNotFoundException, UnsupportedOperationException + { + final String storedPassword = data.get(username); + if (storedPassword == null) { + throw new UserNotFoundException(); + } + data.put(username, password); + } + + @Override + public boolean supportsPasswordRetrieval() + { + return true; + } + + @Override + public boolean isScramSupported() + { + return false; + } + + @Override + public String getSalt(String username) throws UnsupportedOperationException, UserNotFoundException + { + throw new UnsupportedOperationException(); + } + + @Override + public int getIterations(String username) throws UnsupportedOperationException, UserNotFoundException + { + throw new UnsupportedOperationException(); + } + + @Override + public String getServerKey(String username) throws UnsupportedOperationException, UserNotFoundException + { + throw new UnsupportedOperationException(); + } + + @Override + public String getStoredKey(String username) throws UnsupportedOperationException, UserNotFoundException + { + throw new UnsupportedOperationException(); + } + + /** + * A provider that holds no data. + */ + public static class NoAuthProvider extends TestAuthProvider + {} + + /** + * A provider that holds one auth for a user named 'jane'. + */ + public static class JaneAuthProvider extends TestAuthProvider + { + public JaneAuthProvider() { + data.put("jane", "secret"); + } + } + + /** + * A provider that holds one auth for a user named 'john'. + */ + public static class JohnAuthProvider extends TestAuthProvider + { + public JohnAuthProvider() { + data.put("john", "secret"); + } + } +} diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/auth/TestAuthProviderMapper.java b/xmppserver/src/test/java/org/jivesoftware/openfire/auth/TestAuthProviderMapper.java new file mode 100644 index 0000000000..bf908331a2 --- /dev/null +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/auth/TestAuthProviderMapper.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.auth; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A mapper that can be used with the TestAuthProvider implementations. + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public class TestAuthProviderMapper implements AuthProviderMapper +{ + private final Map providers = new HashMap<>(); + + public TestAuthProviderMapper() { + providers.put(null, new TestAuthProvider.NoAuthProvider()); + providers.put("jane", new TestAuthProvider.JaneAuthProvider()); + providers.put("john", new TestAuthProvider.JohnAuthProvider()); + } + + @Override + public AuthProvider getAuthProvider(String username) + { + if (providers.containsKey(username)) { + return providers.get(username); + } + return providers.get(null); + } + + @Override + public Set getAuthProviders() + { + return new HashSet<>(providers.values()); + } +} diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/group/HybridGroupProviderTest.java b/xmppserver/src/test/java/org/jivesoftware/openfire/group/HybridGroupProviderTest.java new file mode 100644 index 0000000000..23f5390be8 --- /dev/null +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/group/HybridGroupProviderTest.java @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.group; + +import org.jivesoftware.Fixtures; +import org.jivesoftware.util.JiveGlobals; +import org.jivesoftware.util.cache.CacheFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests that verify the implementation of {@link HybridGroupProvider} + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public class HybridGroupProviderTest +{ + @BeforeAll + public static void setup() throws Exception { + Fixtures.reconfigureOpenfireHome(); + Fixtures.disableDatabasePersistence(); + CacheFactory.initialize(); + } + + @BeforeEach + @AfterEach + public void unsetProperties() { + JiveGlobals.deleteProperty(HybridGroupProvider.PRIMARY_PROVIDER.getKey()); + JiveGlobals.deleteProperty(HybridGroupProvider.SECONDARY_PROVIDER.getKey()); + JiveGlobals.deleteProperty(HybridGroupProvider.TERTIARY_PROVIDER.getKey()); + } + + /** + * Verifies that the group count is based on data from all providers. + */ + @Test + public void testCountsGroupsFromAllProviders() throws Exception + { + // Setup test fixture. + HybridGroupProvider.PRIMARY_PROVIDER.setValue(TestGroupProvider.NoGroupProvider.class); + HybridGroupProvider.SECONDARY_PROVIDER.setValue(TestGroupProvider.LittleEndiansGroupProvider.class); + HybridGroupProvider.TERTIARY_PROVIDER.setValue(TestGroupProvider.BigEndiansGroupProvider.class); + final HybridGroupProvider provider = new HybridGroupProvider(); + + // Execute system under test. + final int result = provider.getGroupCount(); + + // Verify results. + assertEquals(2, result); + } + + /** + * Verifies that groups are returned from all providers. + */ + @Test + public void testGetGroupNamesFromAllProviders() throws Exception + { + // Setup test fixture. + HybridGroupProvider.PRIMARY_PROVIDER.setValue(TestGroupProvider.NoGroupProvider.class); + HybridGroupProvider.SECONDARY_PROVIDER.setValue(TestGroupProvider.LittleEndiansGroupProvider.class); + HybridGroupProvider.TERTIARY_PROVIDER.setValue(TestGroupProvider.BigEndiansGroupProvider.class); + final HybridGroupProvider provider = new HybridGroupProvider(); + + // Execute system under test. + final Collection result = provider.getGroupNames(); + + // Verify results. + assertEquals(2, result.size()); + assertTrue(result.contains("little-endians")); + assertTrue(result.contains("big-endians")); + } + + /** + * Verifies that a group named 'little-endians' is returned (from the secondary provider). + */ + @Test + public void testGetJane() throws Exception + { + // Setup test fixture. + HybridGroupProvider.PRIMARY_PROVIDER.setValue(TestGroupProvider.NoGroupProvider.class); + HybridGroupProvider.SECONDARY_PROVIDER.setValue(TestGroupProvider.LittleEndiansGroupProvider.class); + HybridGroupProvider.TERTIARY_PROVIDER.setValue(TestGroupProvider.BigEndiansGroupProvider.class); + final HybridGroupProvider provider = new HybridGroupProvider(); + + // Execute system under test. + final Group result = provider.getGroup("little-endians"); + + // Verify results. + assertNotNull(result); + assertEquals("little-endians", result.getName()); + } + + /** + * Verifies that a group named 'big-endians' is returned (from the tertiary provider). + */ + @Test + public void testGetJohn() throws Exception + { + // Setup test fixture. + HybridGroupProvider.PRIMARY_PROVIDER.setValue(TestGroupProvider.NoGroupProvider.class); + HybridGroupProvider.SECONDARY_PROVIDER.setValue(TestGroupProvider.LittleEndiansGroupProvider.class); + HybridGroupProvider.TERTIARY_PROVIDER.setValue(TestGroupProvider.BigEndiansGroupProvider.class); + final HybridGroupProvider provider = new HybridGroupProvider(); + + // Execute system under test. + final Group result = provider.getGroup("big-endians"); + + // Verify results. + assertNotNull(result); + assertEquals("big-endians", result.getName()); + } + + /** + * Verifies that an exception is thrown when a non-existing group is being requested. + */ + @Test() + public void testGetNonExistingGroup() throws Exception + { + // Setup test fixture. + HybridGroupProvider.PRIMARY_PROVIDER.setValue(TestGroupProvider.NoGroupProvider.class); + HybridGroupProvider.SECONDARY_PROVIDER.setValue(TestGroupProvider.LittleEndiansGroupProvider.class); + HybridGroupProvider.TERTIARY_PROVIDER.setValue(TestGroupProvider.BigEndiansGroupProvider.class); + final HybridGroupProvider provider = new HybridGroupProvider(); + + // Execute system under test & Verify results. + assertThrows(GroupNotFoundException.class, () -> provider.getGroup("non-existing-group")); + } + + /** + * Verifies a new group can be created. + */ + @Test() + public void testCreateGroup() throws Exception + { + // Setup test fixture. + HybridGroupProvider.PRIMARY_PROVIDER.setValue(TestGroupProvider.NoGroupProvider.class); + HybridGroupProvider.SECONDARY_PROVIDER.setValue(TestGroupProvider.LittleEndiansGroupProvider.class); + HybridGroupProvider.TERTIARY_PROVIDER.setValue(TestGroupProvider.BigEndiansGroupProvider.class); + final HybridGroupProvider provider = new HybridGroupProvider(); + + // Execute system under test. + provider.createGroup("middle-endians"); + final Group result = provider.getGroup("middle-endians"); + + // Verify results + assertNotNull(result); + assertEquals("middle-endians", result.getName()); + } + + /** + * Verifies that an exception is thrown when a new group is being created, using a groupname that is not unique. + */ + @Test() + public void testCreateDuplicateGroup() throws Exception + { + // Setup test fixture. + HybridGroupProvider.PRIMARY_PROVIDER.setValue(TestGroupProvider.NoGroupProvider.class); + HybridGroupProvider.SECONDARY_PROVIDER.setValue(TestGroupProvider.LittleEndiansGroupProvider.class); + HybridGroupProvider.TERTIARY_PROVIDER.setValue(TestGroupProvider.BigEndiansGroupProvider.class); + final HybridGroupProvider provider = new HybridGroupProvider(); + + // Execute system under test & Verify results. + assertThrows(GroupAlreadyExistsException.class, () -> provider.createGroup("little-endians")); + } +} diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/group/MappedGroupProviderTest.java b/xmppserver/src/test/java/org/jivesoftware/openfire/group/MappedGroupProviderTest.java new file mode 100644 index 0000000000..2fb918de60 --- /dev/null +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/group/MappedGroupProviderTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.group; + +import org.jivesoftware.Fixtures; +import org.jivesoftware.util.JiveGlobals; +import org.jivesoftware.util.cache.CacheFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests that verify the implementation of {@link MappedGroupProvider} + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public class MappedGroupProviderTest +{ + @BeforeAll + public static void setup() throws Exception { + Fixtures.reconfigureOpenfireHome(); + Fixtures.disableDatabasePersistence(); + CacheFactory.initialize(); + } + + @BeforeEach + @AfterEach + public void unsetProperties() { + JiveGlobals.deleteProperty(MappedGroupProvider.PROPERTY_MAPPER_CLASSNAME); + } + + /** + * Verifies that the group count is based on data from all providers. + */ + @Test + public void testCountsGroupsFromAllProviders() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedGroupProvider.PROPERTY_MAPPER_CLASSNAME, TestGroupProviderMapper.class.getName()); + final MappedGroupProvider provider = new MappedGroupProvider(); + + // Execute system under test. + final int result = provider.getGroupCount(); + + // Verify results. + assertEquals(2, result); + } + + /** + * Verifies that groups are returned from all providers. + */ + @Test + public void testGetGroupNamesFromAllProviders() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedGroupProvider.PROPERTY_MAPPER_CLASSNAME, TestGroupProviderMapper.class.getName()); + final MappedGroupProvider provider = new MappedGroupProvider(); + + // Execute system under test. + final Collection result = provider.getGroupNames(); + + // Verify results. + assertEquals(2, result.size()); + assertTrue(result.contains("little-endians")); + assertTrue(result.contains("big-endians")); + } + + /** + * Verifies that a group named 'little-endians' is returned. + */ + @Test + public void testGetJane() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedGroupProvider.PROPERTY_MAPPER_CLASSNAME, TestGroupProviderMapper.class.getName()); + final MappedGroupProvider provider = new MappedGroupProvider(); + + // Execute system under test. + final Group result = provider.getGroup("little-endians"); + + // Verify results. + assertNotNull(result); + assertEquals("little-endians", result.getName()); + } + + /** + * Verifies that a group named 'big-endians' is returned. + */ + @Test + public void testGetJohn() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedGroupProvider.PROPERTY_MAPPER_CLASSNAME, TestGroupProviderMapper.class.getName()); + final MappedGroupProvider provider = new MappedGroupProvider(); + + // Execute system under test. + final Group result = provider.getGroup("big-endians"); + + // Verify results. + assertNotNull(result); + assertEquals("big-endians", result.getName()); + } + + /** + * Verifies that an exception is thrown when a non-existing group is being requested. + */ + @Test() + public void testGetNonExistingGroup() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedGroupProvider.PROPERTY_MAPPER_CLASSNAME, TestGroupProviderMapper.class.getName()); + final MappedGroupProvider provider = new MappedGroupProvider(); + + // Execute system under test & Verify results. + assertThrows(GroupNotFoundException.class, () -> provider.getGroup("non-existing-group")); + } + + /** + * Verifies a new group can be created. + */ + @Test() + public void testCreateGroup() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedGroupProvider.PROPERTY_MAPPER_CLASSNAME, TestGroupProviderMapper.class.getName()); + final MappedGroupProvider provider = new MappedGroupProvider(); + + // Execute system under test. + provider.createGroup("middle-endians"); + final Group result = provider.getGroup("middle-endians"); + + // Verify results + assertNotNull(result); + assertEquals("middle-endians", result.getName()); + } + + /** + * Verifies that an exception is thrown when a new group is being created, using a group name that is not unique. + */ + @Test() + public void testCreateDuplicateGroup() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedGroupProvider.PROPERTY_MAPPER_CLASSNAME, TestGroupProviderMapper.class.getName()); + final MappedGroupProvider provider = new MappedGroupProvider(); + + // Execute system under test & Verify results. + assertThrows(GroupAlreadyExistsException.class, () -> provider.createGroup("little-endians")); + } +} diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/group/TestGroupProvider.java b/xmppserver/src/test/java/org/jivesoftware/openfire/group/TestGroupProvider.java new file mode 100644 index 0000000000..1397834013 --- /dev/null +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/group/TestGroupProvider.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.group; + +import org.jivesoftware.openfire.user.TestUserProvider; +import org.jivesoftware.util.PersistableMap; +import org.xmpp.packet.JID; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * A very basic implementation of a GroupProvider, that retains data in-memory. + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public class TestGroupProvider implements GroupProvider +{ + final Map data = new HashMap<>(); + + @Override + public Group createGroup(String name) throws GroupAlreadyExistsException, GroupNameInvalidException + { + if (data.containsKey(name)) { + throw new GroupAlreadyExistsException(); + } + final Group group = new Group(name, "Used in unit tests", Collections.emptySet(), Collections.emptySet()); + data.put(name, group); + return group; + } + + @Override + public void deleteGroup(String name) throws GroupNotFoundException + { + data.remove(name); + } + + @Override + public Group getGroup(String name) throws GroupNotFoundException + { + final Group result = data.get(name); + if (result == null) { + throw new GroupNotFoundException(); + } + return result; + } + + @Override + public void setName(String oldName, String newName) throws GroupAlreadyExistsException, GroupNameInvalidException, GroupNotFoundException + { + if (!data.containsKey(oldName)) { + throw new GroupNotFoundException(); + } + if (data.containsKey(newName)) { + throw new GroupAlreadyExistsException(); + } + data.put(newName, data.remove(oldName)); + } + + @Override + public void setDescription(String name, String description) throws GroupNotFoundException + { + final Group group = data.get(name); + if (group == null) { + throw new GroupNotFoundException(); + } + group.setDescription(description); + } + + @Override + public int getGroupCount() + { + return data.size(); + } + + @Override + public Collection getGroupNames() + { + return data.keySet(); + } + + @Override + public boolean isSharingSupported() + { + return false; + } + + @Override + public Collection getSharedGroupNames() + { + return List.of(); + } + + @Override + public Collection getSharedGroupNames(JID user) + { + return List.of(); + } + + @Override + public Collection getPublicSharedGroupNames() + { + return List.of(); + } + + @Override + public Collection getVisibleGroupNames(String userGroup) + { + return List.of(); + } + + @Override + public Collection getGroupNames(int startIndex, int numResults) + { + return new ArrayList<>(data.keySet()).subList(startIndex, Math.min(numResults, data.size() - startIndex)); + } + + @Override + public Collection getGroupNames(JID user) + { + return data.values().stream() + .filter(group -> group.isUser(user)) + .map(Group::getName) + .collect(Collectors.toSet()); + } + + @Override + public void addMember(String groupName, JID user, boolean administrator) throws GroupNotFoundException + { + final Group group = data.get(groupName); + if (group == null) { + throw new GroupNotFoundException(); + } + (administrator ? group.getAdmins() : group.getMembers()).add(user); + } + + @Override + public void updateMember(String groupName, JID user, boolean administrator) throws GroupNotFoundException + { + final Group group = data.get(groupName); + if (group == null) { + throw new GroupNotFoundException(); + } + (administrator ? group.getAdmins() : group.getMembers()).add(user); + (!administrator ? group.getAdmins() : group.getMembers()).remove(user); + } + + @Override + public void deleteMember(String groupName, JID user) + { + final Group group = data.get(groupName); + if (group != null) { + group.getMembers().remove(user); + group.getAdmins().remove(user); + } + } + + @Override + public boolean isReadOnly() + { + return false; + } + + @Override + public Collection search(String query) + { + return List.of(); + } + + @Override + public Collection search(String query, int startIndex, int numResults) + { + return List.of(); + } + + @Override + public Collection search(String key, String value) + { + return List.of(); + } + + @Override + public boolean isSearchSupported() + { + return false; + } + + @Override + public PersistableMap loadProperties(Group group) + { + return null; + } + + /** + * A provider that holds no data. + */ + public static class NoGroupProvider extends TestGroupProvider + {} + + public static class LittleEndiansGroupProvider extends TestGroupProvider + { + public LittleEndiansGroupProvider() { + try { + createGroup("little-endians"); + } catch (GroupAlreadyExistsException | GroupNameInvalidException e) { + throw new RuntimeException(e); + } + } + } + + public static class BigEndiansGroupProvider extends TestGroupProvider + { + public BigEndiansGroupProvider() { + try { + createGroup("big-endians"); + } catch (GroupAlreadyExistsException | GroupNameInvalidException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/group/TestGroupProviderMapper.java b/xmppserver/src/test/java/org/jivesoftware/openfire/group/TestGroupProviderMapper.java new file mode 100644 index 0000000000..45e10f3d57 --- /dev/null +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/group/TestGroupProviderMapper.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.group; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A mapper that can be used with the TestUserProvider implementations. + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public class TestGroupProviderMapper implements GroupProviderMapper +{ + private final Map providers = new HashMap<>(); + + public TestGroupProviderMapper() { + providers.put(null, new TestGroupProvider.NoGroupProvider()); + providers.put("little-endians", new TestGroupProvider.LittleEndiansGroupProvider()); + providers.put("big-endians", new TestGroupProvider.BigEndiansGroupProvider()); + } + + @Override + public GroupProvider getGroupProvider(String groupname) + { + if (providers.containsKey(groupname)) { + return providers.get(groupname); + } + return providers.get(null); + } + + @Override + public Set getGroupProviders() + { + return new HashSet<>(providers.values()); + } +} diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/user/HybridUserProviderTest.java b/xmppserver/src/test/java/org/jivesoftware/openfire/user/HybridUserProviderTest.java new file mode 100644 index 0000000000..34da0d6cd9 --- /dev/null +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/user/HybridUserProviderTest.java @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.user; + +import org.jivesoftware.Fixtures; +import org.jivesoftware.util.JiveGlobals; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests that verify the implementation of {@link HybridUserProvider} + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public class HybridUserProviderTest +{ + @BeforeAll + public static void setup() throws Exception { + Fixtures.reconfigureOpenfireHome(); + Fixtures.disableDatabasePersistence(); + } + + @BeforeEach + @AfterEach + public void unsetProperties() { + JiveGlobals.deleteProperty(HybridUserProvider.PRIMARY_PROVIDER.getKey()); + JiveGlobals.deleteProperty(HybridUserProvider.SECONDARY_PROVIDER.getKey()); + JiveGlobals.deleteProperty(HybridUserProvider.TERTIARY_PROVIDER.getKey()); + } + + /** + * Verifies that the user count is based on data from all providers. + */ + @Test + public void testCountsUsersFromAllProviders() throws Exception + { + // Setup test fixture. + HybridUserProvider.PRIMARY_PROVIDER.setValue(TestUserProvider.NoUserProvider.class); + HybridUserProvider.SECONDARY_PROVIDER.setValue(TestUserProvider.JaneUserProvider.class); + HybridUserProvider.TERTIARY_PROVIDER.setValue(TestUserProvider.JohnUserProvider.class); + final HybridUserProvider provider = new HybridUserProvider(); + + // Execute system under test. + final int result = provider.getUserCount(); + + // Verify results. + assertEquals(2, result); + } + + /** + * Verifies that users are returned from all providers. + */ + @Test + public void testGetUserNamesFromAllProviders() throws Exception + { + // Setup test fixture. + HybridUserProvider.PRIMARY_PROVIDER.setValue(TestUserProvider.NoUserProvider.class); + HybridUserProvider.SECONDARY_PROVIDER.setValue(TestUserProvider.JaneUserProvider.class); + HybridUserProvider.TERTIARY_PROVIDER.setValue(TestUserProvider.JohnUserProvider.class); + final HybridUserProvider provider = new HybridUserProvider(); + + // Execute system under test. + final Collection result = provider.getUsernames(); + + // Verify results. + assertEquals(2, result.size()); + assertTrue(result.contains("jane")); + assertTrue(result.contains("john")); + } + + /** + * Verifies that a user named 'jane' is returned (from the secondary provider). + */ + @Test + public void testGetJane() throws Exception + { + // Setup test fixture. + HybridUserProvider.PRIMARY_PROVIDER.setValue(TestUserProvider.NoUserProvider.class); + HybridUserProvider.SECONDARY_PROVIDER.setValue(TestUserProvider.JaneUserProvider.class); + HybridUserProvider.TERTIARY_PROVIDER.setValue(TestUserProvider.JohnUserProvider.class); + final HybridUserProvider provider = new HybridUserProvider(); + + // Execute system under test. + final User result = provider.loadUser("jane"); + + // Verify results. + assertNotNull(result); + assertEquals("jane", result.getUsername()); + } + + /** + * Verifies that a user named 'john' is returned (from the tertiary provider). + */ + @Test + public void testGetJohn() throws Exception + { + // Setup test fixture. + HybridUserProvider.PRIMARY_PROVIDER.setValue(TestUserProvider.NoUserProvider.class); + HybridUserProvider.SECONDARY_PROVIDER.setValue(TestUserProvider.JaneUserProvider.class); + HybridUserProvider.TERTIARY_PROVIDER.setValue(TestUserProvider.JohnUserProvider.class); + final HybridUserProvider provider = new HybridUserProvider(); + + // Execute system under test. + final User result = provider.loadUser("john"); + + // Verify results. + assertNotNull(result); + assertEquals("john", result.getUsername()); + } + + /** + * Verifies that an exception is thrown when a non-existing user is being requested. + */ + @Test() + public void testGetNonExistingUser() throws Exception + { + // Setup test fixture. + HybridUserProvider.PRIMARY_PROVIDER.setValue(TestUserProvider.NoUserProvider.class); + HybridUserProvider.SECONDARY_PROVIDER.setValue(TestUserProvider.JaneUserProvider.class); + HybridUserProvider.TERTIARY_PROVIDER.setValue(TestUserProvider.JohnUserProvider.class); + final HybridUserProvider provider = new HybridUserProvider(); + + // Execute system under test & Verify results. + assertThrows(UserNotFoundException.class, () -> provider.loadUser("non-existing-user")); + } + + /** + * Verifies a new user can be created. + */ + @Test() + public void testCreateUser() throws Exception + { + // Setup test fixture. + HybridUserProvider.PRIMARY_PROVIDER.setValue(TestUserProvider.NoUserProvider.class); + HybridUserProvider.SECONDARY_PROVIDER.setValue(TestUserProvider.JaneUserProvider.class); + HybridUserProvider.TERTIARY_PROVIDER.setValue(TestUserProvider.JohnUserProvider.class); + final HybridUserProvider provider = new HybridUserProvider(); + + // Execute system under test. + provider.createUser("jack", "secret", "Jack Doe", "jack@example.org"); + final User result = provider.loadUser("jack"); + + // Verify results + assertNotNull(result); + assertEquals("jack", result.getUsername()); + } + + /** + * Verifies that an exception is thrown when a new user is being created, using a username that is not unique. + */ + @Test() + public void testCreateDuplicateUser() throws Exception + { + // Setup test fixture. + HybridUserProvider.PRIMARY_PROVIDER.setValue(TestUserProvider.NoUserProvider.class); + HybridUserProvider.SECONDARY_PROVIDER.setValue(TestUserProvider.JaneUserProvider.class); + HybridUserProvider.TERTIARY_PROVIDER.setValue(TestUserProvider.JohnUserProvider.class); + final HybridUserProvider provider = new HybridUserProvider(); + + // Execute system under test & Verify results. + assertThrows(UserAlreadyExistsException.class, () -> provider.createUser("jane", "test", "Jane But Not Doe", "jane@example.com")); + } +} diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/user/MappedUserProviderTest.java b/xmppserver/src/test/java/org/jivesoftware/openfire/user/MappedUserProviderTest.java new file mode 100644 index 0000000000..5a3afe05b6 --- /dev/null +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/user/MappedUserProviderTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.user; + +import org.jivesoftware.Fixtures; +import org.jivesoftware.util.JiveGlobals; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests that verify the implementation of {@link MappedUserProvider} + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public class MappedUserProviderTest +{ + @BeforeAll + public static void setup() throws Exception { + Fixtures.reconfigureOpenfireHome(); + Fixtures.disableDatabasePersistence(); + } + + @BeforeEach + @AfterEach + public void unsetProperties() { + JiveGlobals.deleteProperty(MappedUserProvider.PROPERTY_MAPPER_CLASSNAME); + } + + /** + * Verifies that the user count is based on data from all providers. + */ + @Test + public void testCountsUsersFromAllProviders() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedUserProvider.PROPERTY_MAPPER_CLASSNAME, TestUserProviderMapper.class.getName()); + final MappedUserProvider provider = new MappedUserProvider(); + + // Execute system under test. + final int result = provider.getUserCount(); + + // Verify results. + assertEquals(2, result); + } + + /** + * Verifies that users are returned from all providers. + */ + @Test + public void testGetUserNamesFromAllProviders() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedUserProvider.PROPERTY_MAPPER_CLASSNAME, TestUserProviderMapper.class.getName()); + final MappedUserProvider provider = new MappedUserProvider(); + + // Execute system under test. + final Collection result = provider.getUsernames(); + + // Verify results. + assertEquals(2, result.size()); + assertTrue(result.contains("jane")); + assertTrue(result.contains("john")); + } + + /** + * Verifies that a user named 'jane' is returned. + */ + @Test + public void testGetJane() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedUserProvider.PROPERTY_MAPPER_CLASSNAME, TestUserProviderMapper.class.getName()); + final MappedUserProvider provider = new MappedUserProvider(); + + // Execute system under test. + final User result = provider.loadUser("jane"); + + // Verify results. + assertNotNull(result); + assertEquals("jane", result.getUsername()); + } + + /** + * Verifies that a user named 'john' is returned. + */ + @Test + public void testGetJohn() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedUserProvider.PROPERTY_MAPPER_CLASSNAME, TestUserProviderMapper.class.getName()); + final MappedUserProvider provider = new MappedUserProvider(); + + // Execute system under test. + final User result = provider.loadUser("john"); + + // Verify results. + assertNotNull(result); + assertEquals("john", result.getUsername()); + } + + /** + * Verifies that an exception is thrown when a non-existing user is being requested. + */ + @Test() + public void testGetNonExistingUser() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedUserProvider.PROPERTY_MAPPER_CLASSNAME, TestUserProviderMapper.class.getName()); + final MappedUserProvider provider = new MappedUserProvider(); + + // Execute system under test & Verify results. + assertThrows(UserNotFoundException.class, () -> provider.loadUser("non-existing-user")); + } + + /** + * Verifies a new user can be created. + */ + @Test() + public void testCreateUser() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedUserProvider.PROPERTY_MAPPER_CLASSNAME, TestUserProviderMapper.class.getName()); + final MappedUserProvider provider = new MappedUserProvider(); + + // Execute system under test. + provider.createUser("jack", "secret", "Jack Doe", "jack@example.org"); + final User result = provider.loadUser("jack"); + + // Verify results + assertNotNull(result); + assertEquals("jack", result.getUsername()); + } + + /** + * Verifies that an exception is thrown when a new user is being created, using a username that is not unique. + */ + @Test() + public void testCreateDuplicateUser() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedUserProvider.PROPERTY_MAPPER_CLASSNAME, TestUserProviderMapper.class.getName()); + final MappedUserProvider provider = new MappedUserProvider(); + + // Execute system under test & Verify results. + assertThrows(UserAlreadyExistsException.class, () -> provider.createUser("jane", "test", "Jane But Not Doe", "jane@example.com")); + } +} diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/user/TestUserProvider.java b/xmppserver/src/test/java/org/jivesoftware/openfire/user/TestUserProvider.java new file mode 100644 index 0000000000..6850d4d78b --- /dev/null +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/user/TestUserProvider.java @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.user; + +import java.util.*; + +/** + * A very basic implementation of a UserProvider, that retains data in-memory. + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public class TestUserProvider implements UserProvider +{ + final Map data = new HashMap<>(); + + @Override + public User loadUser(String username) throws UserNotFoundException + { + final User result = data.get(username); + if (result == null) { + throw new UserNotFoundException(); + } + return result; + } + + @Override + public User createUser(String username, String password, String name, String email) throws UserAlreadyExistsException + { + if (data.containsKey(username)) { + throw new UserAlreadyExistsException(); + } + final User user = new User(username, name, email, new Date(), new Date()); + data.put(username, user); + return user; + } + + @Override + public void deleteUser(String username) + { + data.remove(username); + } + + @Override + public int getUserCount() + { + return data.size(); + } + + @Override + public Collection getUsers() + { + return data.values(); + } + + @Override + public Collection getUsernames() + { + return data.keySet(); + } + + @Override + public Collection getUsers(int startIndex, int numResults) + { + return new ArrayList<>(data.values()).subList(startIndex, Math.min(numResults, data.size() - startIndex)); + } + + @Override + public void setName(String username, String name) throws UserNotFoundException + { + loadUser(username).setName(name); + } + + @Override + public void setEmail(String username, String email) throws UserNotFoundException + { + loadUser(username).setEmail(email); + } + + @Override + public void setCreationDate(String username, Date creationDate) throws UserNotFoundException + { + loadUser(username).setCreationDate(creationDate); + } + + @Override + public void setModificationDate(String username, Date modificationDate) throws UserNotFoundException + { + loadUser(username).setModificationDate(modificationDate); + } + + @Override + public Set getSearchFields() throws UnsupportedOperationException + { + throw new UnsupportedOperationException(); + } + + @Override + public Collection findUsers(Set fields, String query) throws UnsupportedOperationException + { + throw new UnsupportedOperationException(); + } + + @Override + public Collection findUsers(Set fields, String query, int startIndex, int numResults) throws UnsupportedOperationException + { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isReadOnly() + { + return false; + } + + @Override + public boolean isNameRequired() + { + return false; + } + + @Override + public boolean isEmailRequired() + { + return false; + } + + /** + * A provider that holds no data. + */ + public static class NoUserProvider extends TestUserProvider + {} + + /** + * A provider that holds one user named 'jane'. + */ + public static class JaneUserProvider extends TestUserProvider + { + public JaneUserProvider() { + try { + createUser("jane", "secret", "Jane Doe", "jane@example.org"); + } catch (UserAlreadyExistsException e) { + throw new RuntimeException(e); + } + } + } + + /** + * A provider that holds one user named 'john'. + */ + public static class JohnUserProvider extends TestUserProvider + { + public JohnUserProvider() { + try { + createUser("john", "secret", "John Doe", "john@example.org"); + } catch (UserAlreadyExistsException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/user/TestUserProviderMapper.java b/xmppserver/src/test/java/org/jivesoftware/openfire/user/TestUserProviderMapper.java new file mode 100644 index 0000000000..81fec6cbab --- /dev/null +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/user/TestUserProviderMapper.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.user; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A mapper that can be used with the TestUserProvider implementations. + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public class TestUserProviderMapper implements UserProviderMapper +{ + private final Map providers = new HashMap<>(); + + public TestUserProviderMapper() { + providers.put(null, new TestUserProvider.NoUserProvider()); + providers.put("jane", new TestUserProvider.JaneUserProvider()); + providers.put("john", new TestUserProvider.JohnUserProvider()); + } + + @Override + public UserProvider getUserProvider(String username) + { + if (providers.containsKey(username)) { + return providers.get(username); + } + return providers.get(null); + } + + @Override + public Set getUserProviders() + { + return new HashSet<>(providers.values()); + } +} diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/user/property/HybridUserPropertyProviderTest.java b/xmppserver/src/test/java/org/jivesoftware/openfire/user/property/HybridUserPropertyProviderTest.java new file mode 100644 index 0000000000..d53a47b32a --- /dev/null +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/user/property/HybridUserPropertyProviderTest.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.user.property; + +import org.jivesoftware.Fixtures; +import org.jivesoftware.openfire.user.*; +import org.jivesoftware.util.JiveGlobals; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests that verify the implementation of {@link HybridUserPropertyProvider} + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public class HybridUserPropertyProviderTest +{ + @BeforeAll + public static void setup() throws Exception { + Fixtures.reconfigureOpenfireHome(); + Fixtures.disableDatabasePersistence(); + } + + @BeforeEach + @AfterEach + public void unsetProperties() { + JiveGlobals.deleteProperty(HybridUserPropertyProvider.PRIMARY_PROVIDER.getKey()); + JiveGlobals.deleteProperty(HybridUserPropertyProvider.SECONDARY_PROVIDER.getKey()); + JiveGlobals.deleteProperty(HybridUserPropertyProvider.TERTIARY_PROVIDER.getKey()); + } + + /** + * Verifies that properties from a user named 'jane' is returned (from the secondary provider). + */ + @Test + public void testGetJane() throws Exception + { + // Setup test fixture. + HybridUserPropertyProvider.PRIMARY_PROVIDER.setValue(TestUserPropertyProvider.NoUserProvider.class); + HybridUserPropertyProvider.SECONDARY_PROVIDER.setValue(TestUserPropertyProvider.JaneUserProvider.class); + HybridUserPropertyProvider.TERTIARY_PROVIDER.setValue(TestUserPropertyProvider.JohnUserProvider.class); + final HybridUserPropertyProvider provider = new HybridUserPropertyProvider(); + + // Execute system under test. + final Map result = provider.loadProperties("jane"); + + // Verify results. + assertNotNull(result); + } + + /** + * Verifies that properties from a user named 'john' is returned (from the tertiary provider). + */ + @Test + public void testGetJohn() throws Exception + { + // Setup test fixture. + HybridUserPropertyProvider.PRIMARY_PROVIDER.setValue(TestUserPropertyProvider.NoUserProvider.class); + HybridUserPropertyProvider.SECONDARY_PROVIDER.setValue(TestUserPropertyProvider.JaneUserProvider.class); + HybridUserPropertyProvider.TERTIARY_PROVIDER.setValue(TestUserPropertyProvider.JohnUserProvider.class); + final HybridUserPropertyProvider provider = new HybridUserPropertyProvider(); + + // Execute system under test. + final Map result = provider.loadProperties("john"); + + // Verify results. + assertNotNull(result); + + } + + /** + * Verifies that an exception is thrown when properties for non-existing user are being requested. + */ + @Test() + public void testGetNonExistingUser() throws Exception + { + // Setup test fixture. + HybridUserPropertyProvider.PRIMARY_PROVIDER.setValue(TestUserPropertyProvider.NoUserProvider.class); + HybridUserPropertyProvider.SECONDARY_PROVIDER.setValue(TestUserPropertyProvider.JaneUserProvider.class); + HybridUserPropertyProvider.TERTIARY_PROVIDER.setValue(TestUserPropertyProvider.JohnUserProvider.class); + final HybridUserPropertyProvider provider = new HybridUserPropertyProvider(); + + // Execute system under test & Verify results. + assertThrows(UserNotFoundException.class, () -> provider.loadProperties("non-existing-user")); + } + + /** + * Verifies a new property can be created. + */ + @Test() + public void testCreateProperty() throws Exception + { + // Setup test fixture. + HybridUserPropertyProvider.PRIMARY_PROVIDER.setValue(TestUserPropertyProvider.NoUserProvider.class); + HybridUserPropertyProvider.SECONDARY_PROVIDER.setValue(TestUserPropertyProvider.JaneUserProvider.class); + HybridUserPropertyProvider.TERTIARY_PROVIDER.setValue(TestUserPropertyProvider.JohnUserProvider.class); + final HybridUserPropertyProvider provider = new HybridUserPropertyProvider(); + + // Execute system under test. + provider.insertProperty("jane", "new property", "new property value"); + final String result = provider.loadProperty("jane", "new property"); + + // Verify results + assertEquals("new property value", result); + } + + /** + * Verifies a property can be deleted. + */ + @Test() + public void testDeleteProperty() throws Exception + { + // Setup test fixture. + HybridUserPropertyProvider.PRIMARY_PROVIDER.setValue(TestUserPropertyProvider.NoUserProvider.class); + HybridUserPropertyProvider.SECONDARY_PROVIDER.setValue(TestUserPropertyProvider.JaneUserProvider.class); + HybridUserPropertyProvider.TERTIARY_PROVIDER.setValue(TestUserPropertyProvider.JohnUserProvider.class); + final HybridUserPropertyProvider provider = new HybridUserPropertyProvider(); + provider.insertProperty("jane", "to delete property", "delete me"); + + // Execute system under test. + provider.deleteProperty("jane", "to delete property"); + + // Verify results + final String result = provider.loadProperty("jane", "new property"); + assertNull(result); + } + + /** + * Verifies a property can be deleted. + */ + @Test() + public void testUpdateProperty() throws Exception + { + // Setup test fixture. + HybridUserPropertyProvider.PRIMARY_PROVIDER.setValue(TestUserPropertyProvider.NoUserProvider.class); + HybridUserPropertyProvider.SECONDARY_PROVIDER.setValue(TestUserPropertyProvider.JaneUserProvider.class); + HybridUserPropertyProvider.TERTIARY_PROVIDER.setValue(TestUserPropertyProvider.JohnUserProvider.class); + final HybridUserPropertyProvider provider = new HybridUserPropertyProvider(); + provider.insertProperty("jane", "to update property", "update me"); + + // Execute system under test. + provider.updateProperty("jane", "to update property", "updated"); + + // Verify results + final String result = provider.loadProperty("jane", "to update property"); + assertEquals("updated", result); + } +} diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/user/property/MappedUserPropertyProviderTest.java b/xmppserver/src/test/java/org/jivesoftware/openfire/user/property/MappedUserPropertyProviderTest.java new file mode 100644 index 0000000000..2dc689eaf4 --- /dev/null +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/user/property/MappedUserPropertyProviderTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.user.property; + +import org.jivesoftware.Fixtures; +import org.jivesoftware.openfire.user.UserNotFoundException; +import org.jivesoftware.util.JiveGlobals; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests that verify the implementation of {@link MappedUserPropertyProvider} + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public class MappedUserPropertyProviderTest +{ + @BeforeAll + public static void setup() throws Exception { + Fixtures.reconfigureOpenfireHome(); + Fixtures.disableDatabasePersistence(); + } + + @BeforeEach + @AfterEach + public void unsetProperties() { + JiveGlobals.deleteProperty(MappedUserPropertyProvider.PROPERTY_MAPPER_CLASSNAME); + } + + /** + * Verifies that properties from a user named 'jane' is returned (from the secondary provider). + */ + @Test + public void testGetJane() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedUserPropertyProvider.PROPERTY_MAPPER_CLASSNAME, TestUserPropertyProviderMapper.class.getName()); + final MappedUserPropertyProvider provider = new MappedUserPropertyProvider(); + + // Execute system under test. + final Map result = provider.loadProperties("jane"); + + // Verify results. + assertNotNull(result); + } + + /** + * Verifies that properties from a user named 'john' is returned (from the tertiary provider). + */ + @Test + public void testGetJohn() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedUserPropertyProvider.PROPERTY_MAPPER_CLASSNAME, TestUserPropertyProviderMapper.class.getName()); + final MappedUserPropertyProvider provider = new MappedUserPropertyProvider(); + + // Execute system under test. + final Map result = provider.loadProperties("john"); + + // Verify results. + assertNotNull(result); + + } + + /** + * Verifies that an exception is thrown when properties for non-existing user are being requested. + */ + @Test() + public void testGetNonExistingUser() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedUserPropertyProvider.PROPERTY_MAPPER_CLASSNAME, TestUserPropertyProviderMapper.class.getName()); + final MappedUserPropertyProvider provider = new MappedUserPropertyProvider(); + + // Execute system under test & Verify results. + assertThrows(UserNotFoundException.class, () -> provider.loadProperties("non-existing-user")); + } + + /** + * Verifies a new property can be created. + */ + @Test() + public void testCreateProperty() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedUserPropertyProvider.PROPERTY_MAPPER_CLASSNAME, TestUserPropertyProviderMapper.class.getName()); + final MappedUserPropertyProvider provider = new MappedUserPropertyProvider(); + + // Execute system under test. + provider.insertProperty("jane", "new property", "new property value"); + final String result = provider.loadProperty("jane", "new property"); + + // Verify results + assertEquals("new property value", result); + } + + /** + * Verifies a property can be deleted. + */ + @Test() + public void testDeleteProperty() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedUserPropertyProvider.PROPERTY_MAPPER_CLASSNAME, TestUserPropertyProviderMapper.class.getName()); + final MappedUserPropertyProvider provider = new MappedUserPropertyProvider(); + provider.insertProperty("jane", "to delete property", "delete me"); + + // Execute system under test. + provider.deleteProperty("jane", "to delete property"); + + // Verify results + final String result = provider.loadProperty("jane", "new property"); + assertNull(result); + } + + /** + * Verifies a property can be deleted. + */ + @Test() + public void testUpdateProperty() throws Exception + { + // Setup test fixture. + JiveGlobals.setProperty(MappedUserPropertyProvider.PROPERTY_MAPPER_CLASSNAME, TestUserPropertyProviderMapper.class.getName()); + final MappedUserPropertyProvider provider = new MappedUserPropertyProvider(); + provider.insertProperty("jane", "to update property", "update me"); + + // Execute system under test. + provider.updateProperty("jane", "to update property", "updated"); + + // Verify results + final String result = provider.loadProperty("jane", "to update property"); + assertEquals("updated", result); + } +} diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/user/property/TestUserPropertyProvider.java b/xmppserver/src/test/java/org/jivesoftware/openfire/user/property/TestUserPropertyProvider.java new file mode 100644 index 0000000000..07d1dd7f0d --- /dev/null +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/user/property/TestUserPropertyProvider.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.user.property; + +import org.jivesoftware.openfire.user.UserNotFoundException; + +import java.util.*; + +/** + * A very basic implementation of a UserPropertyProvider, that retains data in-memory. + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public class TestUserPropertyProvider implements UserPropertyProvider +{ + final Map> data = new HashMap<>(); + + @Override + public boolean isReadOnly() + { + return false; + } + + @Override + public Map loadProperties(String username) throws UserNotFoundException + { + if (!data.containsKey(username)) { + throw new UserNotFoundException(); + } + return data.get(username); + } + + @Override + public String loadProperty(String username, String propName) throws UserNotFoundException + { + if (!data.containsKey(username)) { + throw new UserNotFoundException(); + } + return data.get(username).get(propName); + } + + @Override + public void insertProperty(String username, String propName, String propValue) throws UserNotFoundException, UnsupportedOperationException + { + if (!data.containsKey(username)) { + throw new UserNotFoundException(); + } + data.get(username).put(propName, propValue); + } + + @Override + public void updateProperty(String username, String propName, String propValue) throws UserNotFoundException, UnsupportedOperationException + { + if (!data.containsKey(username)) { + throw new UserNotFoundException(); + } + data.get(username).put(propName, propValue); + } + + @Override + public void deleteProperty(String username, String propName) throws UserNotFoundException, UnsupportedOperationException + { + if (!data.containsKey(username)) { + throw new UserNotFoundException(); + } + data.get(username).remove(propName); + } + + /** + * A provider that holds no data. + */ + public static class NoUserProvider extends TestUserPropertyProvider + {} + + /** + * A provider that holds data for one user, named 'jane'. + */ + public static class JaneUserProvider extends TestUserPropertyProvider + { + public JaneUserProvider() { + final Map userData = new HashMap<>(); + userData.put("testprop", "test jane value"); + data.put("jane", userData); + } + } + + /** + * A provider that holds data for one user, named 'john'. + */ + public static class JohnUserProvider extends TestUserPropertyProvider + { + public JohnUserProvider() { + final Map userData = new HashMap<>(); + userData.put("testprop", "test john value"); + data.put("john", userData); + } + } +} diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/user/property/TestUserPropertyProviderMapper.java b/xmppserver/src/test/java/org/jivesoftware/openfire/user/property/TestUserPropertyProviderMapper.java new file mode 100644 index 0000000000..cf61e72d8b --- /dev/null +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/user/property/TestUserPropertyProviderMapper.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 Ignite Realtime Foundation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.jivesoftware.openfire.user.property; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A mapper that can be used with the TestUserPropertyProvider implementations. + * + * @author Guus der Kinderen, guus@goodbytes.nl + */ +public class TestUserPropertyProviderMapper implements UserPropertyProviderMapper +{ + private final Map providers = new HashMap<>(); + + public TestUserPropertyProviderMapper() { + providers.put(null, new TestUserPropertyProvider.NoUserProvider()); + providers.put("jane", new TestUserPropertyProvider.JaneUserProvider()); + providers.put("john", new TestUserPropertyProvider.JohnUserProvider()); + } + + @Override + public UserPropertyProvider getUserPropertyProvider(String username) + { + if (providers.containsKey(username)) { + return providers.get(username); + } + return providers.get(null); + } + + @Override + public Set getUserPropertyProviders() + { + return new HashSet<>(providers.values()); + } +} From 916f1e62eb6228a6daced5821fdfa803ccdefedd Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Fri, 13 Dec 2024 19:05:20 +0100 Subject: [PATCH 11/14] Improve LDAP documentation for having multiple hosts The `ldap.host` property can be used to define hot standby hosts. The existing documentation hints at that, but isn't overly clear. This commit adds more explicit wording. --- documentation/ldap-guide.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/documentation/ldap-guide.html b/documentation/ldap-guide.html index a842c1929d..73f7620ca2 100644 --- a/documentation/ldap-guide.html +++ b/documentation/ldap-guide.html @@ -180,7 +180,14 @@

      Main Settings

      ldap.host *
      LDAP server host; e.g. localhost or machine.example.com, etc. It is possible to use many LDAP servers but all of them should share the same configuration (e.g. SSL, baseDN, admin account, - etc). To specify many LDAP servers use the comma or the white space character as delimiter.
      + etc). Openfire will prefer the first server, but will connect to the next server in case the first + server is unavailable. +

      To specify many LDAP servers use the comma or the white space character as delimiter. When + a server does not use the port number that's configured below, a port for each server can be configured + by separating them from the hostname with a colon-character. In this example, two servers are configured, + that each use a distinct port: primary.example.org:10389 secondary.example.com:20389

      +

      When a host defines a port in this format, it overrides the port defined in the property below.

      +
      ldap.port
      LDAP server port number. If this property is not set, the default value is 389.
      From 66fcf8145983c5f17b5b70f463b66e3c839601e3 Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Fri, 13 Dec 2024 21:11:51 +0100 Subject: [PATCH 12/14] doc: Integrating with more than one External Data Source This adds documentation for the functionality introduced in OF-2923. --- documentation/index.html | 3 + documentation/multi-providers.html | 374 ++++++++++++++++++ .../separating-admin-users-guide.html | 9 +- 3 files changed, 383 insertions(+), 3 deletions(-) create mode 100644 documentation/multi-providers.html diff --git a/documentation/index.html b/documentation/index.html index 3d56349479..648b318673 100644 --- a/documentation/index.html +++ b/documentation/index.html @@ -95,6 +95,9 @@

      Integration with External Data Sources

      Custom Database Integration Guide
      A guide to integrating Openfire authentication, user, and group data with a custom database.
      + +
      Integrating with more than one External Data Source
      +
      Describes how Openfire can be configured to not only one, but multiple External Data Source.

      In-depth Analysis

      diff --git a/documentation/multi-providers.html b/documentation/multi-providers.html new file mode 100644 index 0000000000..830b967795 --- /dev/null +++ b/documentation/multi-providers.html @@ -0,0 +1,374 @@ + + + + Openfire: Integrating with more than one External Data Source + + + + +
      + +
      + Openfire Logo +

      Integrating with more than one External Data Source

      +
      + + + +
      + +

      Introduction

      + +

      + Openfire can be configured to use a variety of external sources for authentication, users and groups. This is useful + when your users already have accounts in an external system, and you do not wish to duplicate those accounts. More + information on this subject is available in the Custom Database Integration + Guide, as well as the LDAP guide. You can even develop your own, custom + connectivity to external data sources, as described in the Custom User Provider Guide, + Custom Authentication Provider Guide and + Custom Group Provider Guide. +

      +

      + This document takes the concept one step further, and provides instructions on how to configure Openfire to obtain + its users from multiple backend systems. +

      + +

      Topics that are covered in this document:

      + + + +
      + +
      + +

      Mapped Providers

      + + +

      + A Mapped Provider is a provider that based on a particular characteristic of a user, uses a different provider + to perform the actual operations. +

      +

      + Openfire provides mapped providers for the following types of data: +

      +
      +
      Authentication
      +
      org.jivesoftware.openfire.auth.MappedAuthProvider
      + +
      Users
      +
      org.jivesoftware.openfire.user.MappedUserProvider
      + +
      Groups
      +
      org.jivesoftware.openfire.group.MappedGroupProvider
      + +
      User Properties
      +
      org.jivesoftware.openfire.user.property.MappedUserPropertyProvider
      +
      +

      + A Mapped Provider is configured with a Mapper. The Mapper will determine + which secondary provider to use for a particular user. Openfire provides the following types of Mappers: +

      +
        +
      • Authorization Based Mapper -- to draw administrative users from another source than the regular, non-administrative users.
      • +
      • Property Based Mapper -- uses properties to define sets of usernames and a corresponding provider.
      • +
      +

      + For each type of data, these Mappers are available +

      + + + + + + + + + + + + + + + + + + + + + + + + + + +
      Authorization BasedProperty Based
      Authenticationorg.jivesoftware.openfire.auth.AuthorizationBasedAuthProviderMapperorg.jivesoftware.openfire.auth.PropertyBasedAuthProviderMapper
      Usersorg.jivesoftware.openfire.user.AuthorizationBasedUserProviderMapperorg.jivesoftware.openfire.user.PropertyBasedUserProviderMapper
      Groupsorg.jivesoftware.openfire.group.AuthorizationBasedGroupProviderMapperorg.jivesoftware.openfire.group.PropertyBasedGroupProviderMapper
      User Propertiesorg.jivesoftware.openfire.user.property.AuthorizationBasedUserPropertyProviderMapperorg.jivesoftware.openfire.user.property.PropertyBasedUserPropertyProviderMapper
      + +

      Example Configuration

      + +

      + An elaborate example, including configuration examples, of how Mapped Providers can be used is provided in the + Separating Administrative Users Guide. This is a guide to + setting up Openfire to work with different user stores for admins and non-administrative users, by utilizing Mapped Providers. +

      + +
      + +
      + +

      Hybrid Providers

      + + +

      + The Hybrid variant of the providers iterates over its backing providers, operating on the first applicable + instance. +

      +

      + Openfire provides hybrid providers for the following types of data: +

      +
      +
      Authentication
      +
      org.jivesoftware.openfire.auth.HybridAuthProvider
      + +
      Users
      +
      org.jivesoftware.openfire.user.HybridUserProvider
      + +
      Groups
      +
      org.jivesoftware.openfire.group.HybridGroupProvider
      + +
      User Properties
      +
      org.jivesoftware.openfire.user.property.HybridUserPropertyProvider
      +
      +

      + Each Hybrid provider is configured to use up to three backing providers. These are configured through properties like these for the HybridAuthProvider: +

      +
        +
      • hybridAuthProvider.primaryProvider.className
      • +
      • hybridAuthProvider.secondaryProvider.className
      • +
      • hybridAuthProvider.tertiaryProvider.className
      • +
      +

      + The property value for each of these will be the canoncial class name of the desired backing provider. Please refer to the documentation of each Hybrid provider for the names of the properties used by that provider. +

      + +

      Example Configuration

      + +

      + Below is a sample config file section that illustrates the usage of Hybrid providers. For brevity, this + example is limited to User and Auth providers (note: the "..." sections in the examples indicate areas where + the rest of the config file would exist). +

      + +

      + First, Openfire is configured to use the Hybrid providers: +

      + +
      + openfire.xml configuration: enable 'Hybrid Providers' +
      <jive>
      +    ...
      +    <provider>
      +        <auth>
      +            <className>org.jivesoftware.openfire.auth.HybridAuthProvider</className>
      +        </auth>
      +        <user>
      +            <className>org.jivesoftware.openfire.user.HybridUserProvider</className>
      +        </user>
      +    </provider>
      +    ...
      +</jive>
      +
      + +

      + Next, each Hybrid provider is configured with a set of providers to use to interact with data stores. In the + example below, both an LDAP store and the default Openfire data stores are used. +

      + +
      + openfire.xml configuration: Adding specific providers +
      <jive>
      +    ...
      +    <hybridAuthProvider>
      +        <primaryProvider>
      +            <className>org.jivesoftware.openfire.ldap.LdapAuthProvider</className>
      +        </primaryProvider>
      +        <secondaryProvider>
      +            <className>org.jivesoftware.openfire.auth.DefaultAuthProvider</className>
      +        </secondaryProvider>
      +    </hybridAuthProvider>
      +
      +    <hybridUserProvider>
      +        <primaryProvider>
      +            <className>org.jivesoftware.openfire.ldap.LdapUserProvider</className>
      +        </primaryProvider>
      +        <secondaryProvider>
      +            <className>org.jivesoftware.openfire.user.DefaultUserProvider</className>
      +        </secondaryProvider>
      +    </hybridUserProvider>
      +    ...
      +</jive>
      +
      + +

      + The above completes the configuration of the Hybrid providers. When backing providers require additional + configuration, that should be added too. Shown below is the LDAP connection configuration which will be + used by the LDAP user and auth Provider. For good measure, a commonly-used property is used that defines + what usernames are considered to be administrators. +

      + +
      + openfire.xml configuration: Wrapping up configuration +
      <jive>
      +    ...
      +    <ldap>
      +        <host>localhost</host>
      +        <port>389</port>
      +        <baseDN>ou=people,dc=springfield,dc=com</baseDN>
      +        <adminDN>cn=admin,dc=springfield,dc=com</adminDN>
      +        <adminPassword>Anytown</adminPassword>
      +        <startTlsEnabled>false</startTlsEnabled>
      +        <sslEnabled>false</sslEnabled>
      +    </ldap>
      +
      +    <admin>
      +        <authorizedUsernames>bart</authorizedUsernames>
      +    </admin>
      +    ...
      +</jive>
      +
      + +

      Using more than one provider of the same type

      + +

      + The example above shows how a Hybrid provider is used to delegate access to two data stores of different + types: an LDAP store, and Openfire's own store. What if you'd like to use two LDAP services, each on a + separate host? +

      +

      + In the configuration example above, both the LdapAuthProvider and LdapUserProvider + use the same LDAP connectivity configuration. All LDAP providers by default will use LDAP connectivity + configuration as defined in the ldap properties. How can you configure multiple LDAP providers + that each connect to a different LDAP host? +

      +

      + The configuration of each backing provider configured in a Hybrid provider can include an optional + config element. This element is used to point at the property that holds the connection + configuration for the provider. +

      +

      + In the example below, Hybrid providers are configured for both Auth and Users. Each Hybrid provider is + configured to use two backing providers, that are both LDAP-based. Note how for each provider, a config + value is provided that refers to another property, in which the LDAP connection information that's applicable + to that provider is defined. +

      + +
      + openfire.xml configuration: Using two different LDAP servers +
      <jive>
      +    ...
      +    <provider>
      +        <auth>
      +            <className>org.jivesoftware.openfire.auth.HybridAuthProvider</className>
      +        </auth>
      +        <user>
      +            <className>org.jivesoftware.openfire.user.HybridUserProvider</className>
      +        </user>
      +    </provider>
      +
      +    <hybridAuthProvider>
      +        <primaryProvider>
      +            <className>org.jivesoftware.openfire.ldap.LdapAuthProvider</className>
      +            <config>ldapPrimary</config>
      +        </primaryProvider>
      +        <secondaryProvider>
      +            <className>org.jivesoftware.openfire.ldap.LdapAuthProvider</className>
      +            <config>ldapSecondary</config>
      +        </secondaryProvider>
      +    </hybridAuthProvider>
      +
      +    <hybridUserProvider>
      +        <primaryProvider>
      +            <className>org.jivesoftware.openfire.ldap.LdapUserProvider</className>
      +            <config>ldapPrimary</config>
      +        </primaryProvider>
      +        <secondaryProvider>
      +            <className>org.jivesoftware.openfire.ldap.LdapUserProvider</className>
      +            <config>ldapSecondary</config>
      +        </secondaryProvider>
      +    </hybridUserProvider>
      +
      +    <ldapPrimary>
      +        <host>dc.springfieldcom</host>
      +        <port>10389</port>
      +        <baseDN>dc=springfield,dc=com</baseDN>
      +        <adminDN>cn=admin,dc=springfield,dc=com</adminDN>
      +        <adminPassword>Anytown</adminPassword>
      +        <startTlsEnabled>false</startTlsEnabled>
      +        <sslEnabled>false</sslEnabled>
      +    </ldapPrimary>
      +
      +    <ldapSecondary>
      +        <host>ldap.planetexpress.com</host>
      +        <port>389</port>
      +        <baseDN>ou=people,dc=planetexpress,dc=com</baseDN>
      +        <adminDN>cn=admin,dc=planetexpress,dc=com</adminDN>
      +        <adminPassword>GoodNewsEveryone</adminPassword>
      +        <startTlsEnabled>false</startTlsEnabled>
      +        <sslEnabled>false</sslEnabled>
      +        <groupSearchFilter>(objectClass=Group)</groupSearchFilter>
      +    </ldapSecondary>
      +    ...
      +</jive>
      +
      +
      + +
      + +

      Frequently Asked Questions

      + +

      Can I add my own Mapper?

      +

      + Absolutely. You can implement your custom implementation and place the new custom library in the Openfire lib + directory. This will ensure it is automatically available at startup. You can then reference it in Openfire's + configuration by its canonical class name. +

      + +

      Can I use my custom auth/user/group provider in a Mapped or Hybrid provider?

      +

      + Yes! If you've impelmented custom code (as described in the Custom User Provider Guide, + Custom Authentication Provider Guide and + Custom Group Provider Guide) then you can use reference + these implementations by their canonical class name in Openfire's configuration. +

      + +
      + + + +
      + + + diff --git a/documentation/separating-admin-users-guide.html b/documentation/separating-admin-users-guide.html index 025f79e0be..3c8da67a1c 100644 --- a/documentation/separating-admin-users-guide.html +++ b/documentation/separating-admin-users-guide.html @@ -24,8 +24,11 @@

      Introduction

      Openfire can be configured to use a variety of external sources for authentication, users and groups. This is useful when your users already have accounts in an external system, and you do not wish to duplicate those accounts. More - information on this subject is available in the the Custom Database Integration - Guide, as well as the LDAP guide. + information on this subject is available in the Custom Database Integration + Guide, as well as the LDAP guide. You can even develop your own, custom + connectivity to external data sources, as described in the Custom User Provider Guide, + Custom Authentication Provider Guide and + Custom Group Provider Guide.

      This document takes the concept one step further, and provides instructions on how to configure Openfire to obtain @@ -231,7 +234,7 @@

      Example Configuration

      - Each of the Mappers is told what provider to user for administrative and regular users: + Each of the Mappers is told what provider to use for administrative and regular users:

      From 6c26e66aa9ef26a7ee2661bf49f766cfa801ea78 Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Mon, 16 Dec 2024 17:07:38 +0100 Subject: [PATCH 13/14] docs: fix typo in multi-providers guide --- documentation/multi-providers.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/multi-providers.html b/documentation/multi-providers.html index 830b967795..e1933791fd 100644 --- a/documentation/multi-providers.html +++ b/documentation/multi-providers.html @@ -353,7 +353,7 @@

      Can I add my own Mapper?

      Can I use my custom auth/user/group provider in a Mapped or Hybrid provider?

      - Yes! If you've impelmented custom code (as described in the Custom User Provider Guide, + Yes! If you've implemented custom code (as described in the Custom User Provider Guide, Custom Authentication Provider Guide and Custom Group Provider Guide) then you can use reference these implementations by their canonical class name in Openfire's configuration. From e34ea33a6fe7dfa6f3a2734e9186efcf0ef9136d Mon Sep 17 00:00:00 2001 From: Guus der Kinderen Date: Wed, 18 Dec 2024 11:58:24 +0100 Subject: [PATCH 14/14] Applied (non-functional) changes in response to review feedback. --- documentation/multi-providers.html | 4 ++-- documentation/separating-admin-users-guide.html | 2 +- .../org/jivesoftware/openfire/group/GroupMultiProvider.java | 5 +++++ .../java/org/jivesoftware/openfire/group/GroupProvider.java | 5 +++++ .../org/jivesoftware/openfire/group/HybridGroupProvider.java | 2 +- 5 files changed, 14 insertions(+), 4 deletions(-) diff --git a/documentation/multi-providers.html b/documentation/multi-providers.html index e1933791fd..75cdbec4cf 100644 --- a/documentation/multi-providers.html +++ b/documentation/multi-providers.html @@ -83,7 +83,7 @@

      Mapped Providers

      • Authorization Based Mapper -- to draw administrative users from another source than the regular, non-administrative users.
      • -
      • Property Based Mapper -- uses properties to define sets of usernames and a corresponding provider.
      • +
      • Property Based Mapper -- uses Openfire system properties (that hold a list of usernames) to relate specific users to specific providers.

      For each type of data, these Mappers are available @@ -316,7 +316,7 @@

      Using more than one provider of the same type

      </hybridUserProvider> <ldapPrimary> - <host>dc.springfieldcom</host> + <host>dc.springfield.com</host> <port>10389</port> <baseDN>dc=springfield,dc=com</baseDN> <adminDN>cn=admin,dc=springfield,dc=com</adminDN> diff --git a/documentation/separating-admin-users-guide.html b/documentation/separating-admin-users-guide.html index 3c8da67a1c..358f1442d9 100644 --- a/documentation/separating-admin-users-guide.html +++ b/documentation/separating-admin-users-guide.html @@ -234,7 +234,7 @@

      Example Configuration

      - Each of the Mappers is told what provider to use for administrative and regular users: + Each of the Mappers is told which provider to use for administrative and regular users:

      diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/group/GroupMultiProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/group/GroupMultiProvider.java index 32c1da6df0..5b7de6932a 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/group/GroupMultiProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/group/GroupMultiProvider.java @@ -402,6 +402,7 @@ public Group getGroup(String name) throws GroupNotFoundException * @return the newly created group. * @throws GroupAlreadyExistsException if a group with the same name already exists. * @throws UnsupportedOperationException if the provider does not support the operation. + * @throws GroupNameInvalidException if the provided new name is an unacceptable value. */ @Override public Group createGroup(String name) throws GroupAlreadyExistsException, GroupNameInvalidException @@ -437,6 +438,8 @@ public void deleteGroup(String name) throws GroupNotFoundException * @param newName the desired new name of the group. * @throws GroupAlreadyExistsException if a group with the same name already exists. * @throws UnsupportedOperationException if the provider does not support the operation. + * @throws GroupNotFoundException if the provided old name does not refer to an existing group. + * @throws GroupNameInvalidException if the provided new name is an unacceptable value. */ @Override public void setName(String oldName, String newName) throws GroupAlreadyExistsException, GroupNameInvalidException, GroupNotFoundException @@ -466,6 +469,7 @@ public void setDescription(String name, String description) throws GroupNotFound * @param user the (bare) JID of the entity to add * @param administrator True if the member is an administrator of the group * @throws UnsupportedOperationException if the provider does not support the operation. + * @throws GroupNotFoundException if the provided group name does not refer to an existing group. */ @Override public void addMember(String groupName, JID user, boolean administrator) throws GroupNotFoundException @@ -482,6 +486,7 @@ public void addMember(String groupName, JID user, boolean administrator) throws * @param user the (bare) JID of the entity with new privileges * @param administrator True if the member is an administrator of the group * @throws UnsupportedOperationException if the provider does not support the operation. + * @throws GroupNotFoundException if the provided group name does not refer to an existing group. */ @Override public void updateMember(String groupName, JID user, boolean administrator) throws GroupNotFoundException diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/group/GroupProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/group/GroupProvider.java index bcccecf1ce..db77e7c6c3 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/group/GroupProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/group/GroupProvider.java @@ -58,6 +58,7 @@ public interface GroupProvider { * exists. * @throws UnsupportedOperationException if the provider does not * support the operation. + * @throws GroupNameInvalidException if the provided new name is an unacceptable value. */ Group createGroup(String name) throws GroupAlreadyExistsException, GroupNameInvalidException; @@ -88,6 +89,8 @@ public interface GroupProvider { * exists. * @throws UnsupportedOperationException if the provider does not * support the operation. + * @throws GroupNotFoundException if the provided old name does not refer to an existing group. + * @throws GroupNameInvalidException if the provided new name is an unacceptable value. */ void setName(String oldName, String newName) throws GroupAlreadyExistsException, GroupNameInvalidException, GroupNotFoundException; @@ -186,6 +189,7 @@ public interface GroupProvider { * @param administrator True if the member is an administrator of the group * @throws UnsupportedOperationException if the provider does not * support the operation. + * @throws GroupNotFoundException if the provided group name does not refer to an existing group. */ void addMember(String groupName, JID user, boolean administrator) throws GroupNotFoundException; @@ -199,6 +203,7 @@ public interface GroupProvider { * @param administrator True if the member is an administrator of the group * @throws UnsupportedOperationException if the provider does not * support the operation. + * @throws GroupNotFoundException if the provided group name does not refer to an existing group. */ void updateMember(String groupName, JID user, boolean administrator) throws GroupNotFoundException; diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/group/HybridGroupProvider.java b/xmppserver/src/main/java/org/jivesoftware/openfire/group/HybridGroupProvider.java index c38fcb2686..8635ac3aa3 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/group/HybridGroupProvider.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/group/HybridGroupProvider.java @@ -154,7 +154,7 @@ GroupProvider getGroupProvider(String groupName) @Override public Group getGroup(String name) throws GroupNotFoundException { - // Override the implementation in the superclass to prevent obtaining the griyo twice. + // Override the implementation in the superclass to prevent obtaining the group twice. for (final GroupProvider provider : groupProviders) { try { return provider.getGroup(name);