https://issues.apache.org/jira/browse/AMQ-4849 - runtime modifications to simpleAuthenticationPlugin plugin implemented with test

This commit is contained in:
gtully 2013-11-05 20:37:31 +00:00
parent 4367ec1b82
commit 67a7d30b47
8 changed files with 286 additions and 16 deletions

View File

@ -28,6 +28,9 @@ public class AuthenticationUser {
String password;
String groups;
public AuthenticationUser() {
}
public AuthenticationUser(String username, String password, String groups) {
this.username = username;
this.password = password;

View File

@ -34,8 +34,8 @@ public class SimpleAuthenticationBroker extends AbstractAuthenticationBroker {
private boolean anonymousAccessAllowed = false;
private String anonymousUser;
private String anonymousGroup;
private final Map<String,String> userPasswords;
private final Map<String,Set<Principal>> userGroups;
private Map<String,String> userPasswords;
private Map<String,Set<Principal>> userGroups;
public SimpleAuthenticationBroker(Broker next, Map<String,String> userPasswords, Map<String,Set<Principal>> userGroups) {
super(next);
@ -55,6 +55,14 @@ public class SimpleAuthenticationBroker extends AbstractAuthenticationBroker {
this.anonymousGroup = anonymousGroup;
}
public void setUserPasswords(Map<String,String> value) {
userPasswords = value;
}
public void setUserGroups(Map<String, Set<Principal>> value) {
userGroups = value;
}
@Override
public void addConnection(ConnectionContext context, ConnectionInfo info) throws Exception {

View File

@ -93,14 +93,26 @@ public class SimpleAuthenticationPlugin implements BrokerPlugin {
this.anonymousAccessAllowed = anonymousAccessAllowed;
}
public boolean isAnonymousAccessAllowed() {
return anonymousAccessAllowed;
}
public void setAnonymousUser(String anonymousUser) {
this.anonymousUser = anonymousUser;
}
public String getAnonymousUser() {
return anonymousUser;
}
public void setAnonymousGroup(String anonymousGroup) {
this.anonymousGroup = anonymousGroup;
}
public String getAnonymousGroup() {
return anonymousGroup;
}
/**
* Sets the groups a user is in. The key is the user name and the value is a
* Set of groups

View File

@ -17,7 +17,6 @@
package org.apache.activemq.plugin;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.util.ArrayList;
@ -69,10 +68,12 @@ import org.apache.activemq.broker.region.virtual.VirtualTopic;
import org.apache.activemq.command.ActiveMQDestination;
import org.apache.activemq.command.ActiveMQQueue;
import org.apache.activemq.command.ActiveMQTopic;
import org.apache.activemq.command.ConnectionInfo;
import org.apache.activemq.filter.DestinationMapEntry;
import org.apache.activemq.network.DiscoveryNetworkConnector;
import org.apache.activemq.network.NetworkConnector;
import org.apache.activemq.plugin.jmx.RuntimeConfigurationView;
import org.apache.activemq.schema.core.DtoAuthenticationUser;
import org.apache.activemq.schema.core.DtoAuthorizationEntry;
import org.apache.activemq.schema.core.DtoAuthorizationMap;
import org.apache.activemq.schema.core.DtoAuthorizationPlugin;
@ -83,11 +84,15 @@ import org.apache.activemq.schema.core.DtoNetworkConnector;
import org.apache.activemq.schema.core.DtoPolicyEntry;
import org.apache.activemq.schema.core.DtoPolicyMap;
import org.apache.activemq.schema.core.DtoQueue;
import org.apache.activemq.schema.core.DtoSimpleAuthenticationPlugin;
import org.apache.activemq.schema.core.DtoTopic;
import org.apache.activemq.schema.core.DtoVirtualDestinationInterceptor;
import org.apache.activemq.schema.core.DtoVirtualTopic;
import org.apache.activemq.security.AuthenticationUser;
import org.apache.activemq.security.AuthorizationBroker;
import org.apache.activemq.security.AuthorizationMap;
import org.apache.activemq.security.SimpleAuthenticationBroker;
import org.apache.activemq.security.SimpleAuthenticationPlugin;
import org.apache.activemq.security.TempDestinationAuthorizationEntry;
import org.apache.activemq.security.XBeanAuthorizationEntry;
import org.apache.activemq.security.XBeanAuthorizationMap;
@ -95,9 +100,7 @@ import org.apache.activemq.spring.Utils;
import org.apache.activemq.util.IntrospectionSupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.beans.factory.xml.PluggableSchemaResolver;
import org.springframework.core.io.Resource;
import org.w3c.dom.Document;
@ -112,13 +115,15 @@ public class RuntimeConfigurationBroker extends BrokerFilter {
public static final Logger LOG = LoggerFactory.getLogger(RuntimeConfigurationBroker.class);
public static final String objectNamePropsAppendage = ",service=RuntimeConfiguration,name=Plugin";
private final ReentrantReadWriteLock addDestinationBarrier = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock addConnectionBarrier = new ReentrantReadWriteLock();
PropertiesPlaceHolderUtil placeHolderUtil = null;
private long checkPeriod;
private long lastModified = -1;
private Resource configToMonitor;
private DtoBroker currentConfiguration;
private Runnable monitorTask;
private ConcurrentLinkedQueue<Runnable> destinationInterceptorUpdateWork = new ConcurrentLinkedQueue<Runnable>();
private ConcurrentLinkedQueue<Runnable> addDestinationWork = new ConcurrentLinkedQueue<Runnable>();
private ConcurrentLinkedQueue<Runnable> addConnectionWork = new ConcurrentLinkedQueue<Runnable>();
private ObjectName objectName;
private String infoString;
private Schema schema;
@ -184,13 +189,13 @@ public class RuntimeConfigurationBroker extends BrokerFilter {
// modification to virtual destinations interceptor needs exclusive access to destination add
@Override
public Destination addDestination(ConnectionContext context, ActiveMQDestination destination, boolean createIfTemporary) throws Exception {
Runnable work = destinationInterceptorUpdateWork.poll();
Runnable work = addDestinationWork.poll();
if (work != null) {
try {
addDestinationBarrier.writeLock().lockInterruptibly();
do {
work.run();
work = destinationInterceptorUpdateWork.poll();
work = addDestinationWork.poll();
} while (work != null);
return super.addDestination(context, destination, createIfTemporary);
} finally {
@ -206,6 +211,31 @@ public class RuntimeConfigurationBroker extends BrokerFilter {
}
}
// modification to authentication plugin needs exclusive access to connection add
@Override
public void addConnection(ConnectionContext context, ConnectionInfo info) throws Exception {
Runnable work = addConnectionWork.poll();
if (work != null) {
try {
addConnectionBarrier.writeLock().lockInterruptibly();
do {
work.run();
work = addConnectionWork.poll();
} while (work != null);
super.addConnection(context, info);
} finally {
addConnectionBarrier.writeLock().unlock();
}
} else {
try {
addConnectionBarrier.readLock().lockInterruptibly();
super.addConnection(context, info);
} finally {
addConnectionBarrier.readLock().unlock();
}
}
}
public String updateNow() {
LOG.info("Manual configuration update triggered");
infoString = "";
@ -235,7 +265,7 @@ public class RuntimeConfigurationBroker extends BrokerFilter {
}
private void info(String s) {
LOG.info(s);
LOG.info(filterPasswords(s));
if (infoString != null) {
infoString += s;
infoString += ";";
@ -243,7 +273,7 @@ public class RuntimeConfigurationBroker extends BrokerFilter {
}
private void info(String s, Throwable t) {
LOG.info(s, t);
LOG.info(filterPasswords(s), t);
if (infoString != null) {
infoString += s;
infoString += ", " + t;
@ -255,8 +285,8 @@ public class RuntimeConfigurationBroker extends BrokerFilter {
DtoBroker changed = loadConfiguration(configToMonitor);
if (changed != null && !currentConfiguration.equals(changed)) {
LOG.info("change in " + configToMonitor + " at: " + new Date(lastModified));
LOG.debug("current:" + currentConfiguration);
LOG.debug("new :" + changed);
LOG.debug("current:" + filterPasswords(currentConfiguration));
LOG.debug("new :" + filterPasswords(changed));
processSelectiveChanges(currentConfiguration, changed);
currentConfiguration = changed;
} else {
@ -320,7 +350,7 @@ public class RuntimeConfigurationBroker extends BrokerFilter {
answer.add(val);
}
} catch (NoSuchMethodException mappingIncomplete) {
LOG.debug(o + " has no modifiable elements");
LOG.debug(filterPasswords(o) + " has no modifiable elements");
} catch (Exception e) {
info("Failed to access getContents for " + o + ", runtime modifications not supported", e);
}
@ -360,6 +390,24 @@ public class RuntimeConfigurationBroker extends BrokerFilter {
info("failed to apply modified AuthorizationMap to AuthorizationBroker", e);
}
} else if (candidate instanceof DtoSimpleAuthenticationPlugin) {
try {
final SimpleAuthenticationPlugin updatedPlugin = fromDto(candidate, new SimpleAuthenticationPlugin());
final SimpleAuthenticationBroker authenticationBroker =
(SimpleAuthenticationBroker) getBrokerService().getBroker().getAdaptor(SimpleAuthenticationBroker.class);
addConnectionWork.add(new Runnable() {
public void run() {
authenticationBroker.setUserGroups(updatedPlugin.getUserGroups());
authenticationBroker.setUserPasswords(updatedPlugin.getUserPasswords());
authenticationBroker.setAnonymousAccessAllowed(updatedPlugin.isAnonymousAccessAllowed());
authenticationBroker.setAnonymousUser(updatedPlugin.getAnonymousUser());
authenticationBroker.setAnonymousGroup(updatedPlugin.getAnonymousGroup());
}
});
} catch (Exception e) {
info("failed to apply SimpleAuthenticationPlugin modifications to SimpleAuthenticationBroker", e);
}
} else if (candidate instanceof DtoPolicyMap) {
List<Object> existingEntries = filter(existing, DtoPolicyMap.PolicyEntries.class);
@ -450,7 +498,7 @@ public class RuntimeConfigurationBroker extends BrokerFilter {
}
} else if (o instanceof DtoVirtualDestinationInterceptor) {
// whack it
destinationInterceptorUpdateWork.add(new Runnable() {
addDestinationWork.add(new Runnable() {
public void run() {
List<DestinationInterceptor> interceptorsList = new ArrayList<DestinationInterceptor>();
for (DestinationInterceptor candidate : getBrokerService().getDestinationInterceptors()) {
@ -500,7 +548,7 @@ public class RuntimeConfigurationBroker extends BrokerFilter {
}
} else if (o instanceof DtoVirtualDestinationInterceptor) {
final DtoVirtualDestinationInterceptor dto = (DtoVirtualDestinationInterceptor) o;
destinationInterceptorUpdateWork.add(new Runnable() {
addDestinationWork.add(new Runnable() {
public void run() {
boolean updatedExistingInterceptor = false;
@ -570,7 +618,7 @@ public class RuntimeConfigurationBroker extends BrokerFilter {
Properties properties = new Properties();
IntrospectionSupport.getProperties(dto, properties, null);
replacePlaceHolders(properties);
LOG.trace("applying props: " + properties + ", to " + instance.getClass().getSimpleName());
LOG.trace("applying props: " + filterPasswords(properties) + ", to " + instance.getClass().getSimpleName());
IntrospectionSupport.setProperties(instance, properties);
// deal with nested elements
@ -594,6 +642,11 @@ public class RuntimeConfigurationBroker extends BrokerFilter {
return instance;
}
Pattern matchPassword = Pattern.compile("password=.*,");
private String filterPasswords(Object toEscape) {
return matchPassword.matcher(toEscape.toString()).replaceAll("password=???,");
}
private Object matchType(List<Object> parameterValues, Class<?> aClass) {
Object result = parameterValues;
if (Set.class.isAssignableFrom(aClass)) {
@ -607,6 +660,8 @@ public class RuntimeConfigurationBroker extends BrokerFilter {
return new ActiveMQTopic();
} else if (DtoQueue.class.isAssignableFrom(elementContent.getClass())) {
return new ActiveMQQueue();
} else if (DtoAuthenticationUser.class.isAssignableFrom(elementContent.getClass())) {
return new AuthenticationUser();
} else {
info("update not supported for dto: " + elementContent);
return new Object();

View File

@ -124,5 +124,13 @@
<jxb:property name="Contents" />
</jxb:bindings>
<jxb:bindings node="xs:element[@name='simpleAuthenticationPlugin']/xs:complexType/xs:choice">
<jxb:property name="Contents" />
</jxb:bindings>
<jxb:bindings node="xs:element[@name='simpleAuthenticationPlugin']/xs:complexType/xs:choice/xs:choice/xs:element[@name='users']/xs:complexType/xs:sequence">
<jxb:property name="Contents" />
</jxb:bindings>
</jxb:bindings>
</jxb:bindings>

View File

@ -0,0 +1,71 @@
/**
* 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.activemq;
import javax.jms.JMSException;
import javax.jms.Session;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class AuthenticationTest extends RuntimeConfigTestSupport {
String configurationSeed = "authenticationTest";
@Test
public void testMod() throws Exception {
final String brokerConfig = configurationSeed + "-authentication-broker";
applyNewConfig(brokerConfig, configurationSeed + "-users");
startBroker(brokerConfig);
assertTrue("broker alive", brokerService.isStarted());
assertAllowed("test_user_password", "USERS.A");
assertDenied("another_test_user_password", "USERS.A");
// anonymous
assertDenied(null, "USERS.A");
applyNewConfig(brokerConfig, configurationSeed + "-two-users", SLEEP);
assertAllowed("test_user_password", "USERS.A");
assertAllowed("another_test_user_password", "USERS.A");
assertAllowed(null, "USERS.A");
}
private void assertDenied(String userPass, String destination) {
try {
assertAllowed(userPass, destination);
fail("Expected not allowed exception");
} catch (JMSException expected) {
LOG.debug("got:" + expected, expected);
}
}
private void assertAllowed(String userPass, String dest) throws JMSException {
ActiveMQConnection connection = new ActiveMQConnectionFactory("vm://localhost").createActiveMQConnection(userPass, userPass);
connection.start();
try {
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
session.createConsumer(session.createQueue(dest));
} finally {
connection.close();
}
}
}

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core.xsd">
<broker xmlns="http://activemq.apache.org/schema/core" start="false" persistent="false">
<plugins>
<runtimeConfigurationPlugin checkPeriod="1000"/>
<simpleAuthenticationPlugin anonymousAccessAllowed="true" anonymousGroup="users" anonymousUser="au">
<users>
<authenticationUser groups="users" password="test_user_password" username="test_user_password"/>
<authenticationUser groups="users" password="another_test_user_password" username="another_test_user_password"/>
</users>
</simpleAuthenticationPlugin>
<!-- lets configure a destination based authorization mechanism -->
<authorizationPlugin>
<map>
<authorizationMap>
<authorizationEntries>
<authorizationEntry queue=">" read="admins" write="admins" admin="admins"/>
<authorizationEntry queue="USERS.>" read="users" write="users" admin="users"/>
<authorizationEntry topic=">" read="admins" write="admins" admin="admins"/>
<authorizationEntry topic="USERS.>" read="users" write="users" admin="users"/>
<authorizationEntry topic="ActiveMQ.Advisory.>" read="guests,users" write="guests,users"
admin="guests,users"/>
</authorizationEntries>
<tempDestinationAuthorizationEntry>
<tempDestinationAuthorizationEntry read="tempDestinationAdmins" write="tempDestinationAdmins"
admin="tempDestinationAdmins"/>
</tempDestinationAuthorizationEntry>
</authorizationMap>
</map>
</authorizationPlugin>
</plugins>
</broker>
</beans>

View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://activemq.apache.org/schema/core http://activemq.apache.org/schema/core/activemq-core.xsd">
<broker xmlns="http://activemq.apache.org/schema/core" start="false" persistent="false">
<plugins>
<runtimeConfigurationPlugin checkPeriod="1000"/>
<simpleAuthenticationPlugin anonymousAccessAllowed="false" anonymousGroup="ag" anonymousUser="au"><users><authenticationUser groups="users" password="test_user_password" username="test_user_password"/></users></simpleAuthenticationPlugin>
<!-- lets configure a destination based authorization mechanism -->
<authorizationPlugin>
<map>
<authorizationMap>
<authorizationEntries>
<authorizationEntry queue=">" read="admins" write="admins" admin="admins"/>
<authorizationEntry queue="USERS.>" read="users" write="users" admin="users"/>
<authorizationEntry topic=">" read="admins" write="admins" admin="admins"/>
<authorizationEntry topic="USERS.>" read="users" write="users" admin="users"/>
<authorizationEntry topic="ActiveMQ.Advisory.>" read="guests,users" write="guests,users"
admin="guests,users"/>
</authorizationEntries>
<tempDestinationAuthorizationEntry>
<tempDestinationAuthorizationEntry read="tempDestinationAdmins" write="tempDestinationAdmins"
admin="tempDestinationAdmins"/>
</tempDestinationAuthorizationEntry>
</authorizationMap>
</map>
</authorizationPlugin>
</plugins>
</broker>
</beans>