blob: 0c147d911f528d43e7b6a4365fd7e499acb14f33 [file] [log] [blame]
/**
* 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.shiro.authz;
import org.apache.activemq.broker.ConnectionContext;
import org.apache.activemq.broker.ProducerBrokerExchange;
import org.apache.activemq.broker.region.Destination;
import org.apache.activemq.broker.region.Subscription;
import org.apache.activemq.command.ActiveMQDestination;
import org.apache.activemq.command.ConsumerInfo;
import org.apache.activemq.command.DestinationInfo;
import org.apache.activemq.command.Message;
import org.apache.activemq.command.ProducerInfo;
import org.apache.activemq.security.SecurityContext;
import org.apache.activemq.shiro.env.EnvironmentFilter;
import org.apache.activemq.shiro.subject.ConnectionSubjectResolver;
import org.apache.shiro.authz.Permission;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import java.util.Collection;
/**
* The {@code AuthorizationFilter} asserts that actions are allowed to execute first before they are actually
* executed. Such actions include creating, removing, reading from and writing to destinations.
* <p/>
* This implementation is strictly permission-based, allowing for the finest-grained security policies possible.
* Whenever a {@link Subject} associated with a connection attempts to perform an {@link org.apache.activemq.shiro.authz.Action} (such as creating a
* destination, or reading from a queue, etc), one or more {@link Permission}s representing that {@code action} are
* checked.
* <p/>
* If the {@code Subject}{@link Subject#isPermitted(org.apache.shiro.authz.Permission) isPermitted} to perform the
* {@code action}, the action is allowed to execute and the broker filter chain executes uninterrupted.
* <p/>
* However, if the {@code Subject} is not permitted to perform the action, an {@link UnauthorizedException} will be
* thrown, preventing the filter chain from executing that action.
* <h2>ActionPermissionResolver</h2>
* The attempted {@code Action} is guarded by one or more {@link Permission}s as indicated by a configurable
* {@link #setActionPermissionResolver(org.apache.activemq.shiro.authz.ActionPermissionResolver) actionPermissionResolver}. The
* {@code actionPermissionResolver} indicates which permissions must be granted to the connection {@code Subject} in
* order for the action to execute.
* <p/>
* The default {@code actionPermissionResolver} instance is a
* {@link org.apache.activemq.shiro.authz.DestinationActionPermissionResolver DestinationActionPermissionResolver}, which indicates which permissions
* are required to perform any action on a particular destination. Those familiar with Shiro's
* {@link org.apache.shiro.authz.permission.WildcardPermission WildcardPermission} syntax will find the
* {@code DestinationActionPermissionResolver}'s
* {@link org.apache.activemq.shiro.authz.DestinationActionPermissionResolver#createPermissionString createPermissionString} method
* documentation valuable for understanding how destination actions are represented as permissions.
*
* @see org.apache.activemq.shiro.authz.ActionPermissionResolver
* @see org.apache.activemq.shiro.authz.DestinationActionPermissionResolver
* @since 5.10.0
*/
public class AuthorizationFilter extends EnvironmentFilter {
private ActionPermissionResolver actionPermissionResolver;
public AuthorizationFilter() {
this.actionPermissionResolver = new DestinationActionPermissionResolver();
}
/**
* Returns the {@code ActionPermissionResolver} used to indicate which permissions are required to be granted to
* a {@link Subject} to perform a particular destination {@link org.apache.activemq.shiro.authz.Action}, (such as creating a
* destination, or reading from a queue, etc). The default instance is a
* {@link DestinationActionPermissionResolver}.
*
* @return the {@code ActionPermissionResolver} used to indicate which permissions are required to be granted to
* a {@link Subject} to perform a particular destination {@link org.apache.activemq.shiro.authz.Action}, (such as creating a
* destination, or reading from a queue, etc).
*/
public ActionPermissionResolver getActionPermissionResolver() {
return actionPermissionResolver;
}
/**
* Sets the {@code ActionPermissionResolver} used to indicate which permissions are required to be granted to
* a {@link Subject} to perform a particular destination {@link org.apache.activemq.shiro.authz.Action}, (such as creating a
* destination, or reading from a queue, etc). Unless overridden by this method, the default instance is a
* {@link DestinationActionPermissionResolver}.
*
* @param actionPermissionResolver the {@code ActionPermissionResolver} used to indicate which permissions are
* required to be granted to a {@link Subject} to perform a particular destination
* {@link org.apache.activemq.shiro.authz.Action}, (such as creating a destination, or reading from a queue, etc).
*/
public void setActionPermissionResolver(ActionPermissionResolver actionPermissionResolver) {
this.actionPermissionResolver = actionPermissionResolver;
}
/**
* Returns the {@code Subject} associated with the specified connection using a
* {@link org.apache.activemq.shiro.subject.ConnectionSubjectResolver}.
*
* @param ctx the connection context
* @return the {@code Subject} associated with the specified connection.
*/
protected Subject getSubject(ConnectionContext ctx) {
return new ConnectionSubjectResolver(ctx).getSubject();
}
protected String toString(Subject subject) {
PrincipalCollection pc = subject.getPrincipals();
if (pc != null && !pc.isEmpty()) {
return "[" + pc.toString() + "] ";
}
return "";
}
protected void assertAuthorized(DestinationAction action) {
assertAuthorized(action, action.getVerb());
}
//ActiveMQ internals will create a ConnectionContext with a SecurityContext that is not
//Shiro specific. We need to allow actions for internal system operations:
protected boolean isSystemBroker(DestinationAction action) {
ConnectionContext context = action.getConnectionContext();
SecurityContext securityContext = context.getSecurityContext();
return securityContext != null && securityContext.isBrokerContext();
}
protected void assertAuthorized(DestinationAction action, String verbText) {
if (!isEnabled() || isSystemBroker(action)) {
return;
}
final Subject subject = getSubject(action.getConnectionContext());
Collection<Permission> perms = this.actionPermissionResolver.getPermissions(action);
if (!subject.isPermittedAll(perms)) {
String msg = createUnauthorizedMessage(subject, action, verbText);
throw new UnauthorizedException(msg);
}
}
protected String createUnauthorizedMessage(Subject subject, DestinationAction action, String verbDisplayText) {
return "Subject " + toString(subject) + "is not authorized to " + verbDisplayText + " destination: " + action.getDestination();
}
@Override
public void addDestinationInfo(ConnectionContext context, DestinationInfo info) throws Exception {
DestinationAction action = new DestinationAction(context, info.getDestination(), "create");
assertAuthorized(action);
super.addDestinationInfo(context, info);
}
@Override
public Destination addDestination(ConnectionContext context, ActiveMQDestination destination, boolean create) throws Exception {
DestinationAction action = new DestinationAction(context, destination, "create");
assertAuthorized(action);
return super.addDestination(context, destination, create);
}
@Override
public void removeDestination(ConnectionContext context, ActiveMQDestination destination, long timeout) throws Exception {
DestinationAction action = new DestinationAction(context, destination, "remove");
assertAuthorized(action);
super.removeDestination(context, destination, timeout);
}
@Override
public void removeDestinationInfo(ConnectionContext context, DestinationInfo info) throws Exception {
DestinationAction action = new DestinationAction(context, info.getDestination(), "remove");
assertAuthorized(action);
super.removeDestinationInfo(context, info);
}
@Override
public Subscription addConsumer(ConnectionContext context, ConsumerInfo info) throws Exception {
//Unlike when adding a producer, consumers must specify the destination at creation time, so we can rely on
//a destination being available to perform the authz check:
DestinationAction action = new DestinationAction(context, info.getDestination(), "read");
assertAuthorized(action, "read from");
return super.addConsumer(context, info);
}
@Override
public void addProducer(ConnectionContext context, ProducerInfo info) throws Exception {
// JMS allows producers to be created without first specifying a destination. In these cases, every send
// operation must specify a destination. Because of this, we only authorize 'addProducer' if a destination is
// specified. If not specified, the authz check in the 'send' method below will ensure authorization.
if (info.getDestination() != null) {
DestinationAction action = new DestinationAction(context, info.getDestination(), "write");
assertAuthorized(action, "write to");
}
super.addProducer(context, info);
}
@Override
public void send(ProducerBrokerExchange exchange, Message message) throws Exception {
DestinationAction action = new DestinationAction(exchange.getConnectionContext(), message.getDestination(), "write");
assertAuthorized(action, "write to");
super.send(exchange, message);
}
}