diff --git a/examples/features/standard/pom.xml b/examples/features/standard/pom.xml index d900cb67bb..c9edfcc2a9 100644 --- a/examples/features/standard/pom.xml +++ b/examples/features/standard/pom.xml @@ -82,6 +82,7 @@ under the License. request-reply scheduled-message security + security-ldap send-acknowledgements spring-integration ssl-enabled @@ -142,7 +143,7 @@ under the License. rest scheduled-message security - security-jaas + security-ldap send-acknowledgements spring-integration diff --git a/examples/features/standard/security-ldap/pom.xml b/examples/features/standard/security-ldap/pom.xml new file mode 100644 index 0000000000..316c9c9779 --- /dev/null +++ b/examples/features/standard/security-ldap/pom.xml @@ -0,0 +1,151 @@ + + + + + 4.0.0 + + + org.apache.activemq.examples.broker + jms-examples + 1.1.1-SNAPSHOT + + + security-ldap + jar + ActiveMQ Artemis JMS Security LDAP Example + + + ${project.basedir}/../../../.. + 2.0.0-M20 + + + + + + org.apache.directory.server + apacheds-parent + ${version.org.apache.ds} + import + pom + + + + + + + org.apache.activemq + artemis-jms-client + ${project.version} + + + org.slf4j + slf4j-simple + 1.7.7 + + + org.apache.directory.server + apacheds-core-annotations + compile + + + org.slf4j + slf4j-log4j12 + + + + + org.apache.directory.api + api-ldap-codec-standalone + compile + + + org.apache.directory.server + apacheds-protocol-ldap + + + + + + + org.apache.activemq + artemis-maven-plugin + + + create + + create + + + ${noServer} + + + + start + + cli + + + ${noServer} + true + tcp://localhost:61616 + myClusterUser + myClusterPassword + + run + + + + + runClient + + runClient + + + org.apache.activemq.artemis.jms.example.SecurityExample + + ${project.build.outputDirectory}/example.ldif + + + + + stop + + cli + + + ${noServer} + + stop + + + + + + + org.apache.activemq.examples.broker + security-ldap + ${project.version} + + + + + + + diff --git a/examples/features/standard/security-ldap/readme.html b/examples/features/standard/security-ldap/readme.html new file mode 100644 index 0000000000..fad945a7c1 --- /dev/null +++ b/examples/features/standard/security-ldap/readme.html @@ -0,0 +1,326 @@ + + + + + ActiveMQ Artemis JMS Security Example + + + + + +

JMS Security Example

+ +
To run the example, simply type mvn verify from this directory, 
or mvn -PnoServer verify if you want to start and create the server manually.
+ + +

This example shows how to configure and use security using ActiveMQ Artemis with LDAP.

+ +

With security properly configured, ActiveMQ Artemis can restrict client access to its resources, including + connection creation, message sending/receiving, etc. This is done by configuring users and roles as well as permissions in + the configuration files.

+ +

ActiveMQ Artemis supports wild-card security configuration. This feature makes security configuration very + flexible and enables fine-grained control over permissions in an efficient way.

+ +

For a full description of how to configure security with ActiveMQ Artemis, please consult the user + manual.

+ +

This example demonstrates how to configure users/roles, how to configure topics with proper permissions using wild-card + expressions, and how they take effects in a simple program.

+ +

First we need to configure users with roles. Users and Roles are configured in activemq-users.xml. This example has four users + configured as below

+ +
+     
+		   <user name="bill" password="activemq">
+		      <role name="user"/>
+		   </user>
+
+		   <user name="andrew" password="activemq1">
+		      <role name="europe-user"/>
+		      <role name="user"/>
+		   </user>
+
+		   <user name="frank" password="activemq2">
+		      <role name="us-user"/>
+		      <role name="news-user"/>
+		      <role name="user"/>
+		   </user>
+
+		   <user name="sam" password="activemq3">
+		      <role name="news-user"/>
+		      <role name="user"/>
+		   </user>
+     
+     
+ +

+ Each user has three properties available: user name, password, and roles it belongs to. It should be noted that + a user can belong to more than one role. In the above configuration, all users belong to role 'user'. User 'andrew' also + belongs to role 'europe-user', user 'frank' also belongs to 'us-user' and 'news-user' and user 'sam' also belongs to 'news-user'. +

+

+ User name and password consists of a valid account that can be used to establish connections to a ActiveMQ Artemis server, while + roles are used in controlling the access privileges against ActiveMQ Artemis topics and queues. You can achieve this control by + configuring proper permissions in broker.xml, like the following +

+

