Skip to content

Commit

Permalink
ARTEMIS-1884 add plugin API for message level authorization policies
Browse files Browse the repository at this point in the history
  • Loading branch information
ryeats committed Dec 9, 2020
1 parent 4162eff commit 5ec9a9d
Show file tree
Hide file tree
Showing 16 changed files with 921 additions and 0 deletions.
Expand Up @@ -16,6 +16,7 @@
*/
package org.apache.activemq.artemis.core.security;

import javax.security.auth.Subject;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;

Expand All @@ -34,4 +35,6 @@ public interface SecurityStore {
void setSecurityEnabled(boolean securityEnabled);

void stop();

Subject getSessionSubject(SecurityAuth session);
}
Expand Up @@ -349,6 +349,22 @@ public static String getUserFromSubject(Subject subject) {
return validatedUser;
}

/**
* Get the cached Subject. If the Subject is not in the cache then authenticate again to retrieve
* it.
*
* @param session contains the authentication data
* @return the authenticated Subject with all associated role principals or null if not
* authenticated or JAAS is not supported by the SecurityManager.
*/
@Override
public Subject getSessionSubject(SecurityAuth session) {
if (securityManager instanceof ActiveMQSecurityManager5) {
return getSubjectForAuthorization(session, (ActiveMQSecurityManager5) securityManager);
}
return null;
}

/**
* Get the cached Subject. If the Subject is not in the cache then authenticate again to retrieve it.
*
Expand Down
Expand Up @@ -289,6 +289,9 @@ enum SERVER_STATE {

void callBrokerMessagePlugins(ActiveMQPluginRunnable<ActiveMQServerMessagePlugin> pluginRun) throws ActiveMQException;

boolean callBrokerMessagePluginsCanAccept(ServerConsumer serverConsumer,
MessageReference messageReference) throws ActiveMQException;

void callBrokerBridgePlugins(ActiveMQPluginRunnable<ActiveMQServerBridgePlugin> pluginRun) throws ActiveMQException;

void callBrokerCriticalPlugins(ActiveMQPluginRunnable<ActiveMQServerCriticalPlugin> pluginRun) throws ActiveMQException;
Expand Down
Expand Up @@ -131,6 +131,7 @@
import org.apache.activemq.artemis.core.server.LargeServerMessage;
import org.apache.activemq.artemis.core.server.LoggingConfigurationFileReloader;
import org.apache.activemq.artemis.core.server.MemoryManager;
import org.apache.activemq.artemis.core.server.MessageReference;
import org.apache.activemq.artemis.core.server.NetworkHealthCheck;
import org.apache.activemq.artemis.core.server.NodeManager;
import org.apache.activemq.artemis.core.server.PostQueueCreationCallback;
Expand All @@ -139,6 +140,7 @@
import org.apache.activemq.artemis.core.server.QueueFactory;
import org.apache.activemq.artemis.core.server.QueueQueryResult;
import org.apache.activemq.artemis.core.server.SecuritySettingPlugin;
import org.apache.activemq.artemis.core.server.ServerConsumer;
import org.apache.activemq.artemis.core.server.ServerSession;
import org.apache.activemq.artemis.core.server.ServiceComponent;
import org.apache.activemq.artemis.core.server.ServiceRegistry;
Expand Down Expand Up @@ -2557,6 +2559,27 @@ public void callBrokerMessagePlugins(final ActiveMQPluginRunnable<ActiveMQServer
callBrokerPlugins(getBrokerMessagePlugins(), pluginRun);
}

@Override
public boolean callBrokerMessagePluginsCanAccept(ServerConsumer serverConsumer, MessageReference messageReference) throws ActiveMQException {
for (ActiveMQServerMessagePlugin plugin : getBrokerMessagePlugins()) {
try {
//if ANY plugin returned false the message will not be accepted for that consumer
if (!plugin.canAccept(serverConsumer, messageReference)) {
return false;
}
} catch (Throwable e) {
if (e instanceof ActiveMQException) {
logger.debug("plugin " + plugin + " is throwing ActiveMQException");
throw (ActiveMQException) e;
} else {
logger.warn("Internal error on plugin " + plugin, e.getMessage(), e);
}
}
}
//if ALL plugins have returned true consumer can accept message
return true;
}

@Override
public void callBrokerBridgePlugins(final ActiveMQPluginRunnable<ActiveMQServerBridgePlugin> pluginRun) throws ActiveMQException {
callBrokerPlugins(getBrokerBridgePlugins(), pluginRun);
Expand Down
Expand Up @@ -404,6 +404,12 @@ public HandleStatus handle(final MessageReference ref) throws Exception {

return HandleStatus.BUSY;
}
if (server.hasBrokerMessagePlugins() && !server.callBrokerMessagePluginsCanAccept(this, ref)) {
if (logger.isTraceEnabled()) {
logger.trace("Reference " + ref + " is not allowed to be consumed by " + this + " due to message plugin filter.");
}
return HandleStatus.NO_MATCH;
}

synchronized (lock) {
// If the consumer is stopped then we don't accept the message, it
Expand Down
Expand Up @@ -158,6 +158,17 @@ default void onMessageRouteException(Message message, RoutingContext context, bo

}

/**
* Before a message is delivered to a client consumer
*
* @param consumer the consumer the message will be delivered to
* @param reference message reference
* @throws ActiveMQException
*/
default boolean canAccept(ServerConsumer consumer, MessageReference reference) throws ActiveMQException {
return true;
}

