001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.activemq.shiro.authz;
018
019import org.apache.activemq.broker.ConnectionContext;
020import org.apache.activemq.broker.ProducerBrokerExchange;
021import org.apache.activemq.broker.region.Destination;
022import org.apache.activemq.broker.region.Subscription;
023import org.apache.activemq.command.ActiveMQDestination;
024import org.apache.activemq.command.ConsumerInfo;
025import org.apache.activemq.command.DestinationInfo;
026import org.apache.activemq.command.Message;
027import org.apache.activemq.command.ProducerInfo;
028import org.apache.activemq.security.SecurityContext;
029import org.apache.activemq.shiro.env.EnvironmentFilter;
030import org.apache.activemq.shiro.subject.ConnectionSubjectResolver;
031import org.apache.shiro.authz.Permission;
032import org.apache.shiro.authz.UnauthorizedException;
033import org.apache.shiro.subject.PrincipalCollection;
034import org.apache.shiro.subject.Subject;
035
036import java.util.Collection;
037
038/**
039 * The {@code AuthorizationFilter} asserts that actions are allowed to execute first before they are actually
040 * executed.  Such actions include creating, removing, reading from and writing to destinations.
041 * <p/>
042 * This implementation is strictly permission-based, allowing for the finest-grained security policies possible.
043 * Whenever a {@link Subject} associated with a connection attempts to perform an {@link org.apache.activemq.shiro.authz.Action} (such as creating a
044 * destination, or reading from a queue, etc), one or more {@link Permission}s representing that {@code action} are
045 * checked.
046 * <p/>
047 * If the {@code Subject}{@link Subject#isPermitted(org.apache.shiro.authz.Permission) isPermitted} to perform the
048 * {@code action}, the action is allowed to execute and the broker filter chain executes uninterrupted.
049 * <p/>
050 * However, if the {@code Subject} is not permitted to perform the action, an {@link UnauthorizedException} will be
051 * thrown, preventing the filter chain from executing that action.
052 * <h2>ActionPermissionResolver</h2>
053 * The attempted {@code Action} is guarded by one or more {@link Permission}s as indicated by a configurable
054 * {@link #setActionPermissionResolver(org.apache.activemq.shiro.authz.ActionPermissionResolver) actionPermissionResolver}.  The
055 * {@code actionPermissionResolver} indicates which permissions must be granted to the connection {@code Subject} in
056 * order for the action to execute.
057 * <p/>
058 * The default {@code actionPermissionResolver} instance is a
059 * {@link org.apache.activemq.shiro.authz.DestinationActionPermissionResolver DestinationActionPermissionResolver}, which indicates which permissions
060 * are required to perform any action on a particular destination.  Those familiar with Shiro's
061 * {@link org.apache.shiro.authz.permission.WildcardPermission WildcardPermission} syntax will find the
062 * {@code DestinationActionPermissionResolver}'s
063 * {@link org.apache.activemq.shiro.authz.DestinationActionPermissionResolver#createPermissionString createPermissionString} method
064 * documentation valuable for understanding how destination actions are represented as permissions.
065 *
066 * @see org.apache.activemq.shiro.authz.ActionPermissionResolver
067 * @see org.apache.activemq.shiro.authz.DestinationActionPermissionResolver
068 * @since 5.10.0
069 */
070public class AuthorizationFilter extends EnvironmentFilter {
071
072    private ActionPermissionResolver actionPermissionResolver;
073
074    public AuthorizationFilter() {
075        this.actionPermissionResolver = new DestinationActionPermissionResolver();
076    }
077
078    /**
079     * Returns the {@code ActionPermissionResolver} used to indicate which permissions are required to be granted to
080     * a {@link Subject} to perform a particular destination {@link org.apache.activemq.shiro.authz.Action}, (such as creating a
081     * destination, or reading from a queue, etc).  The default instance is a
082     * {@link DestinationActionPermissionResolver}.
083     *
084     * @return the {@code ActionPermissionResolver} used to indicate which permissions are required to be granted to
085     *         a {@link Subject} to perform a particular destination {@link org.apache.activemq.shiro.authz.Action}, (such as creating a
086     *         destination, or reading from a queue, etc).
087     */
088    public ActionPermissionResolver getActionPermissionResolver() {
089        return actionPermissionResolver;
090    }
091
092    /**
093     * Sets the {@code ActionPermissionResolver} used to indicate which permissions are required to be granted to
094     * a {@link Subject} to perform a particular destination {@link org.apache.activemq.shiro.authz.Action}, (such as creating a
095     * destination, or reading from a queue, etc).  Unless overridden by this method, the default instance is a
096     * {@link DestinationActionPermissionResolver}.
097     *
098     * @param actionPermissionResolver the {@code ActionPermissionResolver} used to indicate which permissions are
099     *                                 required to be granted to a {@link Subject} to perform a particular destination
100     *                                 {@link org.apache.activemq.shiro.authz.Action}, (such as creating a destination, or reading from a queue, etc).
101     */
102    public void setActionPermissionResolver(ActionPermissionResolver actionPermissionResolver) {
103        this.actionPermissionResolver = actionPermissionResolver;
104    }
105
106    /**
107     * Returns the {@code Subject} associated with the specified connection using a
108     * {@link org.apache.activemq.shiro.subject.ConnectionSubjectResolver}.
109     *
110     * @param ctx the connection context
111     * @return the {@code Subject} associated with the specified connection.
112     */
113    protected Subject getSubject(ConnectionContext ctx) {
114        return new ConnectionSubjectResolver(ctx).getSubject();
115    }
116
117    protected String toString(Subject subject) {
118        PrincipalCollection pc = subject.getPrincipals();
119        if (pc != null && !pc.isEmpty()) {
120            return "[" + pc.toString() + "] ";
121        }
122        return "";
123    }
124
125    protected void assertAuthorized(DestinationAction action) {
126        assertAuthorized(action, action.getVerb());
127    }
128
129    //ActiveMQ internals will create a ConnectionContext with a SecurityContext that is not
130    //Shiro specific.  We need to allow actions for internal system operations:
131    protected boolean isSystemBroker(DestinationAction action) {
132        ConnectionContext context = action.getConnectionContext();
133        SecurityContext securityContext = context.getSecurityContext();
134        return securityContext != null && securityContext.isBrokerContext();
135    }
136
137    protected void assertAuthorized(DestinationAction action, String verbText) {
138        if (!isEnabled() || isSystemBroker(action)) {
139            return;
140        }
141
142        final Subject subject = getSubject(action.getConnectionContext());
143
144        Collection<Permission> perms = this.actionPermissionResolver.getPermissions(action);
145
146        if (!subject.isPermittedAll(perms)) {
147            String msg = createUnauthorizedMessage(subject, action, verbText);
148            throw new UnauthorizedException(msg);
149        }
150    }
151
152    protected String createUnauthorizedMessage(Subject subject, DestinationAction action, String verbDisplayText) {
153        return "Subject " + toString(subject) + "is not authorized to " + verbDisplayText + " destination: " + action.getDestination();
154    }
155
156    @Override
157    public void addDestinationInfo(ConnectionContext context, DestinationInfo info) throws Exception {
158
159        DestinationAction action = new DestinationAction(context, info.getDestination(), "create");
160        assertAuthorized(action);
161
162        super.addDestinationInfo(context, info);
163    }
164
165    @Override
166    public Destination addDestination(ConnectionContext context, ActiveMQDestination destination, boolean create) throws Exception {
167
168        DestinationAction action = new DestinationAction(context, destination, "create");
169        assertAuthorized(action);
170
171        return super.addDestination(context, destination, create);
172    }
173
174    @Override
175    public void removeDestination(ConnectionContext context, ActiveMQDestination destination, long timeout) throws Exception {
176
177        DestinationAction action = new DestinationAction(context, destination, "remove");
178        assertAuthorized(action);
179
180        super.removeDestination(context, destination, timeout);
181    }
182
183    @Override
184    public void removeDestinationInfo(ConnectionContext context, DestinationInfo info) throws Exception {
185
186        DestinationAction action = new DestinationAction(context, info.getDestination(), "remove");
187        assertAuthorized(action);
188
189        super.removeDestinationInfo(context, info);
190    }
191
192    @Override
193    public Subscription addConsumer(ConnectionContext context, ConsumerInfo info) throws Exception {
194
195        //Unlike when adding a producer, consumers must specify the destination at creation time, so we can rely on
196        //a destination being available to perform the authz check:
197        DestinationAction action = new DestinationAction(context, info.getDestination(), "read");
198        assertAuthorized(action, "read from");
199
200        return super.addConsumer(context, info);
201    }
202
203    @Override
204    public void addProducer(ConnectionContext context, ProducerInfo info) throws Exception {
205
206        // JMS allows producers to be created without first specifying a destination.  In these cases, every send
207        // operation must specify a destination.  Because of this, we only authorize 'addProducer' if a destination is
208        // specified. If not specified, the authz check in the 'send' method below will ensure authorization.
209        if (info.getDestination() != null) {
210            DestinationAction action = new DestinationAction(context, info.getDestination(), "write");
211            assertAuthorized(action, "write to");
212        }
213
214        super.addProducer(context, info);
215    }
216
217    @Override
218    public void send(ProducerBrokerExchange exchange, Message message) throws Exception {
219
220        DestinationAction action = new DestinationAction(exchange.getConnectionContext(), message.getDestination(), "write");
221        assertAuthorized(action, "write to");
222
223        super.send(exchange, message);
224    }
225
226}