+      <security-settings>
+         <!-- any user can have full control of generic topics -->
+		   <security-setting match="jms.topic.#">
+		      <permission type="createDurableQueue" roles="user"/>
+		      <permission type="deleteDurableQueue" roles="user"/>
+		      <permission type="createNonDurableQueue" roles="user"/>
+		      <permission type="deleteNonDurableQueue" roles="user"/>
+		      <permission type="send" roles="user"/>
+		      <permission type="consume" roles="user"/>
+		   </security-setting>
+
+		   <security-setting match="jms.topic.news.europe.#">
+		      <permission type="createDurableQueue" roles="user"/>
+		      <permission type="deleteDurableQueue" roles="user"/>
+		      <permission type="createNonDurableQueue" roles="user"/>
+		      <permission type="deleteNonDurableQueue" roles="user"/>
+		      <permission type="send" roles="europe-user"/>
+		      <permission type="consume" roles="news-user"/>
+		   </security-setting>
+
+		   <security-setting match="jms.topic.news.us.#">
+		      <permission type="createDurableQueue" roles="user"/>
+		      <permission type="deleteDurableQueue" roles="user"/>
+		      <permission type="createNonDurableQueue" roles="user"/>
+		      <permission type="deleteNonDurableQueue" roles="user"/>
+		      <permission type="send" roles="us-user"/>
+		      <permission type="consume" roles="news-user"/>
+		   </security-setting>
+     </security-settings>
+     
+ +

Permissions can be defined on any group of queues, by using a wildcard. You can easily specify + wildcards to apply certain permissions to a set of matching queues and topics. In the above configuration + we have created four sets of permissions, each set matches against a special group of targets, indicated by wild-card match attributes.

+ +

You can provide a very broad permission control as a default and then add more strict control + over specific addresses. By the above we define the following access rules:

