Eliminate ThreadLocals
[aaa.git] / aaa-shiro / impl / src / main / java / org / opendaylight / aaa / shiro / realm / MDSALDynamicAuthorizationFilter.java
1 /*
2  * Copyright (c) 2017 Brocade Communications Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.aaa.shiro.realm;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11 import static com.google.common.base.Verify.verifyNotNull;
12 import static java.util.Objects.requireNonNull;
13
14 import com.google.common.collect.Iterables;
15 import com.google.common.util.concurrent.Futures;
16 import com.google.common.util.concurrent.ListenableFuture;
17 import java.util.ArrayList;
18 import java.util.Collection;
19 import java.util.Comparator;
20 import java.util.List;
21 import java.util.Optional;
22 import java.util.concurrent.ExecutionException;
23 import javax.servlet.Filter;
24 import javax.servlet.ServletRequest;
25 import javax.servlet.ServletResponse;
26 import javax.servlet.http.HttpServletRequest;
27 import org.apache.shiro.subject.Subject;
28 import org.apache.shiro.web.filter.authz.AuthorizationFilter;
29 import org.opendaylight.mdsal.binding.api.ClusteredDataTreeChangeListener;
30 import org.opendaylight.mdsal.binding.api.DataBroker;
31 import org.opendaylight.mdsal.binding.api.DataTreeIdentifier;
32 import org.opendaylight.mdsal.binding.api.DataTreeModification;
33 import org.opendaylight.mdsal.binding.api.ReadTransaction;
34 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
35 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.HttpAuthorization;
36 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.authorization.policies.Policies;
37 import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.permission.Permissions;
38 import org.opendaylight.yangtools.concepts.ListenerRegistration;
39 import org.opendaylight.yangtools.concepts.Registration;
40 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 /**
45  * Provides a dynamic authorization mechanism for restful web services with permission grain
46  * scope.  <code>aaa.yang</code> defines the model for this filtering mechanism.
47  * This model exposes the ability to manipulate policy information for specific paths
48  * based on a tuple of (role, http_permission_list).
49  *
50  * <p>This mechanism will only work when put behind <code>authcBasic</code>.
51  */
52 @SuppressWarnings("checkstyle:AbbreviationAsWordInName")
53 public class MDSALDynamicAuthorizationFilter extends AuthorizationFilter
54         implements ClusteredDataTreeChangeListener<HttpAuthorization> {
55
56     private static final Logger LOG = LoggerFactory.getLogger(MDSALDynamicAuthorizationFilter.class);
57
58     private static final DataTreeIdentifier<HttpAuthorization> AUTHZ_CONTAINER = DataTreeIdentifier.create(
59             LogicalDatastoreType.CONFIGURATION, InstanceIdentifier.create(HttpAuthorization.class));
60
61     private static final ThreadLocal<DataBroker> DATABROKER_TL = new ThreadLocal<>();
62
63     private final DataBroker dataBroker;
64
65     private ListenerRegistration<?> reg;
66     private volatile ListenableFuture<Optional<HttpAuthorization>> authContainer;
67
68     public MDSALDynamicAuthorizationFilter() {
69         this(verifyNotNull(DATABROKER_TL.get(), "MDSALDynamicAuthorizationFilter loading not prepared"));
70     }
71
72     public MDSALDynamicAuthorizationFilter(final DataBroker dataBroker) {
73         this.dataBroker = requireNonNull(dataBroker);
74     }
75
76     public static Registration prepareForLoad(final DataBroker dataBroker) {
77         DATABROKER_TL.set(requireNonNull(dataBroker));
78         return DATABROKER_TL::remove;
79     }
80
81     @Override
82     public Filter processPathConfig(final String path, final String config) {
83         try (ReadTransaction tx = dataBroker.newReadOnlyTransaction()) {
84             authContainer = tx.read(AUTHZ_CONTAINER.getDatastoreType(), AUTHZ_CONTAINER.getRootIdentifier());
85         }
86         reg = dataBroker.registerDataTreeChangeListener(AUTHZ_CONTAINER, this);
87         return super.processPathConfig(path, config);
88     }
89
90     @Override
91     public void destroy() {
92         if (reg != null) {
93             reg.close();
94             reg = null;
95         }
96         super.destroy();
97     }
98
99     @Override
100     public void onDataTreeChanged(final Collection<DataTreeModification<HttpAuthorization>> changes) {
101         final HttpAuthorization newVal = Iterables.getLast(changes).getRootNode().getDataAfter();
102         LOG.debug("Updating authorization information to {}", newVal);
103         authContainer = Futures.immediateFuture(Optional.ofNullable(newVal));
104     }
105
106     @Override
107     public boolean isAccessAllowed(final ServletRequest request, final ServletResponse response,
108                                    final Object mappedValue) {
109         checkArgument(request instanceof HttpServletRequest, "Expected HttpServletRequest, received {}", request);
110
111         final Subject subject = getSubject(request, response);
112         final HttpServletRequest httpServletRequest = (HttpServletRequest)request;
113         final String requestURI = httpServletRequest.getRequestURI();
114         LOG.debug("isAccessAllowed for user={} to requestURI={}", subject, requestURI);
115
116         final Optional<HttpAuthorization> authorizationOptional;
117         try {
118             authorizationOptional = authContainer.get();
119         } catch (ExecutionException | InterruptedException e) {
120             // Something went completely wrong trying to read the authz container.  Deny access.
121             LOG.warn("MDSAL attempt to read Http Authz Container failed, disallowing access", e);
122             return false;
123         }
124
125         if (!authorizationOptional.isPresent()) {
126             // The authorization container does not exist-- hence no authz rules are present
127             // Allow access.
128             LOG.debug("Authorization Container does not exist");
129             return true;
130         }
131
132         final HttpAuthorization httpAuthorization = authorizationOptional.get();
133         final var policies = httpAuthorization.getPolicies();
134         List<Policies> policiesList = policies != null ? policies.getPolicies() : null;
135         if (policiesList == null || policiesList.isEmpty()) {
136             // The authorization container exists, but no rules are present.  Allow access.
137             LOG.debug("Exiting successfully early since no authorization rules exist");
138             return true;
139         }
140
141         // Sort the Policies list based on index
142         policiesList = new ArrayList<>(policiesList);
143         policiesList.sort(Comparator.comparing(Policies::getIndex));
144
145         for (Policies policy : policiesList) {
146             final String resource = policy.getResource();
147             final boolean pathsMatch = pathsMatch(resource, requestURI);
148             if (pathsMatch) {
149                 LOG.debug("paths match for pattern={} and requestURI={}", resource, requestURI);
150                 final String method = httpServletRequest.getMethod();
151                 LOG.trace("method={}", method);
152                 for (Permissions permission : policy.getPermissions()) {
153                     final String role = permission.getRole();
154                     LOG.trace("role={}", role);
155                     for (Permissions.Actions action : permission.getActions()) {
156                         LOG.trace("action={}", action.getName());
157                         if (action.getName().equalsIgnoreCase(method)) {
158                             final boolean hasRole = subject.hasRole(role);
159                             LOG.trace("hasRole({})={}", role, hasRole);
160                             if (hasRole) {
161                                 return true;
162                             }
163                         }
164                     }
165                 }
166                 LOG.debug("couldn't authorize the user for access");
167                 return false;
168             }
169         }
170         LOG.debug("successfully authorized the user for access");
171         return true;
172     }
173 }