/**
* Before a message is delivered to a client consumer
*
Expand Down
@@ -0,0 +1,88 @@
/*
* 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.core.server.plugin.impl;

import javax.security.auth.Subject;

import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.core.security.SecurityStore;
import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.core.server.ConsumerInfo;
import org.apache.activemq.artemis.core.server.MessageReference;
import org.apache.activemq.artemis.core.server.ServerConsumer;
import org.apache.activemq.artemis.core.server.ServerSession;
import org.apache.activemq.artemis.core.server.plugin.ActiveMQServerPlugin;
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal;
import org.jboss.logging.Logger;

public class BrokerMessageAuthorizationPlugin implements ActiveMQServerPlugin {

private static final Logger logger = Logger.getLogger(BrokerMessageAuthorizationPlugin.class);

private static final String ROLE_PROPERTY = "ROLE_PROPERTY";
private final AtomicReference<ActiveMQServer> server = new AtomicReference<>();
private String roleProperty = "requiredRole";

@Override
public void init(Map<String, String> properties) {
roleProperty = properties.getOrDefault(ROLE_PROPERTY, "requiredRole");
}

@Override
public void registered(ActiveMQServer server) {
this.server.set(server);
}

@Override
public void unregistered(ActiveMQServer server) {
this.server.set(null);
}

@Override
public boolean canAccept(ServerConsumer consumer, MessageReference reference) throws ActiveMQException {

String requiredRole = reference.getMessage().getStringProperty(roleProperty);
if (requiredRole == null) {
return true;
}

Subject subject = getSubject(consumer);
if (subject == null) {
if (logger.isDebugEnabled()) {
logger.debug("Subject not found for consumer: " + consumer.getID());
}
return false;
}
boolean permitted = new RolePrincipal(requiredRole).implies(subject);
if (!permitted && logger.isDebugEnabled()) {
logger.debug("Message consumer: " + consumer.getID() + " does not have required role `" + requiredRole + "` needed to receive message: " + reference.getMessageID());
}
return permitted;
}

private Subject getSubject(ConsumerInfo consumer) {
final ActiveMQServer activeMQServer = server.get();
final SecurityStore securityStore = activeMQServer.getSecurityStore();
ServerSession session = activeMQServer.getSessionByID(consumer.getSessionName());
return securityStore.getSessionSubject(session);
}

}
23 changes: 23 additions & 0 deletions docs/user-manual/en/broker-plugins.md
Expand Up @@ -126,3 +126,26 @@ In the example below both `SEND_CONNECTION_NOTIFICATIONS` and
</broker-plugins>
```

## Using the BrokerMessageAuthorizationPlugin

The `BrokerMessageAuthorizationPlugin` filters messages sent to consumers based on if they have a role that matches the value specified in a message property.

You can select which property will be used to specify the required role for consuming a message by setting the following configuration.

Property|Property Description|Default Value
---|---|---
`ROLE_PROPERTY`|Property name used to determine the role required to consume a message.|`requiredRole`.


If the message does not have a property matching the configured `ROLE_PROPERTY` then the message will be sent to any consumer.

To configure the plugin, you can add the following configuration to the broker.
In the example below `ROLE_PROPERTY` is set to `permissions` when that property is present messages will only be sent to consumers with a role matching its value.

```xml
<broker-plugins>
<broker-plugin class-name="org.apache.activemq.artemis.core.server.plugin.impl.BrokerMessageAuthorizationPlugin">
<property key="ROLE_PROPERTY" value="permissions" />
</broker-plugin>
</broker-plugins>
```
134 changes: 134 additions & 0 deletions examples/features/standard/broker-msg-auth-plugin/pom.xml
@@ -0,0 +1,134 @@
<?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.
-->

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.apache.activemq.examples.broker</groupId>
<artifactId>jms-examples</artifactId>
<version>2.17.0-SNAPSHOT</version>
</parent>

<artifactId>broker-msg-auth-plugin</artifactId>
<packaging>jar</packaging>
<name>ActiveMQ Artemis Broker Auth Plugin Example</name>

<properties>
<activemq.basedir>${project.basedir}/../../../..</activemq.basedir>
</properties>

<dependencies>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-jms-client-all</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-amqp-protocol</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.qpid</groupId>
<artifactId>qpid-jms-client</artifactId>
<version>${qpid.jms.version}</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-jms-client-all</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-maven-plugin</artifactId>
<executions>
<execution>
<id>create</id>
<phase>verify</phase>
<configuration>
<!-- The broker plugin will install this library on the server's classpath -->
<libList><arg>org.apache.activemq.examples.broker:broker-msg-auth-plugin:${project.version}</arg></libList>
<ignore>${noServer}</ignore>
</configuration>
<goals>
<goal>create</goal>
</goals>
</execution>
<execution>
<id>start</id>
<goals>
<goal>cli</goal>
</goals>
<configuration>
<spawn>true</spawn>
<ignore>${noServer}</ignore>
<testURI>tcp://localhost:61616</testURI>
<args>
<param>run</param>
</args>
</configuration>
</execution>
<execution>
<id>runClient</id>
<goals>
<goal>runClient</goal>
</goals>
<configuration>
<clientClass>org.apache.activemq.artemis.jms.example.BrokerAuthPluginExample</clientClass>
</configuration>
</execution>
<execution>
<id>stop</id>
<goals>
<goal>cli</goal>
</goals>
<configuration>
<ignore>${noServer}</ignore>
<args>
<param>stop</param>
</args>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.apache.activemq.examples.broker</groupId>
<artifactId>broker-msg-auth-plugin</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-clean-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
5 changes: 5 additions & 0 deletions examples/features/standard/broker-msg-auth-plugin/readme.md
@@ -0,0 +1,5 @@
# Broker Plugin Example

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

This example shows how a message plugin can be used to filter message sent to a consumer depending on that consumers roles. Credentials for a user are by default invalidated every 10 seconds so this plugin may cause excessive authentication if used without configuring the security-invalidation-interval limit appropriately.

0 comments on commit 5ec9a9d

Please sign in to comment.