This is an automated email from the ASF dual-hosted git repository.
vavrtom pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/qpid-broker-j.git
The following commit(s) were added to refs/heads/main by this push:
new 58b1b3e QPID-8566: [Broker-J] Implement composite authentication
provider (#118)
58b1b3e is described below
commit 58b1b3e961c4259956d084bc130ba4bc23fdb468
Author: Daniil Kirilyuk <[email protected]>
AuthorDate: Wed Mar 9 07:04:16 2022 +0100
QPID-8566: [Broker-J] Implement composite authentication provider (#118)
Co-authored-by: aw924 <[email protected]>
---
...ositeUsernamePasswordAuthenticationManager.java | 35 +
...eUsernamePasswordAuthenticationManagerImpl.java | 528 +++++++++++
...eUsernamePasswordAuthenticationManagerTest.java | 963 +++++++++++++++++++++
.../authenticationprovider/composite/add.html | 28 +
.../authenticationprovider/composite/show.html | 26 +
.../authenticationprovider/composite/add.js | 159 ++++
.../authenticationprovider/composite/show.js | 38 +
...Security-Authentication-Providers-Composite.xml | 76 ++
...va-Broker-Security-Authentication-Providers.xml | 1 +
9 files changed, 1854 insertions(+)
diff --git
a/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManager.java
b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManager.java
new file mode 100644
index 0000000..a9e1131
--- /dev/null
+++
b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManager.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.qpid.server.security.auth.manager;
+
+import java.util.List;
+
+import org.apache.qpid.server.model.ManagedAttribute;
+import org.apache.qpid.server.model.ManagedObject;
+
+@ManagedObject( category = false, type = "Composite" )
+public interface CompositeUsernamePasswordAuthenticationManager<T extends
CompositeUsernamePasswordAuthenticationManager<T>>
+ extends CachingAuthenticationProvider<T>,
UsernamePasswordAuthenticationProvider<T>
+{
+ String PROVIDER_TYPE = "Composite";
+
+ @ManagedAttribute(description = "delegate authentication providers",
mandatory = true)
+ List<String> getDelegates();
+
+}
diff --git
a/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManagerImpl.java
b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManagerImpl.java
new file mode 100644
index 0000000..3674ef5
--- /dev/null
+++
b/broker-core/src/main/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManagerImpl.java
@@ -0,0 +1,528 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.qpid.server.security.auth.manager;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.qpid.server.configuration.IllegalConfigurationException;
+import org.apache.qpid.server.model.AuthenticationProvider;
+import org.apache.qpid.server.model.Broker;
+import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.model.Container;
+import org.apache.qpid.server.model.ManagedAttributeField;
+import org.apache.qpid.server.model.ManagedObjectFactoryConstructor;
+import org.apache.qpid.server.model.NamedAddressSpace;
+import org.apache.qpid.server.security.auth.AuthenticationResult;
+import org.apache.qpid.server.security.auth.sasl.PasswordSource;
+import org.apache.qpid.server.security.auth.sasl.SaslNegotiator;
+import org.apache.qpid.server.security.auth.sasl.SaslSettings;
+import
org.apache.qpid.server.security.auth.sasl.crammd5.CramMd5Base64HashedNegotiator;
+import
org.apache.qpid.server.security.auth.sasl.crammd5.CramMd5Base64HexNegotiator;
+import org.apache.qpid.server.security.auth.sasl.crammd5.CramMd5Negotiator;
+import org.apache.qpid.server.security.auth.sasl.plain.PlainNegotiator;
+import org.apache.qpid.server.security.auth.sasl.scram.ScramNegotiator;
+import org.apache.qpid.server.security.auth.sasl.scram.ScramSaslServerSource;
+import
org.apache.qpid.server.security.auth.sasl.scram.ScramSaslServerSourceAdapter;
+import org.apache.qpid.server.util.ConnectionScopedRuntimeException;
+
+/**
+ * Composite username / password authentication provider.
+ *
+ * Contains list of delegate authentication providers, which are assessed one
by one during authentication process
+ * until first successful authentication or until all authentication attempts
fail.
+ *
+ * When two delegates share same SASL mechanism (e.g.
PlainAuthenticationProvider and ScramSHA256AuthenticationManager
+ * have in common SCRAM-SHA-256), implementation is resolved in runtime
choosing the delegate containing username requested.
+ * If user with same username is present in both delegates, authentication
will be performed only against the first
+ * delegate in the list.
+ *
+ */
+public class CompositeUsernamePasswordAuthenticationManagerImpl
+ extends
AbstractAuthenticationManager<CompositeUsernamePasswordAuthenticationManagerImpl>
+ implements
CompositeUsernamePasswordAuthenticationManager<CompositeUsernamePasswordAuthenticationManagerImpl>
+{
+
+ /**
+ * Mechanism name
+ */
+ @SuppressWarnings("unused")
+ public static final String MECHANISM_NAME = "COMPOSITE";
+
+ /**
+ * Logger
+ */
+ private static final Logger LOGGER =
LoggerFactory.getLogger(CompositeUsernamePasswordAuthenticationManagerImpl.class);
+
+ /**
+ * List of delegate authentication provider names
+ */
+ @SuppressWarnings("unused")
+ @ManagedAttributeField
+ private List<String> _delegates;
+
+ /**
+ * Authentication provider delegates
+ */
+ private final Set<UsernamePasswordAuthenticationProvider<?>>
_authenticationProviders = new LinkedHashSet<>();
+
+ /**
+ * Available SASL negotiators
+ */
+ private final Map<String, Function<SaslSettings, SaslNegotiator>>
_saslNegotiators = new HashMap<>();
+
+ /**
+ * Available scram adapters
+ */
+ private final Map<String, ScramSaslServerSourceAdapter> _scramAdapters =
new HashMap<>();
+
+ /**
+ * HMAC names
+ */
+ private final Map<String, String> _hmacNames = new HashMap<>();
+
+ /**
+ * Digest names
+ */
+ private final Map<String, String> _digestNames = new HashMap<>();
+
+ /**
+ * List of supported SASL mechanisms
+ */
+ private List<String> _mechanisms = new ArrayList<>();
+
+ /**
+ * List of supported SASL mechanisms
+ */
+ private List<String> _secureOnlyMechanisms = new ArrayList<>();
+
+ /**
+ * List of supported SASL mechanisms
+ */
+ private List<String> _disabledMechanisms = new ArrayList<>();
+
+ /**
+ * Scram iterations count
+ */
+ final int scramIterationCount = getContextValue(
+ Integer.class,
+
AbstractScramAuthenticationManager.QPID_AUTHMANAGER_SCRAM_ITERATION_COUNT
+ );
+
+ /**
+ * Constructor creates configured object
+ *
+ * @param attributes Attributes
+ * @param container Parent container
+ */
+ @ManagedObjectFactoryConstructor()
+ public CompositeUsernamePasswordAuthenticationManagerImpl(final
Map<String, Object> attributes, final Container<?> container)
+ {
+ super(attributes, container);
+ }
+
+ /**
+ * Initiates SCRAM adapters, delegates
+ */
+ @Override
+ protected void postResolveChildren()
+ {
+ super.postResolveChildren();
+
+ final PasswordSource passwordSource = getPasswordSource();
+
+ _scramAdapters.put(
+ ScramSHA1AuthenticationManager.MECHANISM,
+ new ScramSaslServerSourceAdapter(
+ scramIterationCount,
+ ScramSHA1AuthenticationManager.HMAC_NAME,
+ ScramSHA1AuthenticationManager.DIGEST_NAME,
+ passwordSource
+ )
+ );
+
+ _scramAdapters.put(
+ ScramSHA256AuthenticationManager.MECHANISM,
+ new ScramSaslServerSourceAdapter(
+ scramIterationCount,
+ ScramSHA256AuthenticationManager.HMAC_NAME,
+ ScramSHA256AuthenticationManager.DIGEST_NAME,
+ passwordSource
+ )
+ );
+
+ // check for duplicates
+ if (new HashSet<>(_delegates).size() != _delegates.size())
+ {
+ throw new IllegalConfigurationException("Composite authentication
manager shouldn't contain duplicate names");
+ }
+
+ // prepare delegate authentication providers
+ for (final String delegate: _delegates)
+ {
+ final AuthenticationProvider<?> authProvider =
resolveDelegate(delegate);
+
_authenticationProviders.add((UsernamePasswordAuthenticationProvider<?>)
authProvider);
+ }
+
+ if (_authenticationProviders.isEmpty())
+ {
+ throw new IllegalConfigurationException("Composite authentication
manager should contain at least one delegate");
+ }
+
+ // supported SASL mechanisms are prepared as intersection of delegates
mechanisms
+ _mechanisms = new
ArrayList<>(_authenticationProviders.stream().findFirst().get().getMechanisms());
+ _authenticationProviders.forEach(authProvider ->
_mechanisms.retainAll(authProvider.getMechanisms()));
+ _authenticationProviders.stream()
+ .filter(authProvider -> authProvider.getDisabledMechanisms() !=
null)
+ .forEach(authProvider ->
_mechanisms.removeAll(authProvider.getDisabledMechanisms()));
+
+ // secure only SASL mechanisms are prepared as union of delegates
secure only mechanisms
+ _secureOnlyMechanisms = Stream.concat(
+
Optional.ofNullable(super.getSecureOnlyMechanisms()).orElse(Collections.emptyList()).stream(),
+ _authenticationProviders.stream()
+ .filter(authProvider -> authProvider.getSecureOnlyMechanisms() !=
null)
+ .flatMap(authProvider ->
authProvider.getSecureOnlyMechanisms().stream()))
+ .distinct().collect(Collectors.toList());
+
+ // disabled only SASL mechanisms are prepared as union of delegates
disabled mechanisms
+ _disabledMechanisms = Stream.concat(
+
Optional.ofNullable(super.getDisabledMechanisms()).orElse(Collections.emptyList()).stream(),
+ _authenticationProviders.stream()
+ .filter(authProvider -> authProvider.getDisabledMechanisms() !=
null)
+ .flatMap(authProvider ->
authProvider.getDisabledMechanisms().stream()))
+ .distinct().collect(Collectors.toList());
+ }
+
+ /**
+ * Initializes SASL negotiators
+ */
+ @Override
+ protected void onOpen()
+ {
+ super.onOpen();
+
+ // initialize hmac names
+ _hmacNames.put(ScramSHA1AuthenticationManager.MECHANISM,
ScramSHA1AuthenticationManager.HMAC_NAME);
+ _hmacNames.put(ScramSHA256AuthenticationManager.MECHANISM,
ScramSHA256AuthenticationManager.HMAC_NAME);
+
+ // initialize digest names
+ _digestNames.put(ScramSHA1AuthenticationManager.MECHANISM,
ScramSHA1AuthenticationManager.DIGEST_NAME);
+ _digestNames.put(ScramSHA256AuthenticationManager.MECHANISM,
ScramSHA256AuthenticationManager.DIGEST_NAME);
+
+ // initialize available SASL negotiators
+ _saslNegotiators.put(CramMd5Negotiator.MECHANISM, (saslSettings) ->
new CramMd5Negotiator(getAuthenticationProviderStub(),
saslSettings.getLocalFQDN(), getPasswordSource()));
+ _saslNegotiators.put(CramMd5Base64HashedNegotiator.MECHANISM,
(saslSettings) -> new
CramMd5Base64HashedNegotiator(getAuthenticationProviderStub(),
saslSettings.getLocalFQDN(), getPasswordSource()));
+ _saslNegotiators.put(CramMd5Base64HexNegotiator.MECHANISM,
(saslSettings) -> new
CramMd5Base64HexNegotiator(getAuthenticationProviderStub(),
saslSettings.getLocalFQDN(), getPasswordSource()));
+ _saslNegotiators.put(PlainNegotiator.MECHANISM, (saslSettings) -> new
PlainNegotiator(this));
+ _saslNegotiators.put(ScramSHA1AuthenticationManager.MECHANISM,
(saslSettings) -> new ScramNegotiator(this,
getScramSaslServerSource(ScramSHA1AuthenticationManager.MECHANISM),
ScramSHA1AuthenticationManager.MECHANISM));
+ _saslNegotiators.put(ScramSHA256AuthenticationManager.MECHANISM,
(saslSettings) -> new ScramNegotiator(this,
getScramSaslServerSource(ScramSHA256AuthenticationManager.MECHANISM),
ScramSHA256AuthenticationManager.MECHANISM));
+
+ }
+
+ /**
+ * Validate changes
+ *
+ * @param proxyForValidation ConfiguredObject
+ * @param changedAttributes Attribute names
+ */
+ @Override
+ public void validateChange(final ConfiguredObject<?> proxyForValidation,
final Set<String> changedAttributes)
+ {
+ super.validateChange(proxyForValidation, changedAttributes);
+ final Collection<String> delegates = (Collection<String>)
proxyForValidation.getAttribute("delegates");
+ if (delegates.isEmpty())
+ {
+ throw new IllegalConfigurationException("Composite authentication
manager should contain at least one delegate");
+ }
+ delegates.forEach(this::resolveDelegate);
+ }
+
+ /**
+ * MD5 => ["PLAIN", "CRAM-MD5-HASHED", "CRAM-MD5-HEX"]
+ * Plain => ["PLAIN", "CRAM-MD5", "SCRAM-SHA-1", "SCRAM-SHA-256"]
+ * SCRAM-SHA-1 => ["PLAIN", "SCRAM-SHA-1"]
+ * SCRAM-SHA-256 => ["PLAIN", "SCRAM-SHA-256"]
+ * SimpleLDAP => ["PLAIN"]
+ *
+ * @return List of mechanism names
+ */
+ @Override
+ public List<String> getMechanisms()
+ {
+ return Collections.unmodifiableList(_mechanisms);
+ }
+
+ /**
+ * Returns list of available SASL mechanism names
+ *
+ * @param secure Secure flag
+ *
+ * @return List of mechanism names
+ */
+ @Override
+ public List<String> getAvailableMechanisms(boolean secure)
+ {
+ final List<String> result = _mechanisms.stream()
+ .filter(mechanism -> secure ||
!_secureOnlyMechanisms.contains(mechanism))
+ .filter(mechanism -> !_disabledMechanisms.contains(mechanism))
+ .collect(Collectors.toList());
+ return Collections.unmodifiableList(result);
+ }
+
+ /**
+ * Creates SASL negotiator based on available options
+ *
+ * @param mechanism Mechanism name
+ * @param saslSettings SaslSettings
+ * @param addressSpace NamedAddressSpace
+ *
+ * @return SaslNegotiator
+ */
+ @Override
+ public SaslNegotiator createSaslNegotiator(
+ final String mechanism,
+ final SaslSettings saslSettings,
+ final NamedAddressSpace addressSpace
+ )
+ {
+ return _saslNegotiators.getOrDefault(mechanism, (settings) ->
null).apply(saslSettings);
+ }
+
+ /**
+ * Iterates over authentication provider delegates attempting
authentication for each one.
+ *
+ * @param username username
+ * @param password password
+ *
+ * @return AuthenticationResult
+ */
+ @Override
+ public AuthenticationResult authenticate(String username, String password)
+ {
+ for (final UsernamePasswordAuthenticationProvider<?>
authenticationProvider : _authenticationProviders)
+ {
+ final AuthenticationResult authResult =
authenticationProvider.authenticate(username, password);
+ if
(AuthenticationResult.AuthenticationStatus.ERROR.equals(authResult.getStatus()))
+ {
+ LOGGER.debug(
+ "Authentication of user '{}' against '{}' failed",
+ username,
+ authenticationProvider.getClass().getSimpleName()
+ );
+ continue;
+ }
+ LOGGER.debug(
+ "Authentication of user '{}' against '{}' succeeded",
+ username,
+ authenticationProvider.getClass().getSimpleName()
+ );
+ return authResult;
+ }
+ LOGGER.debug("All authentication attempts failed");
+ return new
AuthenticationResult(AuthenticationResult.AuthenticationStatus.ERROR);
+ }
+
+ /**
+ * Creates ScramSaslServerSource instance which will retrieve
SaltAndPasswordKeys from the authentication
+ * provider delegate containing requested username.
+ *
+ * @param mechanism SASL mechanism
+ *
+ * @return ScramSaslServerSource
+ */
+ private ScramSaslServerSource getScramSaslServerSource(String mechanism)
+ {
+ return new ScramSaslServerSource()
+ {
+
+ @Override
+ public int getIterationCount()
+ {
+ return scramIterationCount;
+ }
+
+ @Override
+ public String getDigestName()
+ {
+ return Optional.ofNullable(_digestNames.get(mechanism))
+ .orElseThrow(() -> new
ConnectionScopedRuntimeException("Mechanism '" + mechanism + "' not
supported"));
+ }
+
+ @Override
+ public String getHmacName()
+ {
+ return Optional.ofNullable(_hmacNames.get(mechanism))
+ .orElseThrow(() -> new
ConnectionScopedRuntimeException("Mechanism '" + mechanism + "' not
supported"));
+ }
+
+ @Override
+ public SaltAndPasswordKeys getSaltAndPasswordKeys(final String
username)
+ {
+ return _authenticationProviders.stream()
+ .filter(authProvider -> authProvider instanceof
ConfigModelPasswordManagingAuthenticationProvider<?>)
+ .filter(authProvider ->
((ConfigModelPasswordManagingAuthenticationProvider<?>)authProvider).getUser(username)
!= null)
+ .findFirst().map(authProvider ->
+ {
+ if (authProvider instanceof
AbstractScramAuthenticationManager<?>)
+ {
+ return ((AbstractScramAuthenticationManager<?>)
authProvider).getSaltAndPasswordKeys(username);
+ }
+ return
_scramAdapters.get(mechanism).getSaltAndPasswordKeys(username);
+
}).orElse(_scramAdapters.get(mechanism).getSaltAndPasswordKeys(username));
+ }
+ };
+ }
+
+ /**
+ * Resolves delegate authentication provider by name
+ *
+ * @param delegate Delegate name
+ *
+ * @return Delegate AuthenticationProvider
+ */
+ private AuthenticationProvider<?> resolveDelegate(final String delegate)
+ {
+ final Broker<?> broker = (Broker<?>) getParent();
+
+ final Optional<AuthenticationProvider<?>> optAuthProvider =
broker.getAuthenticationProviders().stream()
+ .filter(provider ->
provider.getName().equals(delegate)).findFirst();
+
+ if (!optAuthProvider.isPresent())
+ {
+ throw new IllegalConfigurationException("Authentication provider
'" + delegate + "' not found");
+ }
+
+ final AuthenticationProvider<?> authProvider = optAuthProvider.get();
+
+ if (!(authProvider instanceof
UsernamePasswordAuthenticationProvider<?>))
+ {
+ throw new IllegalConfigurationException("Authentication provider
'" + delegate + "' is not UsernamePasswordAuthenticationProvider");
+ }
+
+ if (authProvider instanceof
CompositeUsernamePasswordAuthenticationManager<?>)
+ {
+ throw new IllegalConfigurationException("Composite authentication
providers shouldn't be nested");
+ }
+
+ return authProvider;
+ }
+
+ /**
+ * Retrieves username from the first authentication provider containing
user with matching username
+ *
+ * @return PasswordSource
+ */
+ private PasswordSource getPasswordSource()
+ {
+ return username -> _authenticationProviders.stream()
+ .filter(authProvider -> authProvider instanceof
ConfigModelPasswordManagingAuthenticationProvider)
+ .filter(authProvider ->
((ConfigModelPasswordManagingAuthenticationProvider<?>)
authProvider).getUser(username) != null)
+ .findFirst()
+ .map(authProvider ->
(((ConfigModelPasswordManagingAuthenticationProvider<?>) authProvider)
+ .getPasswordSource().getPassword(username)))
+ .orElse(null);
+ }
+
+ /**
+ * Creates authentication provider stub used by some SASL negotiators
+ *
+ * @return ConfigModelPasswordManagingAuthenticationProvider stub
+ */
+ private <X extends ConfigModelPasswordManagingAuthenticationProvider<X>>
ConfigModelPasswordManagingAuthenticationProvider<X>
getAuthenticationProviderStub()
+ {
+ final Map<String, Object> attributes = new HashMap<>();
+ attributes.put(AuthenticationProvider.NAME,
"AuthenticationProviderStub");
+ attributes.put(AuthenticationProvider.ID, UUID.randomUUID());
+
+ final CompositeUsernamePasswordAuthenticationManagerImpl parent = this;
+ final PasswordSource passwordSource = getPasswordSource();
+
+ return new
ConfigModelPasswordManagingAuthenticationProvider<X>(attributes, (Container<?>)
getParent())
+ {
+
+ @Override
+ public AuthenticationResult authenticate(final String username,
final String password)
+ {
+ return parent.authenticate(username, password);
+ }
+
+ @Override
+ public PasswordSource getPasswordSource()
+ {
+ return passwordSource;
+ }
+
+ @Override
+ protected String createStoredPassword(final String password)
+ {
+ throw new ConnectionScopedRuntimeException("SaslNegotiator
isn't supposed to call createStoredPassword()");
+ }
+
+ @Override
+ void validateUser(final ManagedUser managedUser)
+ {
+ throw new ConnectionScopedRuntimeException("SaslNegotiator
isn't supposed to call validateUser()");
+ }
+
+ @Override
+ public List<String> getMechanisms()
+ {
+ throw new ConnectionScopedRuntimeException("SaslNegotiator
isn't supposed to call getMechanisms()");
+ }
+
+ @Override
+ public SaslNegotiator createSaslNegotiator(
+ final String mechanism,
+ final SaslSettings saslSettings,
+ final NamedAddressSpace addressSpace
+ )
+ {
+ throw new ConnectionScopedRuntimeException("SaslNegotiator
isn't supposed to call createSaslNegotiator()");
+ }
+ };
+ }
+
+ @Override
+ public String toString()
+ {
+ return "CompositeAuthenticationManagerImpl {"
+ + "_authenticationProviders=" + _authenticationProviders + '}';
+ }
+
+ @Override
+ public List<String> getDelegates()
+ {
+ return _delegates;
+ }
+}
diff --git
a/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManagerTest.java
b/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManagerTest.java
new file mode 100644
index 0000000..2e5a081
--- /dev/null
+++
b/broker-core/src/test/java/org/apache/qpid/server/security/auth/manager/CompositeUsernamePasswordAuthenticationManagerTest.java
@@ -0,0 +1,963 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.apache.qpid.server.security.auth.manager;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static
org.apache.qpid.server.security.auth.AuthenticationResult.AuthenticationStatus.SUCCESS;
+import static
org.apache.qpid.server.security.auth.AuthenticationResult.AuthenticationStatus.ERROR;
+import static
org.apache.qpid.server.security.auth.manager.CachingAuthenticationProvider.AUTHENTICATION_CACHE_MAX_SIZE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.when;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.directory.api.ldap.model.constants.SupportedSaslMechanisms;
+import org.apache.directory.server.annotations.CreateLdapServer;
+import org.apache.directory.server.annotations.CreateTransport;
+import org.apache.directory.server.annotations.SaslMechanism;
+import org.apache.directory.server.core.annotations.ApplyLdifFiles;
+import org.apache.directory.server.core.annotations.CreateDS;
+import org.apache.directory.server.core.annotations.CreatePartition;
+import org.apache.directory.server.core.integ.CreateLdapServerRule;
+import org.apache.directory.server.core.kerberos.KeyDerivationInterceptor;
+import
org.apache.directory.server.ldap.handlers.sasl.gssapi.GssapiMechanismHandler;
+import
org.apache.directory.server.ldap.handlers.sasl.plain.PlainMechanismHandler;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import org.apache.qpid.server.configuration.IllegalConfigurationException;
+import org.apache.qpid.server.configuration.updater.CurrentThreadTaskExecutor;
+import org.apache.qpid.server.configuration.updater.TaskExecutor;
+import org.apache.qpid.server.model.AuthenticationProvider;
+import org.apache.qpid.server.model.Broker;
+import org.apache.qpid.server.model.BrokerTestHelper;
+import org.apache.qpid.server.model.ConfiguredObject;
+import org.apache.qpid.server.security.auth.AuthenticationResult;
+import org.apache.qpid.server.security.auth.sasl.SaslNegotiator;
+import org.apache.qpid.server.security.auth.sasl.SaslSettings;
+import org.apache.qpid.server.security.auth.sasl.SaslUtil;
+import
org.apache.qpid.server.security.auth.sasl.crammd5.CramMd5Base64HashedNegotiator;
+import
org.apache.qpid.server.security.auth.sasl.crammd5.CramMd5Base64HexNegotiator;
+import org.apache.qpid.server.security.auth.sasl.crammd5.CramMd5Negotiator;
+import org.apache.qpid.server.util.Strings;
+import org.apache.qpid.test.utils.UnitTestBase;
+
+@CreateDS(
+ name = "testDS",
+ partitions =
+ {
+ @CreatePartition(name = "test", suffix = "dc=qpid,dc=org")
+ },
+ additionalInterceptors =
+ {
+ KeyDerivationInterceptor.class
+ }
+)
+@CreateLdapServer(
+ transports =
+ {
+ @CreateTransport(protocol = "LDAP")
+ },
+ allowAnonymousAccess = true,
+ saslHost = "localhost",
+ saslPrincipal = "ldap/[email protected]",
+ saslMechanisms =
+ {
+ @SaslMechanism(name = SupportedSaslMechanisms.PLAIN, implClass =
PlainMechanismHandler.class),
+ @SaslMechanism(name = SupportedSaslMechanisms.GSSAPI, implClass =
GssapiMechanismHandler.class)
+ }
+)
+@ApplyLdifFiles("users.ldif")
+public class CompositeUsernamePasswordAuthenticationManagerTest extends
UnitTestBase
+{
+ @ClassRule
+ public static CreateLdapServerRule LDAP = new CreateLdapServerRule();
+ private final List<AuthenticationProvider<?>> _authenticationProviders =
new ArrayList<>();
+ private Broker<?> _broker;
+ private TaskExecutor _executor;
+
+ private static final String USERNAME = "user1";
+ private static final String PASSWORD = "password1";
+
+ @Before
+ public void setUp() throws Exception
+ {
+ _executor = new CurrentThreadTaskExecutor();
+ _executor.start();
+ _broker = BrokerTestHelper.createBrokerMock();
+ when(_broker.getTaskExecutor()).thenReturn(_executor);
+ when(_broker.getChildExecutor()).thenReturn(_executor);
+
when(_broker.getAuthenticationProviders()).thenReturn(_authenticationProviders);
+ SaslHelper._clientNonce = UUID.randomUUID().toString();
+ }
+
+ @After
+ public void tearDown() throws Exception
+ {
+ _executor.stop();
+ _authenticationProviders.clear();
+ }
+
+ @SuppressWarnings("unchecked")
+ private CompositeUsernamePasswordAuthenticationManager<?>
createCompositeAuthenticationManager(
+ UsernamePasswordAuthenticationProvider<?>...
authenticationProviders
+
)
+ {
+ final Map<String, Object> attributesMap = new HashMap<>();
+ attributesMap.put(AuthenticationProvider.TYPE,
CompositeUsernamePasswordAuthenticationManager.PROVIDER_TYPE);
+ attributesMap.put(AuthenticationProvider.NAME,
"CompositeAuthenticationProvider");
+ attributesMap.put(AuthenticationProvider.ID, UUID.randomUUID());
+ if (authenticationProviders.length > 0)
+ {
+ attributesMap.put(
+ "delegates",
+
Arrays.stream(authenticationProviders).map(ConfiguredObject::getName).collect(Collectors.toList())
+ );
+ }
+
+ AuthenticationProvider<?> authProvider =
+
_broker.getObjectFactory().create(AuthenticationProvider.class, attributesMap,
_broker);
+ _authenticationProviders.add(authProvider);
+ return (CompositeUsernamePasswordAuthenticationManager<?>)
authProvider;
+ }
+
+ @SuppressWarnings("unchecked")
+ private MD5AuthenticationProvider createMD5AuthenticationProvider()
+ {
+ final Map<String, Object> attributesMap = new HashMap<>();
+ attributesMap.put(AuthenticationProvider.NAME,
"MD5AuthenticationProvider");
+ attributesMap.put(AuthenticationProvider.TYPE, "MD5");
+ attributesMap.put(AuthenticationProvider.ID, UUID.randomUUID());
+ AuthenticationProvider<?> authProvider =
+
_broker.getObjectFactory().create(AuthenticationProvider.class, attributesMap,
_broker);
+ _authenticationProviders.add(authProvider);
+ return (MD5AuthenticationProvider) authProvider;
+ }
+
+ @SuppressWarnings("unchecked")
+ private PlainAuthenticationProvider
createPlainAuthenticationProvider(String... names)
+ {
+ final Map<String, Object> attributesMap = new HashMap<>();
+ attributesMap.put(AuthenticationProvider.NAME, names.length == 0 ?
"PlainAuthenticationProvider" : names[0]);
+ attributesMap.put(AuthenticationProvider.TYPE, "Plain");
+ attributesMap.put(AuthenticationProvider.ID, UUID.randomUUID());
+ PlainAuthenticationProvider authProvider =
(PlainAuthenticationProvider) _broker.getObjectFactory()
+ .create(AuthenticationProvider.class, attributesMap, _broker);
+ _authenticationProviders.add(authProvider);
+ return authProvider;
+ }
+
+ @SuppressWarnings("unchecked")
+ private ScramSHA256AuthenticationManager
createScramSHA256AuthenticationManager(String... names)
+ {
+ final Map<String, Object> attributesMap = new HashMap<>();
+ attributesMap.put(AuthenticationProvider.NAME,
+ names.length == 0 ?
"ScramSHA256AuthenticationManager" : names[0]);
+ attributesMap.put(AuthenticationProvider.TYPE, "SCRAM-SHA-256");
+ attributesMap.put(AuthenticationProvider.ID, UUID.randomUUID());
+ ScramSHA256AuthenticationManager authProvider =
(ScramSHA256AuthenticationManager) _broker.getObjectFactory()
+ .create(AuthenticationProvider.class, attributesMap, _broker);
+ _authenticationProviders.add(authProvider);
+ return authProvider;
+ }
+
+ @SuppressWarnings("unchecked")
+ private ScramSHA1AuthenticationManager
createScramSHA1AuthenticationManager(String... names)
+ {
+ final Map<String, Object> attributesMap = new HashMap<>();
+ attributesMap.put(AuthenticationProvider.NAME,
+ names.length == 0 ? "ScramSHA1AuthenticationManager"
: names[0]);
+ attributesMap.put(AuthenticationProvider.TYPE, "SCRAM-SHA-1");
+ attributesMap.put(AuthenticationProvider.ID, UUID.randomUUID());
+ ScramSHA1AuthenticationManager authProvider =
(ScramSHA1AuthenticationManager) _broker.getObjectFactory()
+ .create(AuthenticationProvider.class, attributesMap, _broker);
+ _authenticationProviders.add(authProvider);
+ return authProvider;
+ }
+
+ @SuppressWarnings("unchecked")
+ private SimpleLDAPAuthenticationManager<?>
createSimpleLDAPAuthenticationManager()
+ {
+ final String LDAP_URL_TEMPLATE = "ldap://localhost:%d";
+ final String ROOT = "dc=qpid,dc=org";
+ final String SEARCH_CONTEXT_VALUE = "ou=users," + ROOT;
+ final String SEARCH_FILTER_VALUE = "(uid={0})";
+
+ final Map<String, Object> attributesMap = new HashMap<>();
+ attributesMap.put(SimpleLDAPAuthenticationManager.NAME,
"SimpleLDAPAuthenticationManager");
+ attributesMap.put(SimpleLDAPAuthenticationManager.ID,
UUID.randomUUID());
+ attributesMap.put(SimpleLDAPAuthenticationManager.TYPE,
SimpleLDAPAuthenticationManager.PROVIDER_TYPE);
+ attributesMap.put(SimpleLDAPAuthenticationManager.SEARCH_CONTEXT,
SEARCH_CONTEXT_VALUE);
+ attributesMap.put(SimpleLDAPAuthenticationManager.PROVIDER_URL,
+ String.format(LDAP_URL_TEMPLATE,
LDAP.getLdapServer().getPort()));
+ attributesMap.put(SimpleLDAPAuthenticationManager.SEARCH_FILTER,
SEARCH_FILTER_VALUE);
+ attributesMap.put(SimpleLDAPAuthenticationManager.CONTEXT,
+
Collections.singletonMap(AUTHENTICATION_CACHE_MAX_SIZE, "0"));
+ final SimpleLDAPAuthenticationManager<?> authProvider =
+ (SimpleLDAPAuthenticationManager<?>) _broker.getObjectFactory()
+ .create(AuthenticationProvider.class, attributesMap, _broker);
+ _authenticationProviders.add(authProvider);
+ return authProvider;
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void failToCreateCompositeAuthenticationManager()
+ {
+ createCompositeAuthenticationManager();
+ }
+
+ @Test()
+ public void authenticateAgainstPlainAuthenticationProvider() throws
Exception
+ {
+
+ final PlainAuthenticationProvider plainAuthenticationProvider =
createPlainAuthenticationProvider();
+ plainAuthenticationProvider.createUser(USERNAME, PASSWORD,
Collections.emptyMap());
+ final CompositeUsernamePasswordAuthenticationManager<?> authManager =
createCompositeAuthenticationManager(
+ plainAuthenticationProvider
+ );
+
+ final AuthenticationResult result = authManager.authenticate(USERNAME,
PASSWORD);
+ assertEquals("Unexpected result status", SUCCESS, result.getStatus());
+ assertEquals("Unexpected result principal", USERNAME,
result.getMainPrincipal().getName());
+
+ // authenticate via SASL PLAIN
+ final String RESPONSE = String.format("\0%s\0%s", USERNAME, PASSWORD);
+ final SaslNegotiator plainSaslNegotiator =
authManager.createSaslNegotiator("PLAIN", null, null);
+ final AuthenticationResult plainAuthResult =
plainSaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+ assertEquals("Unexpected result status",
+ SUCCESS,
+ plainAuthResult.getStatus());
+
+ // authenticate via SASL CRAM-MD5
+ saslCramMd(
+ CramMd5Negotiator.MECHANISM,
+ authManager.createSaslNegotiator(
+ CramMd5Negotiator.MECHANISM,
+ CRAM_MD_SASL_SETTINGS,
+ null
+ ),
+ USERNAME,
+ PASSWORD
+ );
+
+ // authenticate via SASL SCRAM-SHA-1
+ saslScramSha(
+ ScramSHA1AuthenticationManager.MECHANISM,
+
authManager.createSaslNegotiator(ScramSHA1AuthenticationManager.MECHANISM,
null, null),
+ USERNAME,
+ PASSWORD
+ );
+
+ // authenticate via SASL SCRAM-SHA-256
+ saslScramSha(
+ ScramSHA256AuthenticationManager.MECHANISM,
+
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM,
null, null),
+ USERNAME,
+ PASSWORD
+ );
+ }
+
+ @Test()
+ public void authenticateAgainstMD5AuthenticationProvider() throws Exception
+ {
+
+ final PlainAuthenticationProvider plainAuthenticationProvider =
createPlainAuthenticationProvider();
+ final MD5AuthenticationProvider md5AuthenticationProvider =
createMD5AuthenticationProvider();
+ md5AuthenticationProvider.createUser(USERNAME, PASSWORD,
Collections.emptyMap());
+ final CompositeUsernamePasswordAuthenticationManager<?> authManager =
createCompositeAuthenticationManager(
+ plainAuthenticationProvider, md5AuthenticationProvider
+ );
+ AuthenticationResult result = authManager.authenticate(USERNAME,
PASSWORD);
+ assertEquals("Unexpected result status", SUCCESS, result.getStatus());
+ assertEquals("Unexpected result principal", USERNAME,
result.getMainPrincipal().getName());
+
+ // authenticate via SASL PLAIN
+ final String RESPONSE = String.format("\0%s\0%s", USERNAME, PASSWORD);
+ final SaslNegotiator plainSaslNegotiator =
authManager.createSaslNegotiator("PLAIN", null, null);
+ final AuthenticationResult plainAuthResult =
plainSaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+ assertEquals("Unexpected result status", SUCCESS,
plainAuthResult.getStatus());
+
+ // authenticate via SASL CRAM-MD5-HASHED
+ saslCramMd(
+ CramMd5Base64HashedNegotiator.MECHANISM,
+ authManager.createSaslNegotiator(
+ CramMd5Base64HashedNegotiator.MECHANISM,
+ CRAM_MD_SASL_SETTINGS,
+ null
+ ),
+ USERNAME,
+ PASSWORD
+ );
+
+ // authenticate via SASL CRAM-MD5-HEX
+ saslCramMd(
+ CramMd5Base64HexNegotiator.MECHANISM,
+ authManager.createSaslNegotiator(
+ CramMd5Base64HexNegotiator.MECHANISM,
+ CRAM_MD_SASL_SETTINGS,
+ null
+ ),
+ USERNAME,
+ PASSWORD
+ );
+ }
+
+ @Test()
+ public void authenticateAgainstScramSHA1AuthenticationManager() throws
Exception
+ {
+
+ final ScramSHA1AuthenticationManager scramSHA1AuthenticationManager =
+ createScramSHA1AuthenticationManager();
+ scramSHA1AuthenticationManager.createUser(USERNAME, PASSWORD,
Collections.emptyMap());
+ final CompositeUsernamePasswordAuthenticationManager<?> authManager =
createCompositeAuthenticationManager(
+ scramSHA1AuthenticationManager
+ );
+ AuthenticationResult result = authManager.authenticate(USERNAME,
PASSWORD);
+ assertEquals("Unexpected result status", SUCCESS, result.getStatus());
+ assertEquals("Unexpected result principal", USERNAME,
result.getMainPrincipal().getName());
+
+ saslScramSha(
+ ScramSHA1AuthenticationManager.MECHANISM,
+
authManager.createSaslNegotiator(ScramSHA1AuthenticationManager.MECHANISM,
null, null),
+ USERNAME,
+ PASSWORD
+ );
+ }
+
+ @Test()
+ public void authenticateAgainstScramSHA256AuthenticationManager() throws
Exception
+ {
+
+ final ScramSHA256AuthenticationManager
scramSHA256AuthenticationManager =
+ createScramSHA256AuthenticationManager();
+ scramSHA256AuthenticationManager.createUser(USERNAME, PASSWORD,
Collections.emptyMap());
+ final CompositeUsernamePasswordAuthenticationManager<?> authManager =
createCompositeAuthenticationManager(
+ scramSHA256AuthenticationManager
+
);
+ AuthenticationResult result = authManager.authenticate(USERNAME,
PASSWORD);
+ assertEquals("Unexpected result status", SUCCESS, result.getStatus());
+ assertEquals("Unexpected result principal", USERNAME,
result.getMainPrincipal().getName());
+
+ saslScramSha(
+ ScramSHA256AuthenticationManager.MECHANISM,
+
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM,
null, null),
+ USERNAME,
+ PASSWORD
+ );
+ }
+
+ @Test()
+ public void authenticateAgainstSimpleLDAPAuthenticationManager()
+ {
+ final String LDAP_USERNAME = "test1";
+
+ final SimpleLDAPAuthenticationManager<?>
simpleLDAPAuthenticationManager =
+ createSimpleLDAPAuthenticationManager();
+
+ final CompositeUsernamePasswordAuthenticationManager<?> authManager =
createCompositeAuthenticationManager(
+ simpleLDAPAuthenticationManager
+ );
+
+ AuthenticationResult result = authManager.authenticate(LDAP_USERNAME,
PASSWORD);
+ assertEquals("Unexpected result status", SUCCESS, result.getStatus());
+ assertEquals("Unexpected result principal",
"cn=integration-test1,ou=users,dc=qpid,dc=org",
result.getMainPrincipal().getName());
+
+ // authenticate via SASL PLAIN
+ final String RESPONSE = String.format("\0%s\0%s", LDAP_USERNAME,
PASSWORD);
+ final SaslNegotiator plainSaslNegotiator =
authManager.createSaslNegotiator("PLAIN", null, null);
+ final AuthenticationResult plainAuthResult =
plainSaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+ assertEquals("Unexpected result status", SUCCESS,
plainAuthResult.getStatus());
+ }
+
+ @Test()
+ public void authenticateAgainstPlainAndMd5AndSimpleLdap() throws Exception
+ {
+ final String MD5_USERNAME = "user2";
+ final String MD5_PASSWORD = "password2";
+ final String LDAP_USERNAME = "test1";
+ final String LDAP_PASSWORD = "password1";
+
+ final PlainAuthenticationProvider plainAuthenticationProvider =
createPlainAuthenticationProvider();
+ plainAuthenticationProvider.createUser(USERNAME, PASSWORD,
Collections.emptyMap());
+
+ final MD5AuthenticationProvider md5AuthenticationProvider =
createMD5AuthenticationProvider();
+ md5AuthenticationProvider.createUser(MD5_USERNAME, MD5_PASSWORD,
Collections.emptyMap());
+
+ final SimpleLDAPAuthenticationManager<?>
simpleLDAPAuthenticationManager =
+ createSimpleLDAPAuthenticationManager();
+
+ final CompositeUsernamePasswordAuthenticationManager<?> authManager =
createCompositeAuthenticationManager(
+ plainAuthenticationProvider, md5AuthenticationProvider,
simpleLDAPAuthenticationManager
+ );
+
+ // authenticate against PlainAuthenticationProvider via SASL PLAIN
+ String RESPONSE = String.format("\0%s\0%s", USERNAME, PASSWORD);
+ final SaslNegotiator plainSaslNegotiator =
authManager.createSaslNegotiator("PLAIN", null, null);
+ final AuthenticationResult plainAuthResult =
plainSaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+ assertEquals("Unexpected result status", SUCCESS,
plainAuthResult.getStatus());
+ assertEquals(
+ "Unexpected result principal",
+ USERNAME,
+ plainAuthResult.getMainPrincipal().getName()
+ );
+
+ // authenticate against PlainAuthenticationProvider via SASL CRAM-MD5
+ saslCramMd(
+ CramMd5Negotiator.MECHANISM,
+ authManager.createSaslNegotiator(CramMd5Negotiator.MECHANISM,
CRAM_MD_SASL_SETTINGS, null),
+ USERNAME,
+ PASSWORD
+ );
+
+ // authenticate against PlainAuthenticationProvider via SASL
SCRAM-SHA-1
+ saslScramSha(
+ ScramSHA1AuthenticationManager.MECHANISM,
+
authManager.createSaslNegotiator(ScramSHA1AuthenticationManager.MECHANISM,
null, null),
+ USERNAME,
+ PASSWORD
+ );
+
+ // authenticate against PlainAuthenticationProvider via SASL
SCRAM-SHA-256
+ saslScramSha(
+ ScramSHA256AuthenticationManager.MECHANISM,
+
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM,
null, null),
+ USERNAME,
+ PASSWORD
+ );
+
+ // authenticate against MD5AuthenticationProvider via SASL PLAIN
+ RESPONSE = String.format("\0%s\0%s", MD5_USERNAME, MD5_PASSWORD);
+ final SaslNegotiator md5SaslNegotiator =
authManager.createSaslNegotiator("PLAIN", null, null);
+ final AuthenticationResult md5AuthResult =
md5SaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+ assertEquals("Unexpected result status", SUCCESS,
md5AuthResult.getStatus());
+ assertEquals(
+ "Unexpected result principal",
+ MD5_USERNAME,
+ md5AuthResult.getMainPrincipal().getName()
+ );
+
+ // authenticate against MD5AuthenticationProvider via SASL
CRAM-MD5-HASHED
+ saslCramMd(
+ CramMd5Base64HashedNegotiator.MECHANISM,
+
authManager.createSaslNegotiator(CramMd5Base64HashedNegotiator.MECHANISM,
CRAM_MD_SASL_SETTINGS, null),
+ MD5_USERNAME,
+ MD5_PASSWORD
+ );
+
+ // authenticate against MD5AuthenticationProvider via SASL CRAM-MD5-HEX
+ saslCramMd(
+ CramMd5Base64HexNegotiator.MECHANISM,
+
authManager.createSaslNegotiator(CramMd5Base64HexNegotiator.MECHANISM,
CRAM_MD_SASL_SETTINGS,null),
+ MD5_USERNAME,
+ MD5_PASSWORD
+ );
+
+ // authenticate against SimpleLdapAuthenticationProvider via SASL PLAIN
+ RESPONSE = String.format("\0%s\0%s", LDAP_USERNAME, LDAP_PASSWORD);
+ final SaslNegotiator ldapSaslNegotiator =
authManager.createSaslNegotiator("PLAIN", null, null);
+ final AuthenticationResult ldapAuthResult =
ldapSaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+ assertEquals("Unexpected result status", SUCCESS,
ldapAuthResult.getStatus());
+ assertEquals(
+ "Unexpected result principal",
+ "cn=integration-test1,ou=users,dc=qpid,dc=org",
+ ldapAuthResult.getMainPrincipal().getName()
+ );
+ }
+
+ @Test()
+ public void authenticateAgainstPlainAndSha256AndSimpleLdap() throws
Exception
+ {
+ final String SHA256_USERNAME = "user2";
+ final String SHA256_PASSWORD = "password2";
+ final String LDAP_USERNAME = "test1";
+ final String LDAP_PASSWORD = "password1";
+
+ final PlainAuthenticationProvider plainAuthenticationProvider =
createPlainAuthenticationProvider();
+ plainAuthenticationProvider.createUser(USERNAME, PASSWORD,
Collections.emptyMap());
+
+ final ScramSHA256AuthenticationManager sha256AuthenticationProvider =
createScramSHA256AuthenticationManager();
+ sha256AuthenticationProvider.createUser(SHA256_USERNAME,
SHA256_PASSWORD, Collections.emptyMap());
+
+ final SimpleLDAPAuthenticationManager<?>
simpleLDAPAuthenticationManager =
+ createSimpleLDAPAuthenticationManager();
+
+ final CompositeUsernamePasswordAuthenticationManager<?> authManager =
createCompositeAuthenticationManager(
+ plainAuthenticationProvider, sha256AuthenticationProvider,
simpleLDAPAuthenticationManager
+ );
+
+ // authenticate against PlainAuthenticationProvider via SASL PLAIN
+ String RESPONSE = String.format("\0%s\0%s", USERNAME, PASSWORD);
+ final SaslNegotiator plainSaslNegotiator =
authManager.createSaslNegotiator("PLAIN", null, null);
+ final AuthenticationResult plainAuthResult =
plainSaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+ assertEquals("Unexpected result status", SUCCESS,
plainAuthResult.getStatus());
+ assertEquals("Unexpected result principal", USERNAME,
plainAuthResult.getMainPrincipal().getName());
+
+ // authenticate against PlainAuthenticationProvider via SASL CRAM-MD5
+ saslCramMd(
+ CramMd5Negotiator.MECHANISM,
+ authManager.createSaslNegotiator(CramMd5Negotiator.MECHANISM,
CRAM_MD_SASL_SETTINGS, null),
+ USERNAME, PASSWORD);
+
+ // authenticate against PlainAuthenticationProvider via SASL
SCRAM-SHA-1
+ saslScramSha(
+ ScramSHA1AuthenticationManager.MECHANISM,
+
authManager.createSaslNegotiator(ScramSHA1AuthenticationManager.MECHANISM,
null, null),
+ USERNAME, PASSWORD);
+
+ // authenticate against PlainAuthenticationProvider via SASL
SCRAM-SHA-256
+ saslScramSha(
+ ScramSHA256AuthenticationManager.MECHANISM,
+
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM,
null, null),
+ USERNAME, PASSWORD);
+
+ // authenticate against ScramSHA256AuthenticationManager via SASL
SCRAM-SHA-256
+ saslScramSha(
+ ScramSHA256AuthenticationManager.MECHANISM,
+
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM,
null, null),
+ SHA256_USERNAME, SHA256_PASSWORD);
+
+ // authenticate against SimpleLdapAuthenticationProvider via SASL PLAIN
+ RESPONSE = String.format("\0%s\0%s", LDAP_USERNAME, LDAP_PASSWORD);
+ final SaslNegotiator ldapSaslNegotiator =
authManager.createSaslNegotiator("PLAIN", null, null);
+ final AuthenticationResult ldapAuthResult =
ldapSaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+ assertEquals("Unexpected result status", SUCCESS,
ldapAuthResult.getStatus());
+ assertEquals(
+ "Unexpected result principal",
+ "cn=integration-test1,ou=users,dc=qpid,dc=org",
+ ldapAuthResult.getMainPrincipal().getName()
+ );
+ }
+
+ @Test()
+ public void usernameCollision() throws Exception
+ {
+ final String PLAIN_PASSWORD = "password1";
+ final String SHA256_PASSWORD = "password2";
+
+ final PlainAuthenticationProvider plainAuthenticationProvider =
createPlainAuthenticationProvider();
+ plainAuthenticationProvider.createUser(USERNAME, PLAIN_PASSWORD,
Collections.emptyMap());
+
+ final ScramSHA256AuthenticationManager sha256AuthenticationProvider =
createScramSHA256AuthenticationManager();
+ sha256AuthenticationProvider.createUser(USERNAME, SHA256_PASSWORD,
Collections.emptyMap());
+
+ final CompositeUsernamePasswordAuthenticationManager<?> authManager =
createCompositeAuthenticationManager(
+ plainAuthenticationProvider, sha256AuthenticationProvider);
+
+ // authenticate against PlainAuthenticationProvider via SASL
SCRAM-SHA-256
+ saslScramSha(
+ ScramSHA256AuthenticationManager.MECHANISM,
+
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM,
null, null),
+ USERNAME, PLAIN_PASSWORD);
+
+ // authenticate against ScramSHA256AuthenticationManager via SASL
SCRAM-SHA-256 (fails due username collision)
+ saslScramShaInvalidCredentials(
+ ScramSHA256AuthenticationManager.MECHANISM,
+
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM,
null, null),
+ USERNAME, SHA256_PASSWORD);
+ }
+
+ @Test()
+ public void differentUsersInScramSHA256AuthenticationManagers() throws
Exception
+ {
+ final String SHA256_USERNAME2 = "user2";
+ final String SHA256_PASSWORD2 = "password2";
+ final String SHA256_USERNAME3 = "user3";
+ final String SHA256_PASSWORD3 = "password4";
+
+ final ScramSHA256AuthenticationManager sha256AuthenticationProvider1 =
createScramSHA256AuthenticationManager("ScramSHA256AuthenticationManager1");
+ sha256AuthenticationProvider1.createUser(USERNAME, PASSWORD,
Collections.emptyMap());
+
+ final ScramSHA256AuthenticationManager sha256AuthenticationProvider2 =
createScramSHA256AuthenticationManager("ScramSHA256AuthenticationManager2");
+ sha256AuthenticationProvider2.createUser(SHA256_USERNAME2,
SHA256_PASSWORD2, Collections.emptyMap());
+
+ final ScramSHA256AuthenticationManager sha256AuthenticationProvider3 =
createScramSHA256AuthenticationManager("ScramSHA256AuthenticationManager3");
+ sha256AuthenticationProvider3.createUser(SHA256_USERNAME3,
SHA256_PASSWORD3, Collections.emptyMap());
+
+ final CompositeUsernamePasswordAuthenticationManager<?> authManager =
createCompositeAuthenticationManager(
+ sha256AuthenticationProvider1, sha256AuthenticationProvider2,
sha256AuthenticationProvider3);
+
+ // authenticate against first ScramSHA256AuthenticationManager via
SASL SCRAM-SHA-256
+ saslScramSha(
+ ScramSHA256AuthenticationManager.MECHANISM,
+
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM,
null, null),
+ USERNAME, PASSWORD);
+
+ // authenticate against second ScramSHA256AuthenticationManager via
SASL SCRAM-SHA-256
+ saslScramSha(
+ ScramSHA256AuthenticationManager.MECHANISM,
+
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM,
null, null),
+ SHA256_USERNAME2, SHA256_PASSWORD2);
+
+ // authenticate against third ScramSHA256AuthenticationManager via
SASL SCRAM-SHA-256
+ saslScramSha(
+ ScramSHA256AuthenticationManager.MECHANISM,
+
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM,
null, null),
+ SHA256_USERNAME3, SHA256_PASSWORD3);
+
+ }
+
+ @Test()
+ public void userNotFound() throws Exception
+ {
+ final String SHA1_USERNAME = "user2";
+ final String SHA1_PASSWORD = "password2";
+ final String NON_EXISTING_USERNAME = "test99";
+
+ final ScramSHA256AuthenticationManager sha256AuthenticationProvider =
createScramSHA256AuthenticationManager();
+ sha256AuthenticationProvider.createUser(USERNAME, PASSWORD,
Collections.emptyMap());
+
+ final ScramSHA1AuthenticationManager sha1AuthenticationProvider =
createScramSHA1AuthenticationManager();
+ sha1AuthenticationProvider.createUser(SHA1_USERNAME, SHA1_PASSWORD,
Collections.emptyMap());
+
+ final SimpleLDAPAuthenticationManager<?>
simpleLDAPAuthenticationManager =
+ createSimpleLDAPAuthenticationManager();
+
+ final CompositeUsernamePasswordAuthenticationManager<?> authManager =
createCompositeAuthenticationManager(
+ sha256AuthenticationProvider, sha1AuthenticationProvider,
simpleLDAPAuthenticationManager);
+
+ // authenticate against ScramSHA256AuthenticationManager via SASL
SCRAM-SHA-256
+ saslScramShaInvalidCredentials(
+ ScramSHA256AuthenticationManager.MECHANISM,
+
authManager.createSaslNegotiator(ScramSHA256AuthenticationManager.MECHANISM,
null, null),
+ NON_EXISTING_USERNAME, PASSWORD);
+
+ // authenticate against ScramSHA256AuthenticationManager via SASL
SCRAM-SHA-1
+ saslScramShaInvalidCredentials(
+ ScramSHA1AuthenticationManager.MECHANISM,
+
authManager.createSaslNegotiator(ScramSHA1AuthenticationManager.MECHANISM,
null, null),
+ NON_EXISTING_USERNAME, SHA1_PASSWORD);
+
+ // authenticate against SimpleLdapAuthenticationProvider via SASL PLAIN
+ String RESPONSE = String.format("\0%s\0%s", NON_EXISTING_USERNAME,
PASSWORD);
+ final SaslNegotiator ldapSaslNegotiator =
authManager.createSaslNegotiator("PLAIN", null, null);
+ final AuthenticationResult ldapAuthResult =
ldapSaslNegotiator.handleResponse(RESPONSE.getBytes(US_ASCII));
+ assertEquals("Unexpected result status", ERROR,
ldapAuthResult.getStatus());
+ }
+
+ @Test(expected = IllegalConfigurationException.class)
+ public void nestedComposteUsernamePasswordAuthenticationManager()
+ {
+ final ScramSHA256AuthenticationManager sha256AuthenticationProvider =
createScramSHA256AuthenticationManager();
+ sha256AuthenticationProvider.createUser(USERNAME, PASSWORD,
Collections.emptyMap());
+ final CompositeUsernamePasswordAuthenticationManager<?> composite1 =
createCompositeAuthenticationManager(sha256AuthenticationProvider);
+ createCompositeAuthenticationManager(composite1);
+ }
+
+ @Test(expected = IllegalConfigurationException.class)
+ public void duplicateDelegates()
+ {
+ final ScramSHA256AuthenticationManager sha256AuthenticationProvider =
createScramSHA256AuthenticationManager();
+ sha256AuthenticationProvider.createUser(USERNAME, PASSWORD,
Collections.emptyMap());
+ createCompositeAuthenticationManager(sha256AuthenticationProvider,
sha256AuthenticationProvider);
+ }
+
+ private void saslCramMd(
+ String mechanism,
+ SaslNegotiator saslNegotiator,
+ String username,
+ String password
+ ) throws Exception
+ {
+
+ final AuthenticationResult firstResult =
saslNegotiator.handleResponse(new byte[0]);
+ assertEquals(
+ "Unexpected first result status",
+ AuthenticationResult.AuthenticationStatus.CONTINUE,
+ firstResult.getStatus()
+ );
+
+ byte[] responseBytes = SaslUtil.generateCramMD5ClientResponse(
+ mechanism, username, password, firstResult.getChallenge()
+ );
+
+ final AuthenticationResult secondResult =
saslNegotiator.handleResponse(responseBytes);
+
+ assertEquals("Unexpected second result status",
+ SUCCESS,
+ secondResult.getStatus());
+ assertNull("Unexpected second result challenge",
secondResult.getChallenge());
+ assertEquals("Unexpected second result main principal",
+ username,
+ secondResult.getMainPrincipal().getName());
+
+ final AuthenticationResult thirdResult =
saslNegotiator.handleResponse(new byte[0]);
+ assertEquals("Unexpected third result status",
+ AuthenticationResult.AuthenticationStatus.ERROR,
+ thirdResult.getStatus());
+ }
+
+ private void saslScramSha(
+ String mechanism,
+ SaslNegotiator saslNegotiator,
+ String username,
+ String password
+ ) throws Exception
+ {
+
+ final byte[] initialResponse =
SaslHelper.createInitialResponse(username);
+
+ final AuthenticationResult firstResult =
saslNegotiator.handleResponse(initialResponse);
+ assertEquals(
+ "Unexpected first result status",
+ AuthenticationResult.AuthenticationStatus.CONTINUE,
+ firstResult.getStatus()
+ );
+ assertNotNull("Unexpected first result challenge",
firstResult.getChallenge());
+
+ final byte[] response = SaslHelper.calculateClientProof(
+ firstResult.getChallenge(),
+ ScramSHA256AuthenticationManager.MECHANISM.equals(mechanism)
+ ? ScramSHA256AuthenticationManager.HMAC_NAME :
ScramSHA1AuthenticationManager.HMAC_NAME,
+ ScramSHA256AuthenticationManager.MECHANISM.equals(mechanism)
+ ? ScramSHA256AuthenticationManager.DIGEST_NAME :
ScramSHA1AuthenticationManager.DIGEST_NAME,
+ password
+ );
+
+ final AuthenticationResult secondResult =
saslNegotiator.handleResponse(response);
+ assertEquals(
+ "Unexpected second result status",
+ SUCCESS,
+ secondResult.getStatus()
+ );
+ assertNotNull("Unexpected second result challenge",
secondResult.getChallenge());
+ assertEquals(
+ "Unexpected second result principal",
+ username,
+ secondResult.getMainPrincipal().getName()
+ );
+
+ final String serverFinalMessage = new
String(secondResult.getChallenge(), SaslHelper.ASCII);
+ final String[] parts = serverFinalMessage.split(",");
+ if (!parts[0].startsWith("v="))
+ {
+ fail("Server final message did not contain verifier");
+ }
+ final byte[] serverSignature =
Strings.decodeBase64(parts[0].substring(2));
+ if (!Arrays.equals(SaslHelper._serverSignature, serverSignature))
+ {
+ fail("Server signature did not match");
+ }
+
+ final AuthenticationResult thirdResult =
saslNegotiator.handleResponse(initialResponse);
+ assertEquals("Unexpected result status after completion of
negotiation",
+ AuthenticationResult.AuthenticationStatus.ERROR,
+ thirdResult.getStatus());
+ assertNull("Unexpected principal after completion of negotiation",
thirdResult.getMainPrincipal());
+ }
+
+ private void saslScramShaInvalidCredentials(
+ String mechanism,
+ SaslNegotiator saslNegotiator,
+ String username,
+ String password) throws Exception
+ {
+
+ final byte[] initialResponse =
SaslHelper.createInitialResponse(username);
+
+ final AuthenticationResult firstResult =
saslNegotiator.handleResponse(initialResponse);
+ assertEquals(
+ "Unexpected first result status",
+ AuthenticationResult.AuthenticationStatus.CONTINUE,
+ firstResult.getStatus()
+ );
+ assertNotNull("Unexpected first result challenge",
firstResult.getChallenge());
+
+ final byte[] response = SaslHelper.calculateClientProof(
+ firstResult.getChallenge(),
+ ScramSHA256AuthenticationManager.MECHANISM.equals(mechanism)
+ ? ScramSHA256AuthenticationManager.HMAC_NAME :
ScramSHA1AuthenticationManager.HMAC_NAME,
+ ScramSHA256AuthenticationManager.MECHANISM.equals(mechanism)
+ ? ScramSHA256AuthenticationManager.DIGEST_NAME :
ScramSHA1AuthenticationManager.DIGEST_NAME,
+ password
+ );
+
+ final AuthenticationResult secondResult =
saslNegotiator.handleResponse(response);
+ assertEquals(
+ "Unexpected second result status",
+ ERROR,
+ secondResult.getStatus());
+ assertNull("Unexpected second result challenge",
secondResult.getChallenge());
+
+ }
+
+ private static class SaslHelper
+ {
+
+ private static final String GS2_HEADER = "n,,";
+ private static final Charset ASCII = US_ASCII;
+ private static String _clientFirstMessageBare;
+ private static String _clientNonce;
+ private static byte[] _serverSignature;
+
+ private static byte[] calculateClientProof(
+ final byte[] challenge,
+ String hmacName,
+ String digestName,
+ String userPassword
+ ) throws Exception
+ {
+
+ final String serverFirstMessage = new String(challenge, ASCII);
+ final String[] parts = serverFirstMessage.split(",");
+ if (parts.length < 3)
+ {
+ fail("Server challenge '" + serverFirstMessage + "' cannot be
parsed");
+ }
+ else if (parts[0].startsWith("m="))
+ {
+ fail("Server requires mandatory extension which is not
supported: " + parts[0]);
+ }
+ else if (!parts[0].startsWith("r="))
+ {
+ fail("Server challenge '" + serverFirstMessage + "' cannot be
parsed, cannot find nonce");
+ }
+ final String nonce = parts[0].substring(2);
+ if (!nonce.startsWith(_clientNonce))
+ {
+ fail("Server challenge did not use correct client nonce");
+ }
+ if (!parts[1].startsWith("s="))
+ {
+ fail("Server challenge '" + serverFirstMessage + "' cannot be
parsed, cannot find salt");
+ }
+ final byte[] salt = Strings.decodeBase64(parts[1].substring(2));
+ if (!parts[2].startsWith("i="))
+ {
+ fail("Server challenge '" + serverFirstMessage + "' cannot be
parsed, cannot find iteration count");
+ }
+ final int _iterationCount =
Integer.parseInt(parts[2].substring(2));
+ if (_iterationCount <= 0)
+ {
+ fail("Iteration count " + _iterationCount + " is not a
positive integer");
+ }
+ final byte[] passwordBytes =
saslPrep(userPassword).getBytes(StandardCharsets.UTF_8);
+ final byte[] saltedPassword =
generateSaltedPassword(passwordBytes, hmacName, _iterationCount, salt);
+
+ final String clientFinalMessageWithoutProof =
+ "c=" +
Base64.getEncoder().encodeToString(GS2_HEADER.getBytes(ASCII))
+ + ",r=" + nonce;
+
+ final String authMessage =
+ _clientFirstMessageBare + "," + serverFirstMessage + "," +
clientFinalMessageWithoutProof;
+ final byte[] clientKey = computeHmac(saltedPassword, "Client Key",
hmacName);
+ final byte[] storedKey =
MessageDigest.getInstance(digestName).digest(clientKey);
+ final byte[] clientSignature = computeHmac(storedKey, authMessage,
hmacName);
+ final byte[] clientProof = clientKey.clone();
+ for (int i = 0; i < clientProof.length; i++)
+ {
+ clientProof[i] ^= clientSignature[i];
+ }
+ final byte[] serverKey = computeHmac(saltedPassword, "Server Key",
hmacName);
+ _serverSignature = computeHmac(serverKey, authMessage, hmacName);
+ final String finalMessageWithProof = clientFinalMessageWithoutProof
+ + ",p=" +
Base64.getEncoder().encodeToString(clientProof);
+ return finalMessageWithProof.getBytes();
+ }
+
+ private static byte[] computeHmac(final byte[] key, final String
string, String hmacName)
+ throws Exception
+ {
+ final Mac mac = createHmac(key, hmacName);
+ mac.update(string.getBytes(ASCII));
+ return mac.doFinal();
+ }
+
+ private static byte[] generateSaltedPassword(
+ final byte[] passwordBytes,
+ String hmacName,
+ final int iterationCount,
+ final byte[] salt
+ ) throws Exception
+ {
+ final Mac mac = createHmac(passwordBytes, hmacName);
+ mac.update(salt);
+ mac.update(new byte[]{0, 0, 0, 1});
+ final byte[] result = mac.doFinal();
+
+ byte[] previous = null;
+ for (int i = 1; i < iterationCount; i++)
+ {
+ mac.update(previous != null ? previous : result);
+ previous = mac.doFinal();
+ for (int x = 0; x < result.length; x++)
+ {
+ result[x] ^= previous[x];
+ }
+ }
+
+ return result;
+ }
+
+ private static Mac createHmac(final byte[] keyBytes, String hmacName)
throws Exception
+ {
+ final SecretKeySpec key = new SecretKeySpec(keyBytes, hmacName);
+ final Mac mac = Mac.getInstance(hmacName);
+ mac.init(key);
+ return mac;
+ }
+
+ private static String saslPrep(String name)
+ {
+ name = name.replace("=", "=3D");
+ name = name.replace(",", "=2C");
+ return name;
+ }
+
+ private static byte[] createInitialResponse(final String userName)
+ {
+ _clientFirstMessageBare = "n=" + saslPrep(userName) + ",r=" +
_clientNonce;
+ return (GS2_HEADER + _clientFirstMessageBare).getBytes(ASCII);
+ }
+ }
+
+ private static final SaslSettings CRAM_MD_SASL_SETTINGS = new
SaslSettings()
+ {
+ @Override
+ public String getLocalFQDN()
+ {
+ return "example.com";
+ }
+
+ @Override
+ public Principal getExternalPrincipal()
+ {
+ return null;
+ }
+ };
+}
diff --git
a/broker-plugins/management-http/src/main/java/resources/authenticationprovider/composite/add.html
b/broker-plugins/management-http/src/main/java/resources/authenticationprovider/composite/add.html
new file mode 100644
index 0000000..b20ead9
--- /dev/null
+++
b/broker-plugins/management-http/src/main/java/resources/authenticationprovider/composite/add.html
@@ -0,0 +1,28 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you 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.
+ -->
+<div class="clear">
+ <div class="formLabel-labelCell tableContainer-labelCell">Delegates:</div>
+ <div class="formLabel-controlCell tableContainer-valueCell">
+ <div id="composite.delegates.container"/>
+ <input type="hidden" id="composite.delegates"/>
+ </div>
+ <div data-dojo-type="dijit/Tooltip"
+ data-dojo-props="connectId: ['composite.delegates'],
+ label: 'List of authentication provider
delegate names'"/>
+</div>
diff --git
a/broker-plugins/management-http/src/main/java/resources/authenticationprovider/composite/show.html
b/broker-plugins/management-http/src/main/java/resources/authenticationprovider/composite/show.html
new file mode 100644
index 0000000..1f40b1f
--- /dev/null
+++
b/broker-plugins/management-http/src/main/java/resources/authenticationprovider/composite/show.html
@@ -0,0 +1,26 @@
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one
+ ~ or more contributor license agreements. See the NOTICE file
+ ~ distributed with this work for additional information
+ ~ regarding copyright ownership. The ASF licenses this file
+ ~ to you 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.
+ -->
+<div>
+ <div class="clear">
+ <div class="formLabel-labelCell">Delegates:</div>
+ <div ><span class="delegates" ></span></div>
+ </div>
+ <div class="clear"></div>
+</div>
+
diff --git
a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/composite/add.js
b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/composite/add.js
new file mode 100644
index 0000000..5490ec6
--- /dev/null
+++
b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/composite/add.js
@@ -0,0 +1,159 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ *
+ */
+define([
+ 'dojo/dom',
+ 'dojo/query',
+ 'dijit/registry',
+ 'qpid/common/util'
+], function (
+ dom,
+ query,
+ registry,
+ util
+)
+{
+ return {
+ show: function (data)
+ {
+ util.parseHtmlIntoDiv(data.containerNode,
'authenticationprovider/composite/add.html', function ()
+ {
+ if (!!data.data)
+ {
+ util.applyToWidgets(data.containerNode,
+ 'AuthenticationProvider',
+ 'Composite',
+ data.data,
+ data.metadata);
+ }
+ this.data = data;
+ const that = this;
+ this.management.load({type: 'authenticationprovider'},
{excludeInheritedContext: true, depth: 1})
+ .then(function (data)
+ {
+ const allowed = ['MD5', 'Plain', 'SCRAM-SHA-1',
'SCRAM-SHA-256', 'SimpleLDAP'];
+ const names = data
+ .filter(authProvider =>
allowed.includes(authProvider.type))
+ .map(authProvider => authProvider.name);
+
+ const authProviderNames = that.data?.data?.delegates
+ ? that.data?.data?.delegates.slice() : [];
+ for (const name of names)
+ {
+ if (!authProviderNames.includes(name))
+ {
+ authProviderNames.push(name);
+ }
+ }
+ const authProvidersMultiSelect =
dom.byId('composite.delegates.container');
+ authProvidersMultiSelect.style = 'border: 1px solid
lightgray; padding: .5em; width: 14em;';
+
+ for (const name of authProviderNames)
+ {
+ const row = document.createElement('div');
+ row.style = 'display: flex; justify-content:
space-between; width: 15em; margin-bottom: .5em;';
+
+ const checkboxContainer =
document.createElement('span');
+ checkboxContainer.style = 'display: flex;';
+
+ const checkbox = document.createElement('input');
+ checkbox.setAttribute('type', 'checkbox');
+ checkbox.setAttribute('data-dojo-type',
'dijit/form/CheckBox');
+ checkbox.setAttribute('id', name + '-checkbox');
+ checkbox.setAttribute('name', name + '-checkbox');
+ checkbox.style = 'cursor: pointer; margin-right:
.5em;';
+ checkboxContainer.appendChild(checkbox);
+
+ const label = document.createElement('label');
+ label.setAttribute('for', name + '-checkbox');
+ label.style = 'cursor: pointer;';
+ label.innerHTML = name;
+ checkboxContainer.appendChild(label);
+
+ const buttonContainer =
document.createElement('span');
+ buttonContainer.style = 'display: flex;
padding-right: .5em;';
+
+ const up = document.createElement('button');
+ up.innerHTML = '▲';
+ up.setAttribute('data-dojo-type',
'dijit/form/Button');
+ up.setAttribute('type', 'button');
+ up.addEventListener('click', (el) =>
moveUp(el.target));
+ up.style = 'width: 1.5em; height: 1.5em;
margin-right: .2em; display: flex; '
+ + 'align-content: center; align-items:
center; justify-content: center;';
+ buttonContainer.appendChild(up);
+
+ const down = document.createElement('button');
+ down.innerHTML = '▼';
+ down.setAttribute('data-dojo-type',
'dijit/form/Button');
+ down.setAttribute('type', 'button');
+ down.addEventListener('click',(el) =>
moveDown(el.target));
+ down.style = 'width: 1.5em; height: 1.5em;
margin-right: .2em; display: flex; '
+ + 'align-content: center;
align-items: center; justify-content: center;';
+ buttonContainer.appendChild(down);
+
+ row.appendChild(checkboxContainer);
+ row.appendChild(buttonContainer);
+
+ authProvidersMultiSelect.appendChild(row);
+ }
+
+ if (that.data?.data?.delegates)
+ {
+ for (const delegate of that.data.data.delegates)
+ {
+ dom.byId(delegate +
'-checkbox').setAttribute('checked', 'checked');
+ }
+ }
+
+ }, util.xhrErrorHandler);
+
+ function moveUp(el) {
+ const row = el.parentNode.parentNode;
+ if (row.previousElementSibling)
+ {
+ row.parentNode.insertBefore(row,
row.previousElementSibling);
+ }
+ }
+
+ function moveDown(el) {
+ const row = el.parentNode.parentNode;
+ if (row.nextElementSibling)
+ {
+ row.parentNode.insertBefore(row.nextElementSibling,
row);
+ }
+ }
+ });
+ },
+ _preSubmit: function(formData)
+ {
+ let result = [];
+ const rows =
document.getElementById('composite.delegates.container').children;
+ for (let i = 0; i < rows.length; i ++)
+ {
+ if (rows.item(i).firstChild?.firstChild?.checked)
+ {
+ result.push(rows.item(i).firstChild?.innerText);
+ }
+ }
+ formData.delegates = result;
+ }
+ };
+});
+
diff --git
a/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/composite/show.js
b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/composite/show.js
new file mode 100644
index 0000000..0d98104
--- /dev/null
+++
b/broker-plugins/management-http/src/main/java/resources/js/qpid/management/authenticationprovider/composite/show.js
@@ -0,0 +1,38 @@
+/*
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ *
+ */
+define(["qpid/common/util", "dojo/domReady!"], function (util)
+{
+
+ function CompositeAuthenticationProvider(data)
+ {
+ util.buildUI(data.containerNode, data.parent,
"authenticationprovider/composite/show.html", ["delegates"], this);
+ }
+
+ CompositeAuthenticationProvider.prototype.update = function (data)
+ {
+ if (this['delegates'])
+ {
+ this['delegates'].innerHTML = data?.delegates?.join(', ');
+ }
+ }
+
+ return CompositeAuthenticationProvider;
+});
diff --git
a/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers-Composite.xml
b/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers-Composite.xml
new file mode 100644
index 0000000..ce09492
--- /dev/null
+++
b/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers-Composite.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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.
+
+-->
+
+<section xmlns="http://docbook.org/ns/docbook" version="5.0"
xml:id="Java-Broker-Security-Composite-Provider">
+ <title>Composite Provider</title>
+
+ <para>Composite Provider uses existing username / password authentication
providers allowing to perform authentication
+ against them in order defined. It can contains following
authentication providers:
+ <itemizedlist>
+ <listitem><para><emphasis><link
linkend="Java-Broker-Security-MD5-Provider">MD5
Provider</link></emphasis></para></listitem>
+ <listitem><para><emphasis><link
linkend="Java-Broker-Security-Plain-Provider">Plain
Provider</link></emphasis></para></listitem>
+ <listitem><para><emphasis><link
linkend="Java-Broker-Security-ScramSha-Providers">SCRAM SHA
Providers</link></emphasis></para></listitem>
+ <listitem><para><emphasis><link
linkend="Java-Broker-Security-LDAP-Provider">Simple LDAP
Providers</link></emphasis></para></listitem>
+ </itemizedlist>
+ </para>
+
+ <para>When performing authentication, composite provider checks presence
of a user with a given username in the first
+ delegate provider and if found, performs authentication. It should be
considered, that in case of name collision
+ (when delegate providers contains users with same username but different
passwords), it's not guaranteed that
+ authentication will succeed even with the correct credentials. Therefore
username collision should be avoided, i.e.
+ each delegate provider should contain users with unique usernames.</para>
+
+ <table pgwide="1">
+ <title>SASL Mechanisms</title>
+ <tgroup cols="2">
+ <colspec colnum="1" colname="authentication_provider"
colwidth="1*"/>
+ <colspec colnum="2" colname="sasl_mechanisms" colwidth="1*"/>
+ <thead>
+ <row>
+ <entry>Authentication provider</entry>
+ <entry>SASL mechanisms</entry>
+ </row>
+ </thead>
+ <tbody>
+ <row
xml:id="Java-Broker-Security-Composite-Provider-Delegate-SASL-Mechanisms-MD5">
+ <entry><link
linkend="Java-Broker-Security-MD5-Provider">MD5 Provider</link></entry>
+ <entry>PLAIN, CRAM-MD5-HASHED, CRAM-MD5-HEX</entry>
+ </row>
+ <row
xml:id="Java-Broker-Security-Composite-Provider-Delegate-SASL-Mechanisms-Plain">
+ <entry><link
linkend="Java-Broker-Security-Plain-Provider">Plain</link></entry>
+ <entry>PLAIN, CRAM-MD5, SCRAM-SHA-1, SCRAM-SHA-256</entry>
+ </row>
+ <row
xml:id="Java-Broker-Security-Composite-Provider-Delegate-SASL-Mechanisms-ScramSha">
+ <entry><link
linkend="Java-Broker-Security-ScramSha-Providers">SCRAM SHA
Providers</link></entry>
+ <entry>PLAIN, SCRAM-SHA-1, SCRAM-SHA-256</entry>
+ </row>
+ <row
xml:id="Java-Broker-Security-Composite-Provider-Delegate-SASL-Mechanisms-Simple-LDAP">
+ <entry><link
linkend="Java-Broker-Security-LDAP-Provider">Simple LDAP
Providers</link></entry>
+ <entry>PLAIN</entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
+
+ <para>Composite provider exposes intersection of SASL mechanism provided
by its delegates.</para>
+
+</section>
diff --git
a/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers.xml
b/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers.xml
index fa733fb..3ca0e2c 100644
---
a/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers.xml
+++
b/doc/java-broker/src/docbkx/security/Java-Broker-Security-Authentication-Providers.xml
@@ -63,4 +63,5 @@
<xi:include xmlns:xi="http://www.w3.org/2001/XInclude"
href="Java-Broker-Security-Authentication-Providers-PlainPasswordFile.xml"/>
<xi:include xmlns:xi="http://www.w3.org/2001/XInclude"
href="Java-Broker-Security-Authentication-Providers-MD5.xml"/>
<xi:include xmlns:xi="http://www.w3.org/2001/XInclude"
href="Java-Broker-Security-Authentication-Providers-Base64MD5PasswordFile.xml"/>
+ <xi:include xmlns:xi="http://www.w3.org/2001/XInclude"
href="Java-Broker-Security-Authentication-Providers-Composite.xml"/>
</section>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]