+ +
  • Only role 'us-user' can create/delete and pulish messages to topics whose names match wild-card pattern 'news.us.#'.
  • +
  • Only role 'europe-user' can create/delete and publish messages to topics whose names match wild-card pattern 'news.europe.#'.
  • +
  • Only role 'news-user' can subscribe messages to topics whose names match wild-card pattern 'news.us.#' and 'news.europe.#'.
  • +
  • For any other topics that don't match any of the above wild-card patterns, permissions are granted to users of role 'user'.
  • + +

    To illustrate the effect of permissions, three topics are deployed. Topic 'genericTopic' matches 'jms.topic.#' wild-card, topic 'news.europe.europeTopic' matches + jms.topic.news.europe.#' wild-cards, and topic 'news.us.usTopic' matches 'jms.topic.news.us.#'.

    + +

    With ActiveMQ Artemis, the security manager is also configurable. You can use JAASSecurityManager or JBossASSecurityManager based on you need. Please + check out the activemq-beans.xml for how to do. In this example we just use the basic ActiveMQSecurityManagerImpl which reads users/roles/passwords from the xml + file activemq-users.xml. + + +

    Example step-by-step

    +

    To run the example, simply type mvn verify -Pexample from this directory

    + +
      +
    1. First we need to get an initial context so we can look-up the JMS connection factory and destination objects from JNDI. This initial context will get it's properties from the client-jndi.properties file in the directory ../common/config
    2. +
      +           
      +           InitialContext initialContext = getContext(0);
      +           
      +        
      + +
    3. We perform lookup on the topics
    4. +
      +           
      +           Topic genericTopic = (Topic) initialContext.lookup("/topic/genericTopic");
      +           Topic europeTopic = (Topic) initialContext.lookup("/topic/europeTopic");
      +           Topic usTopic = (Topic) initialContext.lookup("/topic/usTopic");
      +           
      +        
      + +
    5. We perform a lookup on the Connection Factory
    6. +
      +           
      +           ConnectionFactory cf = (ConnectionFactory) initialContext.lookup("/ConnectionFactory");
      +           
      +        
      + +
    7. We try to create a JMS Connection without user/password. It will fail.
    8. +
      +           
      +           try
      +           {
      +              cf.createConnection();
      +              result = false;
      +           }
      +           catch (JMSSecurityException e)
      +           {
      +              System.out.println("Default user cannot get a connection. Details: " + e.getMessage());
      +           }
      +           
      +        
      + +
    9. Bill tries to make a connection using wrong password
    10. +
      +           
      +           billConnection = null;
      +           try
      +           {
      +              billConnection = createConnection("bill", "activemq1", cf);
      +              result = false;
      +           }
      +           catch (JMSException e)
      +           {
      +              System.out.println("User bill failed to connect. Details: " + e.getMessage());
      +           }
      +           
      +        
      + +
    11. Bill makes a good connection.
    12. +
      +          
      +           billConnection = createConnection("bill", "activemq", cf);
      +           billConnection.start();
      +          
      +       
      + +
    13. Andrew makes a good connection
    14. +
      +           
      +           andrewConnection = createConnection("andrew", "activemq1", cf);
      +           andrewConnection.start();
      +           
      +         
      + +
    15. Frank makes a good connection
    16. +
      +           
      +           frankConnection = createConnection("frank", "activemq2", cf);
      +           frankConnection.start();
      +           
      +        
      + +
    17. Sam makes a good connection
    18. +
      +           
      +           samConnection = createConnection("sam", "activemq3", cf);
      +           samConnection.start();
      +           
      +        
      + +
    19. We check every user can publish/subscribe genericTopics
    20. +
      +           
      +           checkUserSendAndReceive(genericTopic, billConnection, "bill");
      +           checkUserSendAndReceive(genericTopic, andrewConnection, "andrew");
      +           checkUserSendAndReceive(genericTopic, frankConnection, "frank");
      +           checkUserSendAndReceive(genericTopic, samConnection, "sam");
      +           
      +        
      + +
    21. We check permissions on news.europe.europeTopic for bill: can't send and can't receive
    22. +
      +           
      +           checkUserNoSendNoReceive(europeTopic, billConnection, "bill", andrewConnection, frankConnection);
      +           
      +        
      + +
    23. We check permissions on news.europe.europeTopic for andrew: can send but can't receive
    24. +
      +           
      +           checkUserSendNoReceive(europeTopic, andrewConnection, "andrew", frankConnection);
      +           
      +        
      + +
    25. We check permissions on news.europe.europeTopic for frank: can't send but can receive
    26. +
      +           
      +           checkUserReceiveNoSend(europeTopic, frankConnection, "frank", andrewConnection);
      +           
      +        
      + +
    27. We check permissions on news.europe.europeTopic for sam: can't send but can receive
    28. +
      +           
      +           checkUserReceiveNoSend(europeTopic, samConnection, "sam", andrewConnection);
      +           
      +        
      + +
    29. We check permissions on news.us.usTopic for bill: can't send and can't receive
    30. +
      +           
      +           checkUserNoSendNoReceive(usTopic, billConnection, "bill");
      +           
      +        
      + +
    31. We check permissions on news.us.usTopic for andrew: can't send and can't receive
    32. +
      +           
      +           checkUserNoSendNoReceive(usTopic, andrewConnection, "andrew");
      +           
      +        
      + +
    33. We check permissions on news.us.usTopic for frank: can both send and receive
    34. +
      +           
      +           checkUserSendAndReceive(usTopic, frankConnection, "frank");
      +           
      +        
      + +
    35. We check permissions on news.us.usTopic for sam: can't send but can receive
    36. +
      +           
      +           checkUserReceiveNoSend(usTopic, samConnection, "sam", frankConnection);
      +           
      +        
      + +
    37. And finally, always remember to close your JMS connections and resources after use, in a finally block. Closing a JMS connection will automatically close all of its sessions, consumers, producer and browser objects
    38. + +
      +           
      +           finally
      +           {
      +              if (billConnection != null)
      +              {
      +                 billConnection.close();
      +              }
      +              if (andrewConnection != null)
      +              {
      +                 andrewConnection.close();
      +              }
      +              if (frankConnection != null)
      +              {
      +                 frankConnection.close();
      +              }
      +              if (samConnection != null)
      +              {
      +                 samConnection.close();
      +              }
      +
      +              // Also the initialContext
      +              if (initialContext != null)
      +              {
      +                 initialContext.close();
      +              }
      +           }
      +           
      +        
      +
    + + diff --git a/examples/features/standard/security-ldap/src/main/java/org/apache/activemq/artemis/jms/example/SecurityExample.java b/examples/features/standard/security-ldap/src/main/java/org/apache/activemq/artemis/jms/example/SecurityExample.java new file mode 100644 index 0000000000..94f4b43477 --- /dev/null +++ b/examples/features/standard/security-ldap/src/main/java/org/apache/activemq/artemis/jms/example/SecurityExample.java @@ -0,0 +1,288 @@ +/* + * 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.artemis.jms.example; + +import javax.jms.Connection; +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import javax.jms.JMSSecurityException; +import javax.jms.MessageConsumer; +import javax.jms.MessageProducer; +import javax.jms.Session; +import javax.jms.TextMessage; +import javax.jms.Topic; +import javax.naming.InitialContext; + +import org.apache.activemq.artemis.jms.example.ldap.LdapServer; + +public class SecurityExample { + + public static void main(final String[] args) throws Exception { + LdapServer ldapServer = new LdapServer(args[0]); + + boolean result = true; + Connection failConnection = null; + Connection billConnection = null; + Connection andrewConnection = null; + Connection frankConnection = null; + Connection samConnection = null; + + InitialContext initialContext = null; + try { + // /Step 1. Create an initial context to perform the JNDI lookup. + initialContext = new InitialContext(); + + // Step 2. perform lookup on the topics + Topic genericTopic = (Topic) initialContext.lookup("topic/genericTopic"); + Topic europeTopic = (Topic) initialContext.lookup("topic/europeTopic"); + Topic usTopic = (Topic) initialContext.lookup("topic/usTopic"); + + // Step 3. perform a lookup on the Connection Factory + ConnectionFactory cf = (ConnectionFactory) initialContext.lookup("ConnectionFactory"); + + // Step 4. Try to create a JMS Connection without user/password. It will fail. + try { + failConnection = cf.createConnection(); + result = false; + } + catch (JMSSecurityException e) { + System.out.println("Default user cannot get a connection. Details: " + e.getMessage()); + } + + // Step 5. bill tries to make a connection using wrong password + billConnection = null; + try { + billConnection = createConnection("bill", "activemq1", cf); + result = false; + } + catch (JMSException e) { + System.out.println("User bill failed to connect. Details: " + e.getMessage()); + } + + // Step 6. bill makes a good connection. + billConnection = createConnection("bill", "activemq", cf); + billConnection.start(); + + // Step 7. andrew makes a good connection. + andrewConnection = createConnection("andrew", "activemq1", cf); + andrewConnection.start(); + + // Step 8. frank makes a good connection. + frankConnection = createConnection("frank", "activemq2", cf); + frankConnection.start(); + + // Step 9. sam makes a good connection. + samConnection = createConnection("sam", "activemq3", cf); + samConnection.start(); + + // Step 10. Check every user can publish/subscribe genericTopics. + System.out.println("------------------------Checking permissions on " + genericTopic + "----------------"); + checkUserSendAndReceive(genericTopic, billConnection, "bill"); + checkUserSendAndReceive(genericTopic, andrewConnection, "andrew"); + checkUserSendAndReceive(genericTopic, frankConnection, "frank"); + checkUserSendAndReceive(genericTopic, samConnection, "sam"); + System.out.println("-------------------------------------------------------------------------------------"); + + System.out.println("------------------------Checking permissions on " + europeTopic + "----------------"); + + // Step 11. Check permissions on news.europe.europeTopic for bill: can't send and can't receive + checkUserNoSendNoReceive(europeTopic, billConnection, "bill"); + + // Step 12. Check permissions on news.europe.europeTopic for andrew: can send but can't receive + checkUserSendNoReceive(europeTopic, andrewConnection, "andrew", frankConnection); + + // Step 13. Check permissions on news.europe.europeTopic for frank: can't send but can receive + checkUserReceiveNoSend(europeTopic, frankConnection, "frank", andrewConnection); + + // Step 14. Check permissions on news.europe.europeTopic for sam: can't send but can receive + checkUserReceiveNoSend(europeTopic, samConnection, "sam", andrewConnection); + System.out.println("-------------------------------------------------------------------------------------"); + + System.out.println("------------------------Checking permissions on " + usTopic + "----------------"); + + // Step 15. Check permissions on news.us.usTopic for bill: can't send and can't receive + checkUserNoSendNoReceive(usTopic, billConnection, "bill"); + + // Step 16. Check permissions on news.us.usTopic for andrew: can't send and can't receive + checkUserNoSendNoReceive(usTopic, andrewConnection, "andrew"); + + // Step 17. Check permissions on news.us.usTopic for frank: can both send and receive + checkUserSendAndReceive(usTopic, frankConnection, "frank"); + + // Step 18. Check permissions on news.us.usTopic for sam: can't send but can receive + checkUserReceiveNoSend(usTopic, samConnection, "sam", frankConnection); + System.out.println("-------------------------------------------------------------------------------------"); + } + finally { + // Step 19. Be sure to close our JMS resources! + if (failConnection != null) { + failConnection.close(); + } + if (billConnection != null) { + billConnection.close(); + } + if (andrewConnection != null) { + andrewConnection.close(); + } + if (frankConnection != null) { + frankConnection.close(); + } + if (samConnection != null) { + samConnection.close(); + } + + // Also the initialContext + if (initialContext != null) { + initialContext.close(); + } + + ldapServer.stop(); + } + } + + // Check the user can receive message but cannot send message. + private static void checkUserReceiveNoSend(final Topic topic, + final Connection connection, + final String user, + final Connection sendingConn) throws JMSException { + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + MessageProducer producer = session.createProducer(topic); + MessageConsumer consumer = session.createConsumer(topic); + TextMessage msg = session.createTextMessage("hello-world-1"); + + try { + producer.send(msg); + throw new IllegalStateException("Security setting is broken! User " + user + + " can send message [" + + msg.getText() + + "] to topic " + + topic); + } + catch (JMSException e) { + System.out.println("User " + user + " cannot send message [" + msg.getText() + "] to topic: " + topic); + } + + // Now send a good message + Session session1 = sendingConn.createSession(false, Session.AUTO_ACKNOWLEDGE); + producer = session1.createProducer(topic); + producer.send(msg); + + TextMessage receivedMsg = (TextMessage) consumer.receive(2000); + + if (receivedMsg != null) { + System.out.println("User " + user + " can receive message [" + receivedMsg.getText() + "] from topic " + topic); + } + else { + throw new IllegalStateException("Security setting is broken! User " + user + " cannot receive message from topic " + topic); + } + + session1.close(); + session.close(); + } + + // Check the user can send message but cannot receive message + private static void checkUserSendNoReceive(final Topic topic, + final Connection connection, + final String user, + final Connection receivingConn) throws JMSException { + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + MessageProducer producer = session.createProducer(topic); + try { + session.createConsumer(topic); + } + catch (JMSException e) { + System.out.println("User " + user + " cannot receive any message from topic " + topic); + } + + Session session1 = receivingConn.createSession(false, Session.AUTO_ACKNOWLEDGE); + MessageConsumer goodConsumer = session1.createConsumer(topic); + + TextMessage msg = session.createTextMessage("hello-world-2"); + producer.send(msg); + + TextMessage receivedMsg = (TextMessage) goodConsumer.receive(2000); + if (receivedMsg != null) { + System.out.println("User " + user + " can send message [" + receivedMsg.getText() + "] to topic " + topic); + } + else { + throw new IllegalStateException("Security setting is broken! User " + user + + " cannot send message [" + + msg.getText() + + "] to topic " + + topic); + } + + session.close(); + session1.close(); + } + + // Check the user has neither send nor receive permission on topic + private static void checkUserNoSendNoReceive(final Topic topic, + final Connection connection, + final String user) throws JMSException { + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + MessageProducer producer = session.createProducer(topic); + + try { + session.createConsumer(topic); + } + catch (JMSException e) { + System.out.println("User " + user + " cannot create consumer on topic " + topic); + } + + TextMessage msg = session.createTextMessage("hello-world-3"); + try { + producer.send(msg); + throw new IllegalStateException("Security setting is broken! User " + user + + " can send message [" + + msg.getText() + + "] to topic " + + topic); + } + catch (JMSException e) { + System.out.println("User " + user + " cannot send message [" + msg.getText() + "] to topic: " + topic); + } + + session.close(); + } + + // Check the user connection has both send and receive permissions on the topic + private static void checkUserSendAndReceive(final Topic topic, + final Connection connection, + final String user) throws JMSException { + Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); + TextMessage msg = session.createTextMessage("hello-world-4"); + MessageProducer producer = session.createProducer(topic); + MessageConsumer consumer = session.createConsumer(topic); + producer.send(msg); + TextMessage receivedMsg = (TextMessage) consumer.receive(5000); + if (receivedMsg != null) { + System.out.println("User " + user + " can send message: [" + msg.getText() + "] to topic: " + topic); + System.out.println("User " + user + " can receive message: [" + msg.getText() + "] from topic: " + topic); + } + else { + throw new IllegalStateException("Error! User " + user + " cannot receive the message! "); + } + session.close(); + } + + private static Connection createConnection(final String username, + final String password, + final ConnectionFactory cf) throws JMSException { + return cf.createConnection(username, password); + } +} diff --git a/examples/features/standard/security-ldap/src/main/java/org/apache/activemq/artemis/jms/example/ldap/InMemoryDirectoryServiceFactory.java b/examples/features/standard/security-ldap/src/main/java/org/apache/activemq/artemis/jms/example/ldap/InMemoryDirectoryServiceFactory.java new file mode 100644 index 0000000000..61bf77eee5 --- /dev/null +++ b/examples/features/standard/security-ldap/src/main/java/org/apache/activemq/artemis/jms/example/ldap/InMemoryDirectoryServiceFactory.java @@ -0,0 +1,168 @@ +/* + * 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.artemis.jms.example.ldap; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.config.CacheConfiguration; +import net.sf.ehcache.config.Configuration; + +import org.apache.commons.io.FileUtils; +import org.apache.directory.api.ldap.model.constants.SchemaConstants; +import org.apache.directory.api.ldap.model.schema.LdapComparator; +import org.apache.directory.api.ldap.model.schema.SchemaManager; +import org.apache.directory.api.ldap.model.schema.comparators.NormalizingComparator; +import org.apache.directory.api.ldap.model.schema.registries.ComparatorRegistry; +import org.apache.directory.api.ldap.model.schema.registries.SchemaLoader; +import org.apache.directory.api.ldap.schema.loader.JarLdifSchemaLoader; +import org.apache.directory.api.ldap.schema.manager.impl.DefaultSchemaManager; +import org.apache.directory.api.util.exception.Exceptions; +import org.apache.directory.server.constants.ServerDNConstants; +import org.apache.directory.server.core.DefaultDirectoryService; +import org.apache.directory.server.core.api.CacheService; +import org.apache.directory.server.core.api.DirectoryService; +import org.apache.directory.server.core.api.InstanceLayout; +import org.apache.directory.server.core.api.partition.Partition; +import org.apache.directory.server.core.api.schema.SchemaPartition; +import org.apache.directory.server.core.factory.AvlPartitionFactory; +import org.apache.directory.server.core.factory.DirectoryServiceFactory; +import org.apache.directory.server.core.factory.PartitionFactory; +import org.apache.directory.server.i18n.I18n; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Factory for a fast (mostly in-memory-only) ApacheDS DirectoryService. Use only for tests!! + */ +public class InMemoryDirectoryServiceFactory implements DirectoryServiceFactory { + + private static Logger LOG = LoggerFactory.getLogger(InMemoryDirectoryServiceFactory.class); + + private final DirectoryService directoryService; + private final PartitionFactory partitionFactory; + + /** + * Default constructor which creates {@link DefaultDirectoryService} instance and configures {@link AvlPartitionFactory} as + * the {@link PartitionFactory} implementation. + */ + public InMemoryDirectoryServiceFactory() { + try { + directoryService = new DefaultDirectoryService(); + } + catch (Exception e) { + throw new RuntimeException(e); + } + directoryService.setShutdownHookEnabled(false); + partitionFactory = new AvlPartitionFactory(); + } + + /** + * Constructor which uses provided {@link DirectoryService} and {@link PartitionFactory} implementations. + * + * @param directoryService must be not-null + * @param partitionFactory must be not-null + */ + public InMemoryDirectoryServiceFactory(DirectoryService directoryService, PartitionFactory partitionFactory) { + this.directoryService = directoryService; + this.partitionFactory = partitionFactory; + } + + /** + * {@inheritDoc} + */ + public void init(String name) throws Exception { + if ((directoryService != null) && directoryService.isStarted()) { + return; + } + + directoryService.setInstanceId(name); + + // instance layout + InstanceLayout instanceLayout = new InstanceLayout(System.getProperty("java.io.tmpdir") + "/server-work-" + name); + if (instanceLayout.getInstanceDirectory().exists()) { + try { + FileUtils.deleteDirectory(instanceLayout.getInstanceDirectory()); + } + catch (IOException e) { + LOG.warn("couldn't delete the instance directory before initializing the DirectoryService", e); + } + } + directoryService.setInstanceLayout(instanceLayout); + + // EhCache in disabled-like-mode + Configuration ehCacheConfig = new Configuration(); + CacheConfiguration defaultCache = new CacheConfiguration("default", 1) + .eternal(false) + .timeToIdleSeconds(30) + .timeToLiveSeconds(30) + .overflowToDisk(false); + ehCacheConfig.addDefaultCache(defaultCache); + CacheService cacheService = new CacheService(new CacheManager(ehCacheConfig)); + directoryService.setCacheService(cacheService); + + // Init the schema + // SchemaLoader loader = new SingleLdifSchemaLoader(); + SchemaLoader loader = new JarLdifSchemaLoader(); + SchemaManager schemaManager = new DefaultSchemaManager(loader); + schemaManager.loadAllEnabled(); + ComparatorRegistry comparatorRegistry = schemaManager.getComparatorRegistry(); + for (LdapComparator comparator : comparatorRegistry) { + if (comparator instanceof NormalizingComparator) { + ((NormalizingComparator) comparator).setOnServer(); + } + } + directoryService.setSchemaManager(schemaManager); + InMemorySchemaPartition inMemorySchemaPartition = new InMemorySchemaPartition(schemaManager); + + SchemaPartition schemaPartition = new SchemaPartition(schemaManager); + schemaPartition.setWrappedPartition(inMemorySchemaPartition); + directoryService.setSchemaPartition(schemaPartition); + List errors = schemaManager.getErrors(); + if (errors.size() != 0) { + throw new Exception(I18n.err(I18n.ERR_317, Exceptions.printErrors(errors))); + } + + // Init system partition + Partition systemPartition = partitionFactory.createPartition(directoryService.getSchemaManager(), directoryService + .getDnFactory(), "system", ServerDNConstants.SYSTEM_DN, 500, new File(directoryService + .getInstanceLayout() + .getPartitionsDirectory(), "system")); + systemPartition.setSchemaManager(directoryService.getSchemaManager()); + partitionFactory.addIndex(systemPartition, SchemaConstants.OBJECT_CLASS_AT, 100); + directoryService.setSystemPartition(systemPartition); + + directoryService.startup(); + } + + /** + * {@inheritDoc} + */ + public DirectoryService getDirectoryService() throws Exception { + return directoryService; + } + + /** + * {@inheritDoc} + */ + public PartitionFactory getPartitionFactory() throws Exception { + return partitionFactory; + } + +} diff --git a/examples/features/standard/security-ldap/src/main/java/org/apache/activemq/artemis/jms/example/ldap/InMemorySchemaPartition.java b/examples/features/standard/security-ldap/src/main/java/org/apache/activemq/artemis/jms/example/ldap/InMemorySchemaPartition.java new file mode 100644 index 0000000000..b7bb1f7a67 --- /dev/null +++ b/examples/features/standard/security-ldap/src/main/java/org/apache/activemq/artemis/jms/example/ldap/InMemorySchemaPartition.java @@ -0,0 +1,95 @@ +/* + * 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.artemis.jms.example.ldap; + +import java.net.URL; +import java.util.Map; +import java.util.TreeSet; +import java.util.UUID; +import java.util.regex.Pattern; + +import javax.naming.InvalidNameException; + +import org.apache.directory.api.ldap.model.constants.SchemaConstants; +import org.apache.directory.api.ldap.model.entry.DefaultEntry; +import org.apache.directory.api.ldap.model.entry.Entry; +import org.apache.directory.api.ldap.model.ldif.LdifEntry; +import org.apache.directory.api.ldap.model.ldif.LdifReader; +import org.apache.directory.api.ldap.model.schema.SchemaManager; +import org.apache.directory.api.ldap.schema.extractor.impl.DefaultSchemaLdifExtractor; +import org.apache.directory.api.ldap.schema.extractor.impl.ResourceMap; +import org.apache.directory.server.core.api.interceptor.context.AddOperationContext; +import org.apache.directory.server.core.partition.ldif.AbstractLdifPartition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * In-memory schema-only partition which loads the data in the similar way as the + * {@link org.apache.directory.api.ldap.schema.loader.JarLdifSchemaLoader}. + */ +public class InMemorySchemaPartition extends AbstractLdifPartition { + + private static Logger LOG = LoggerFactory.getLogger(InMemorySchemaPartition.class); + + /** + * Filesystem path separator pattern, either forward slash or backslash. java.util.regex.Pattern is immutable so only one + * instance is needed for all uses. + */ + + public InMemorySchemaPartition(SchemaManager schemaManager) { + super(schemaManager); + } + + /** + * Partition initialization - loads schema entries from the files on classpath. + * + * @see org.apache.directory.server.core.partition.impl.avl.AvlPartition#doInit() + */ + @Override + protected void doInit() throws InvalidNameException, Exception { + if (initialized) { + return; + } + + LOG.debug("Initializing schema partition " + getId()); + suffixDn.apply(schemaManager); + super.doInit(); + + // load schema + final Map resMap = ResourceMap.getResources(Pattern.compile("schema[/\\Q\\\\E]ou=schema.*")); + for (String resourcePath : new TreeSet(resMap.keySet())) { + if (resourcePath.endsWith(".ldif")) { + URL resource = DefaultSchemaLdifExtractor.getUniqueResource(resourcePath, "Schema LDIF file"); + LdifReader reader = new LdifReader(resource.openStream()); + LdifEntry ldifEntry = reader.next(); + reader.close(); + + Entry entry = new DefaultEntry(schemaManager, ldifEntry.getEntry()); + // add mandatory attributes + if (entry.get(SchemaConstants.ENTRY_CSN_AT) == null) { + entry.add(SchemaConstants.ENTRY_CSN_AT, defaultCSNFactory.newInstance().toString()); + } + if (entry.get(SchemaConstants.ENTRY_UUID_AT) == null) { + entry.add(SchemaConstants.ENTRY_UUID_AT, UUID.randomUUID().toString()); + } + AddOperationContext addContext = new AddOperationContext(null, entry); + super.add(addContext); + } + } + } + +} diff --git a/examples/features/standard/security-ldap/src/main/java/org/apache/activemq/artemis/jms/example/ldap/LdapServer.java b/examples/features/standard/security-ldap/src/main/java/org/apache/activemq/artemis/jms/example/ldap/LdapServer.java new file mode 100644 index 0000000000..1161922b72 --- /dev/null +++ b/examples/features/standard/security-ldap/src/main/java/org/apache/activemq/artemis/jms/example/ldap/LdapServer.java @@ -0,0 +1,102 @@ +/* + * 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.artemis.jms.example.ldap; + +import java.io.IOException; + +import org.apache.directory.api.ldap.model.entry.DefaultEntry; +import org.apache.directory.api.ldap.model.ldif.LdifEntry; +import org.apache.directory.api.ldap.model.ldif.LdifReader; +import org.apache.directory.api.ldap.model.name.Dn; +import org.apache.directory.api.ldap.model.schema.SchemaManager; +import org.apache.directory.server.core.api.DirectoryService; +import org.apache.directory.server.core.partition.impl.avl.AvlPartition; +import org.apache.directory.server.protocol.shared.transport.TcpTransport; + +/** + * Creates and starts LDAP server(s). + */ +public class LdapServer { + + private final DirectoryService directoryService; + private final org.apache.directory.server.ldap.LdapServer ldapServer; + + /** + * Create a single LDAP server. + * + * @param ldifFile + * + * @throws Exception + */ + public LdapServer(String ldifFile) throws Exception { + InMemoryDirectoryServiceFactory dsFactory = new InMemoryDirectoryServiceFactory(); + dsFactory.init("ds"); + + directoryService = dsFactory.getDirectoryService(); + + final SchemaManager schemaManager = directoryService.getSchemaManager(); + importLdif(directoryService, schemaManager, new LdifReader(ldifFile)); + + ldapServer = new org.apache.directory.server.ldap.LdapServer(); + ldapServer.setTransports(new TcpTransport("127.0.0.1", 1024)); + ldapServer.setDirectoryService(directoryService); + + ldapServer.start(); + } + + /** + * Stops LDAP server and the underlying directory service. + * + * @throws Exception + */ + public void stop() throws Exception { + ldapServer.stop(); + directoryService.shutdown(); + } + + private void importLdif(DirectoryService directoryService, final SchemaManager schemaManager, LdifReader ldifReader) throws Exception { + try { + for (LdifEntry ldifEntry : ldifReader) { + checkPartition(ldifEntry); + directoryService.getAdminSession().add(new DefaultEntry(schemaManager, ldifEntry.getEntry())); + } + } + finally { + try { + ldifReader.close(); + } + catch (IOException ioe) { + // ignore + } + } + } + + private void checkPartition(LdifEntry ldifEntry) throws Exception { + Dn dn = ldifEntry.getDn(); + Dn parent = dn.getParent(); + try { + directoryService.getAdminSession().exists(parent); + } + catch (Exception e) { + System.out.println("Creating new partition for DN=" + dn + "\n"); + AvlPartition partition = new AvlPartition(directoryService.getSchemaManager()); + partition.setId(dn.getName()); + partition.setSuffixDn(dn); + directoryService.addPartition(partition); + } + } +} \ No newline at end of file diff --git a/examples/features/standard/security-ldap/src/main/resources/activemq/server0/artemis-roles.properties b/examples/features/standard/security-ldap/src/main/resources/activemq/server0/artemis-roles.properties new file mode 100644 index 0000000000..71e672d40b --- /dev/null +++ b/examples/features/standard/security-ldap/src/main/resources/activemq/server0/artemis-roles.properties @@ -0,0 +1,20 @@ +## --------------------------------------------------------------------------- +## 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. +## --------------------------------------------------------------------------- +user=bill,andrew,frank,sam +europe-user=andrew +news-user=frank,sam +us-user=frank \ No newline at end of file diff --git a/examples/features/standard/security-ldap/src/main/resources/activemq/server0/artemis-users.properties b/examples/features/standard/security-ldap/src/main/resources/activemq/server0/artemis-users.properties new file mode 100644 index 0000000000..0a206c6053 --- /dev/null +++ b/examples/features/standard/security-ldap/src/main/resources/activemq/server0/artemis-users.properties @@ -0,0 +1,20 @@ +## --------------------------------------------------------------------------- +## 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. +## --------------------------------------------------------------------------- +bill=activemq +andrew=activemq1 +frank=activemq2 +sam=activemq3 \ No newline at end of file diff --git a/examples/features/standard/security-ldap/src/main/resources/activemq/server0/broker.xml b/examples/features/standard/security-ldap/src/main/resources/activemq/server0/broker.xml new file mode 100644 index 0000000000..667ac955b4 --- /dev/null +++ b/examples/features/standard/security-ldap/src/main/resources/activemq/server0/broker.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + ./data/messaging/bindings + + ./data/messaging/journal + + ./data/messaging/largemessages + + ./data/messaging/paging + + myClusterUser + myClusterPassword + + + + tcp://localhost:61616 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/features/standard/security-ldap/src/main/resources/activemq/server0/login.config b/examples/features/standard/security-ldap/src/main/resources/activemq/server0/login.config new file mode 100644 index 0000000000..90fb445636 --- /dev/null +++ b/examples/features/standard/security-ldap/src/main/resources/activemq/server0/login.config @@ -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. + */ + +activemq { + org.apache.activemq.artemis.spi.core.security.jaas.LDAPLoginModule required + debug=true + initialContextFactory=com.sun.jndi.ldap.LdapCtxFactory + connectionURL="ldap://localhost:1024" + connectionUsername="uid=admin,ou=system" + connectionPassword=secret + connectionProtocol=s + authentication=simple + userBase="dc=activemq,dc=org" + userSearchMatching="(uid={0})" + userSearchSubtree=false + roleBase="dc=activemq,dc=org" + roleName=cn + roleSearchMatching="(member=uid={1},dc=activemq,dc=org)" + roleSearchSubtree=false + ; +}; \ No newline at end of file diff --git a/examples/features/standard/security-ldap/src/main/resources/example.ldif b/examples/features/standard/security-ldap/src/main/resources/example.ldif new file mode 100644 index 0000000000..da135dadc6 --- /dev/null +++ b/examples/features/standard/security-ldap/src/main/resources/example.ldif @@ -0,0 +1,81 @@ +## --------------------------------------------------------------------------- +## 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. +## --------------------------------------------------------------------------- + +dn: dc=activemq,dc=org +dc: activemq +objectClass: top +objectClass: domain + +dn: uid=bill,dc=activemq,dc=org +uid: bill +userPassword: activemq +objectClass: account +objectClass: simpleSecurityObject +objectClass: top + +dn: uid=andrew,dc=activemq,dc=org +uid: andrew +userPassword: activemq1 +objectClass: account +objectClass: simpleSecurityObject +objectClass: top + +dn: uid=frank,dc=activemq,dc=org +uid: frank +userPassword: activemq2 +objectClass: account +objectClass: simpleSecurityObject +objectClass: top + +dn: uid=sam,dc=activemq,dc=org +uid: sam +userPassword: activemq3 +objectClass: account +objectClass: simpleSecurityObject +objectClass: top + +################### +## Define groups ## +################### + +dn: cn=user,dc=activemq,dc=org +cn: user +member: uid=bill,dc=activemq,dc=org +member: uid=andrew,dc=activemq,dc=org +member: uid=frank,dc=activemq,dc=org +member: uid=sam,dc=activemq,dc=org +objectClass: groupOfNames +objectClass: top + +dn: cn=europe-user,dc=activemq,dc=org +cn: europe-user +member: uid=andrew,dc=activemq,dc=org +objectClass: groupOfNames +objectClass: top + +dn: cn=news-user,dc=activemq,dc=org +cn: news-user +member: uid=frank,dc=activemq,dc=org +member: uid=sam,dc=activemq,dc=org +objectClass: groupOfNames +objectClass: top + +dn: cn=us-user,dc=activemq,dc=org +cn: us-user +member: uid=frank,dc=activemq,dc=org +objectClass: groupOfNames +objectClass: top \ No newline at end of file diff --git a/examples/features/standard/security-ldap/src/main/resources/jndi.properties b/examples/features/standard/security-ldap/src/main/resources/jndi.properties new file mode 100644 index 0000000000..0a3b640261 --- /dev/null +++ b/examples/features/standard/security-ldap/src/main/resources/jndi.properties @@ -0,0 +1,22 @@ +# 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. + +java.naming.factory.initial=org.apache.activemq.artemis.jndi.ActiveMQInitialContextFactory +connectionFactory.ConnectionFactory=tcp://localhost:61616 +topic.topic/genericTopic=genericTopic +topic.topic/europeTopic=news.europe.europeTopic +topic.topic/usTopic=news.us.usTopic