Addition of dynamic http authorization based on particular http operations 94/50694/9
authorRyan Goulding <ryandgoulding@gmail.com>
Thu, 19 Jan 2017 01:18:58 +0000 (20:18 -0500)
committerRyan Goulding <ryandgoulding@gmail.com>
Wed, 1 Feb 2017 19:40:55 +0000 (14:40 -0500)
In the past, shiro.ini has been used to configure URL based authorization
through the RolesAuthorizationFilter.  This is a bit messy for two main
reasons:

1) The urls are expected to be relative to the servlet context.  This sucks,
since if you are using the same Filter over multiple contexts, the servlet
context part of the URL is chopped off.  Thus, there are chances for
ambiguity through configuring through shiro.ini

2) The rules can only be recognized on system startup-- they are not dynamic.

This adds a model to add in rules for protecting REST urls.  Future
revisions of the model will provide black/white lists etc.  For now, this
just provides Role Based Access Control (RBAC).

Change-Id: I1f1ce957a43eb7f7eba69cab74a65ed653ab1832
Signed-off-by: Ryan Goulding <ryandgoulding@gmail.com>
12 files changed:
aaa-shiro/api/src/main/yang/aaa-shiro.yang [deleted file]
aaa-shiro/api/src/main/yang/aaa.yang [new file with mode: 0644]
aaa-shiro/impl/pom.xml
aaa-shiro/impl/src/main/java/org/opendaylight/aaa/impl/AAAShiroProvider.java
aaa-shiro/impl/src/main/java/org/opendaylight/aaa/impl/shiro/realm/MDSALDynamicAuthorizationFilter.java [new file with mode: 0644]
aaa-shiro/impl/src/main/resources/org/opendaylight/blueprint/impl-blueprint.xml
aaa-shiro/impl/src/main/resources/shiro.ini
aaa-shiro/impl/src/test/java/org/opendaylight/aaa/impl/shiro/realm/MDSALDynamicAuthorizationFilterTest.java [new file with mode: 0644]
aaa-shiro/impl/src/test/java/org/opendaylight/aaa/shiro/realm/mapping/impl/BestAttemptGroupToRolesMappingStrategyTest.java
features/authn/pom.xml
features/shiro/pom.xml
features/shiro/src/main/features/features.xml

diff --git a/aaa-shiro/api/src/main/yang/aaa-shiro.yang b/aaa-shiro/api/src/main/yang/aaa-shiro.yang
deleted file mode 100644 (file)
index 063c2b6..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
-module aaa-shiro {
-    yang-version 1;
-    namespace "urn:opendaylight:params:xml:ns:yang:aaa-shiro";
-    prefix "aaa-shiro";
-
-    revision "2015-01-05" {
-        description "Initial revision of aaa-shiro model";
-    }
-}
diff --git a/aaa-shiro/api/src/main/yang/aaa.yang b/aaa-shiro/api/src/main/yang/aaa.yang
new file mode 100644 (file)
index 0000000..31b29f6
--- /dev/null
@@ -0,0 +1,45 @@
+module aaa {
+    yang-version 1;
+    namespace "urn:opendaylight:params:xml:ns:yang:aaa";
+    prefix "aaa";
+
+    revision "2016-12-14" {
+        description "Initial revision of aaa model";
+    }
+
+    grouping http-permission {
+        leaf resource {
+            type string;
+            default "*";
+        }
+        list permissions {
+            leaf-list actions {
+                type enumeration {
+                    enum get;
+                    enum put;
+                    enum post;
+                    enum patch;
+                    enum delete;
+                }
+            }
+            leaf role {
+                type string;
+            }
+        }
+        leaf description {
+            type string;
+            default "";
+        }
+    }
+
+    container http-authorization {
+
+        container policies {
+            list policies {
+                key "resource";
+                uses http-permission;
+                ordered-by user;
+            }
+        }
+    }
+}
index 88a6b675b47f72648669dc09a5dd3c36d0bc4148..4c58486939d7784507c24fda35bb472fab08a453 100644 (file)
@@ -113,6 +113,11 @@ and is available at http://www.eclipse.org/legal/epl-v10.html
       <artifactId>logback-classic</artifactId>
       <scope>test</scope>
     </dependency>
+      <dependency>
+          <groupId>org.apache.shiro</groupId>
+          <artifactId>shiro-core</artifactId>
+          <version>1.3.1</version>
+      </dependency>
   </dependencies>
 
   <build>
index 953695476a6e59e21b5c739e264301612cc17409..d21b353b50ba2b9292f910ea191101d3a0630657 100644 (file)
@@ -11,16 +11,49 @@ import org.opendaylight.controller.md.sal.binding.api.DataBroker;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+/**
+ * Provider for AAA shiro implementation.
+ *
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ */
 public class AAAShiroProvider {
 
     private static final Logger LOG = LoggerFactory.getLogger(AAAShiroProvider.class);
 
-    private final DataBroker dataBroker;
+    private static AAAShiroProvider INSTANCE;
+    private DataBroker dataBroker;
 
-    public AAAShiroProvider(final DataBroker dataBroker) {
+    /**
+     * Provider for this bundle.
+     *
+     * @param dataBroker injected from blueprint
+     */
+    private AAAShiroProvider(final DataBroker dataBroker) {
         this.dataBroker = dataBroker;
     }
 
+    /**
+     * Singleton creation
+     *
+     * @return the Provider
+     */
+    public static AAAShiroProvider newInstance(final DataBroker dataBroker) {
+        INSTANCE = new AAAShiroProvider(dataBroker);
+        return INSTANCE;
+    }
+
+    /**
+     * Singleton extraction
+     *
+     * @return the Provider
+     */
+    public static AAAShiroProvider getInstance() {
+        if (INSTANCE == null) {
+            newInstance(null);
+        }
+        return INSTANCE;
+    }
+
     /**
      * Method called when the blueprint container is created.
      */
@@ -34,4 +67,13 @@ public class AAAShiroProvider {
     public void close() {
         LOG.info("AAAShiroProvider Closed");
     }
+
+    /**
+     * Extract the data broker.
+     *
+     * @return the data broker
+     */
+    public DataBroker getDataBroker() {
+        return this.dataBroker;
+    }
 }
\ No newline at end of file
diff --git a/aaa-shiro/impl/src/main/java/org/opendaylight/aaa/impl/shiro/realm/MDSALDynamicAuthorizationFilter.java b/aaa-shiro/impl/src/main/java/org/opendaylight/aaa/impl/shiro/realm/MDSALDynamicAuthorizationFilter.java
new file mode 100644 (file)
index 0000000..7066d27
--- /dev/null
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2017 Brocade Communications Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.shiro.realm;
+
+import com.google.common.util.concurrent.CheckedFuture;
+import com.google.common.base.Optional;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import org.apache.shiro.subject.Subject;
+import org.apache.shiro.web.filter.authz.AuthorizationFilter;
+import org.opendaylight.aaa.impl.AAAShiroProvider;
+import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.controller.md.sal.binding.api.ReadOnlyTransaction;
+import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
+import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.HttpAuthorization;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.authorization.Policies;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.permission.Permissions;
+import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provides a dynamic authorization mechanism for restful web services with permission grain
+ * scope.  <code>aaa.yang</code> defines the model for this filtering mechanism.
+ * This model exposes the ability to manipulate policy information for specific paths
+ * based on a tuple of (role, http_permission_list).
+ *
+ * This mechanism will only work when put behind <code>authcBasic</code>
+ *
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ */
+public class MDSALDynamicAuthorizationFilter extends AuthorizationFilter {
+
+    private static final Logger LOG = LoggerFactory.getLogger(MDSALDynamicAuthorizationFilter.class);
+
+    private static final InstanceIdentifier<HttpAuthorization> AUTHZ_CONTAINER_IID =
+            InstanceIdentifier.builder(HttpAuthorization.class).build();
+
+    public static Optional<HttpAuthorization> getHttpAuthzContainer(final DataBroker dataBroker)
+            throws ExecutionException, InterruptedException {
+
+        final ReadOnlyTransaction ro = dataBroker.newReadOnlyTransaction();
+        final CheckedFuture<Optional<HttpAuthorization>, ReadFailedException> result =
+                ro.read(LogicalDatastoreType.CONFIGURATION, AUTHZ_CONTAINER_IID);
+        return result.get();
+    }
+
+    @Override
+    public boolean isAccessAllowed(final ServletRequest request, final ServletResponse response,
+                                   final Object mappedValue) {
+        return isAccessAllowed(request, response, mappedValue, AAAShiroProvider.getInstance().getDataBroker());
+    }
+
+    public boolean isAccessAllowed(final ServletRequest request, final ServletResponse response,
+                                   final Object mappedValue, final DataBroker dataBroker) {
+
+        final Subject subject = getSubject(request, response);
+        final HttpServletRequest httpServletRequest = (HttpServletRequest)request;
+        final String requestURI = httpServletRequest.getRequestURI();
+        LOG.debug("isAccessAllowed for user={} to requestURI={}", subject, requestURI);
+
+        final Optional<HttpAuthorization> authorizationOptional;
+        try {
+            authorizationOptional = getHttpAuthzContainer(dataBroker);
+        } catch(ExecutionException | InterruptedException e) {
+            // Something went completely wrong trying to read the authz container.  Deny access.
+            LOG.debug("Error accessing the Http Authz Container", e);
+            return false;
+        }
+
+        if (!authorizationOptional.isPresent()) {
+            // The authorization container does not exist-- hence no authz rules are present
+            // Allow access.
+            LOG.debug("Authorization Container does not exist");
+            return true;
+        }
+
+
+        final HttpAuthorization httpAuthorization = authorizationOptional.get();
+        final Policies policies = httpAuthorization.getPolicies();
+        final List<org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.authorization.policies.Policies> policiesList =
+                policies.getPolicies();
+        if(policiesList.isEmpty()) {
+            // The authorization container exists, but no rules are present.  Allow access.
+            LOG.debug("Exiting successfully early since no authorization rules exist");
+            return true;
+        }
+
+        for (org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.authorization.policies.Policies policy :
+                policiesList) {
+            final String resource = policy.getResource();
+            final boolean pathsMatch = pathsMatch(resource, requestURI);
+            if (pathsMatch) {
+                LOG.debug("paths match for pattern={} and requestURI={}", resource, requestURI);
+                final String method = httpServletRequest.getMethod();
+                LOG.trace("method={}", method);
+                final List<Permissions> permissions = policy.getPermissions();
+                for (Permissions permission : permissions) {
+                    final String role = permission.getRole();
+                    LOG.trace("role={}", role);
+                    final List<Permissions.Actions> actions = permission.getActions();
+                    for(Permissions.Actions action : actions) {
+                        LOG.trace("action={}", action.getName());
+                        if(action.getName().equalsIgnoreCase(method)) {
+                            final boolean hasRole = subject.hasRole(role);
+                            LOG.trace("hasRole({})={}", role, hasRole);
+                            if (hasRole) {
+                                return true;
+                            }
+                        }
+                    }
+                }
+                LOG.debug("couldn't authorize the user for access");
+                return false;
+            }
+        }
+        LOG.debug("successfully authorized the user for access");
+        return true;
+    }
+}
\ No newline at end of file
index cfffb84053f50c5a980d5d4edc922ca1dcfa85f3..8f20f127dd204f2910bf72ae5e1af822aa19d28a 100644 (file)
@@ -16,7 +16,7 @@ and is available at http://www.eclipse.org/legal/epl-v10.html
     odl:type="default" />
 
   <bean id="provider"
-    class="org.opendaylight.aaa.impl.AAAShiroProvider"
+    class="org.opendaylight.aaa.impl.AAAShiroProvider" factory-method="newInstance"
     init-method="init" destroy-method="close">
     <argument ref="dataBroker" />
   </bean>
index 57203cde56cd5a3bf99343b3d16040be964e46dc..e411326062919cbc6a4562711f7ed26768794a52 100644 (file)
@@ -112,6 +112,8 @@ authcBasic = org.opendaylight.aaa.shiro.filters.ODLHttpAuthenticationFilter
 accountingListener = org.opendaylight.aaa.shiro.filters.AuthenticationListener
 securityManager.authenticator.authenticationListeners = $accountingListener
 
+# Filter to support dynamic urls rules based on md-sal model
+dynamicAuthorization = org.opendaylight.aaa.shiro.realm.MDSALDynamicAuthorizationFilter
 
 
 [urls]
@@ -120,12 +122,13 @@ securityManager.authenticator.authenticationListeners = $accountingListener
 #                                                                             #
 # This section is dedicated to defining url-based authorization according to: #
 # http://shiro.apache.org/web.html                                            #
+#                                                                             #
+# DO NOT EDIT THE FOLLOWING UNLESS YOU KNOW WHAT YOU ARE DOING!               #
 ###############################################################################
 
-# Restrict AAA endpoints to users w/ admin role
-/v1/users/** = authcBasic, roles[admin]
-/v1/domains/** = authcBasic, roles[admin]
-/v1/roles/** = authcBasic, roles[admin]
+# Temporarily added authorization endpoints;  will be removed when MDSAL based
+# Model can be initialized from file.
+/v1/** = authcBasic, roles[admin], dynamicAuthorization
 
 # Restrict AAA-Certificate REST APIs to Admin role
 /config/aaa-cert-mdsal** = authcBasic, roles[admin]
@@ -142,4 +145,4 @@ securityManager.authenticator.authenticationListeners = $accountingListener
 #/token = rest
 
 # General access through AAAFilter requires valid credentials (AuthN only).
-/** = authcBasic
\ No newline at end of file
+/** = authcBasic, dynamicAuthorization
diff --git a/aaa-shiro/impl/src/test/java/org/opendaylight/aaa/impl/shiro/realm/MDSALDynamicAuthorizationFilterTest.java b/aaa-shiro/impl/src/test/java/org/opendaylight/aaa/impl/shiro/realm/MDSALDynamicAuthorizationFilterTest.java
new file mode 100644 (file)
index 0000000..338bcb9
--- /dev/null
@@ -0,0 +1,399 @@
+/*
+ * Copyright (c) 2017 Brocade Communications Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.aaa.shiro.realm;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertNotNull;
+import static junit.framework.TestCase.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import com.google.common.base.Optional;
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.CheckedFuture;
+import com.google.common.util.concurrent.Futures;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+import org.apache.shiro.subject.Subject;
+import org.junit.Test;
+import org.opendaylight.aaa.shiro.realm.MDSALDynamicAuthorizationFilter;
+import org.opendaylight.controller.md.sal.binding.api.DataBroker;
+import org.opendaylight.controller.md.sal.binding.api.ReadOnlyTransaction;
+import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.HttpAuthorization;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.authorization.Policies;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.permission.Permissions;
+import org.opendaylight.yangtools.yang.binding.DataObject;
+
+/**
+ * Tests the Dyanmic AuthZ Filter.
+ *
+ * @author Ryan Goulding (ryandgoulding@gmail.com)
+ */
+public class MDSALDynamicAuthorizationFilterTest {
+
+    // test helper method to generate some cool mdsal data
+    private DataBroker getTestData() throws Exception {
+        return getTestData("/**", "admin", "Default Test AuthZ Rule", Permissions.Actions.Put);
+    }
+
+    // test helper method to generate some cool mdsal data
+    private DataBroker getTestData(final String resource, final String role,
+                                   final String description, final Permissions.Actions actions) throws Exception {
+
+        final List<Permissions.Actions> actionsList = Lists.newArrayList(actions);
+        final Permissions permissions = mock(Permissions.class);
+        when(permissions.getRole()).thenReturn(role);
+        when(permissions.getActions()).thenReturn(actionsList);
+        final List<Permissions> permissionsList = Lists.newArrayList(permissions);
+        final org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.authorization.policies.Policies innerPolicies =
+                mock(org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.authorization.policies.Policies.class);
+        when(innerPolicies.getResource()).thenReturn(resource);
+        when(innerPolicies.getDescription()).thenReturn(description);
+        when(innerPolicies.getPermissions()).thenReturn(permissionsList);
+        final List<org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.authorization.policies.Policies> policiesList =
+                Lists.newArrayList(innerPolicies);
+        final Policies policies = mock(Policies.class);
+        when(policies.getPolicies()).thenReturn(policiesList);
+        final HttpAuthorization httpAuthorization = mock(HttpAuthorization.class);
+        when(httpAuthorization.getPolicies()).thenReturn(policies);
+        final Optional<DataObject> dataObjectOptional = mock(Optional.class);
+        when(dataObjectOptional.get()).thenReturn(httpAuthorization);
+        when(dataObjectOptional.isPresent()).thenReturn(true);
+        final CheckedFuture<Optional<DataObject>, ReadFailedException> cf = mock(CheckedFuture.class);
+        when(cf.get()).thenReturn(dataObjectOptional);
+        final ReadOnlyTransaction rot = mock(ReadOnlyTransaction.class);
+        when(rot.read(any(), any())).thenReturn(cf);
+        final DataBroker dataBroker = mock(DataBroker.class);
+        when(dataBroker.newReadOnlyTransaction()).thenReturn(rot);
+
+        return dataBroker;
+    }
+
+    @Test
+    public void testIsAccessAllowed() throws Exception {
+        //
+        // Test Setup:
+        //
+        // Ensure that the base isAccessAllowed(...) method calls the static helper method.
+        final MDSALDynamicAuthorizationFilter filter = mock(MDSALDynamicAuthorizationFilter.class);
+        when(filter.isAccessAllowed(any(), any(), any(), any())).thenReturn(true);
+        when(filter.isAccessAllowed(any(), any(), any())).thenCallRealMethod();
+        assertTrue(filter.isAccessAllowed(null, null, null));
+        when(filter.isAccessAllowed(any(), any(), any(), any())).thenReturn(false);
+        assertFalse(filter.isAccessAllowed(null, null, null));
+    }
+
+    @Test
+    public void testGetHttpAuthzContainer() throws Exception {
+        //
+        // Test Setup:
+        //
+        // Ensure that data can be extracted appropriately.
+        final DataBroker dataBroker = getTestData();
+        final Optional<HttpAuthorization> httpAuthorizationOptional =
+                MDSALDynamicAuthorizationFilter.getHttpAuthzContainer(dataBroker);
+
+        assertNotNull(httpAuthorizationOptional);
+        final HttpAuthorization authz = httpAuthorizationOptional.get();
+        assertNotNull(authz);
+        assertEquals(1, authz.getPolicies().getPolicies().size());
+    }
+
+    @Test
+    public void testBasicAccessWithNoRules() throws Exception {
+        //
+        // Test Setup: No rules are added to the HttpAuthorization container.  Open access should be allowed.
+        final Subject subject = mock(Subject.class);
+        final MDSALDynamicAuthorizationFilter filter = new MDSALDynamicAuthorizationFilter() {
+            @Override
+            protected Subject getSubject(final ServletRequest request, final ServletResponse servletResponse) {
+                return subject;
+            }
+        };
+
+        final HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getRequestURI()).thenReturn("abc");
+        when(request.getMethod()).thenReturn("Put");
+        final Optional<DataObject> dataObjectOptional = mock(Optional.class);
+        when(dataObjectOptional.isPresent()).thenReturn(false);
+        final CheckedFuture<Optional<DataObject>, ReadFailedException> cf = mock(CheckedFuture.class);
+        when(cf.get()).thenReturn(dataObjectOptional);
+        final ReadOnlyTransaction rot = mock(ReadOnlyTransaction.class);
+        when(rot.read(any(), any())).thenReturn(cf);
+        final DataBroker dataBroker = mock(DataBroker.class);
+        when(dataBroker.newReadOnlyTransaction()).thenReturn(rot);
+
+        //
+        // Ensure that access is allowed since no data is returned from the MDSAL read.
+        // This is through making sure the Optional is not present.
+        assertTrue(filter.isAccessAllowed(request, null, null, dataBroker));
+
+        //
+        // Same as above, but with an empty policy list returned.
+        final Policies policies = mock(Policies.class);
+        when(policies.getPolicies()).thenReturn(Lists.newArrayList());
+        final HttpAuthorization httpAuthorization = mock(HttpAuthorization.class);
+        when(httpAuthorization.getPolicies()).thenReturn(policies);
+        when(dataObjectOptional.isPresent()).thenReturn(true);
+        when(dataObjectOptional.get()).thenReturn(httpAuthorization);
+        assertTrue(filter.isAccessAllowed(request, null, null, dataBroker));
+    }
+
+    @Test
+    public void testMDSALExceptionDuringRead() throws Exception {
+        //
+        // Test Setup: No rules are added to the HttpAuthorization container.  The MDSAL read
+        // is instructed to return an immediateFailedCheckedFuture, to emulate an error in reading
+        // the Data Store.
+        final Subject subject = mock(Subject.class);
+        final MDSALDynamicAuthorizationFilter filter = new MDSALDynamicAuthorizationFilter() {
+            @Override
+            protected Subject getSubject(final ServletRequest request, final ServletResponse servletResponse) {
+                return subject;
+            }
+        };
+
+        final HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getRequestURI()).thenReturn("abc");
+        when(request.getMethod()).thenReturn("Put");
+
+        final Optional<DataObject> dataObjectOptional = mock(Optional.class);
+        when(dataObjectOptional.isPresent()).thenReturn(false);
+        final CheckedFuture<Optional<DataObject>, ReadFailedException> cf =
+                Futures.immediateFailedCheckedFuture(new ReadFailedException("Test Fail"));
+        final ReadOnlyTransaction rot = mock(ReadOnlyTransaction.class);
+        when(rot.read(any(), any())).thenReturn(cf);
+        final DataBroker dataBroker = mock(DataBroker.class);
+        when(dataBroker.newReadOnlyTransaction()).thenReturn(rot);
+
+        //
+        // Ensure that if an error occurs while reading MD-SAL that access is denied.
+        assertFalse(filter.isAccessAllowed(request, null, null, dataBroker));
+    }
+
+    @Test
+    public void testBasicAccessWithOneRule() throws Exception {
+
+        //
+        // Test Setup:
+        //
+        // A Rule is added to match /** allowing HTTP PUT for the admin role.
+        // All other Methods are considered unauthorized.
+        final Subject subject = mock(Subject.class);
+        final DataBroker dataBroker = getTestData();
+        final MDSALDynamicAuthorizationFilter filter = new MDSALDynamicAuthorizationFilter() {
+            @Override
+            protected Subject getSubject(final ServletRequest request, final ServletResponse servletResponse) {
+                return subject;
+            }
+        };
+
+        final HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getRequestURI()).thenReturn("abc");
+        when(request.getMethod()).thenReturn("Put");
+        when(subject.hasRole("admin")).thenReturn(true);
+
+        //
+        // Test Case 1:
+        //
+        // Make a PUT HTTP request from a Subject with the admin role.  The request URL does not match,
+        // since "abc" does not start with a "/" character.  Since no rule exists for this particular request,
+        // then access should be allowed.
+        assertTrue(filter.isAccessAllowed(request, null, null, dataBroker));
+
+        //
+        // Test Case 2:
+        //
+        // Repeat of the above against a matching endpoint.  Access should be allowed.
+        when(request.getRequestURI()).thenReturn("/anotherexamplethatshouldwork");
+        assertTrue(filter.isAccessAllowed(request, null, null, dataBroker));
+
+        //
+        // Test Case 3:
+        //
+        // Repeat of the above request against a more complex endpoint.  Access should be allowed.
+        when(request.getRequestURI()).thenReturn("/auth/v1/users");
+        assertTrue(filter.isAccessAllowed(request, null, null, dataBroker));
+
+        //
+        // Test Case 4:
+        //
+        // Negative test case-- ensure that when an unallowed method (POST) is tried with an otherwise
+        // allowable request, that access is denied.
+        when(request.getMethod()).thenReturn("Post");
+        assertFalse(filter.isAccessAllowed(request, null, null, dataBroker));
+
+        //
+        // Test Case 5:
+        //
+        // Negative test case-- ensure that when an unallowed role is tried with an otherwise allowable
+        // request, that acess is denied.
+        when(request.getMethod()).thenReturn("Put");
+        when(subject.hasRole("admin")).thenReturn(false);
+        assertFalse(filter.isAccessAllowed(request, null, null, dataBroker));
+    }
+
+    @Test
+    public void testSeveralMatchingRules() throws Exception {
+        //
+        // Test Setup:
+        //
+        // Create some mock data which has a couple of rules which may/may not match.  This
+        // test ensures the correct application of said rules.
+        final List<Permissions.Actions> actionsList = Lists.newArrayList(Permissions.Actions.Get,
+                Permissions.Actions.Delete, Permissions.Actions.Patch, Permissions.Actions.Put,
+                Permissions.Actions.Post);
+        final String role = "admin";
+        final String resource = "/**";
+        final String resource2 = "/specialendpoint/**";
+        final String description = "All encompassing rule";
+        final Permissions permissions = mock(Permissions.class);
+        when(permissions.getRole()).thenReturn(role);
+        when(permissions.getActions()).thenReturn(actionsList);
+        final List<Permissions> permissionsList = Lists.newArrayList(permissions);
+        final org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.authorization.policies.Policies innerPolicies =
+                mock(org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.authorization.policies.Policies.class);
+        when(innerPolicies.getResource()).thenReturn(resource);
+        when(innerPolicies.getDescription()).thenReturn(description);
+        when(innerPolicies.getPermissions()).thenReturn(permissionsList);
+        final org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.authorization.policies.Policies innerPolicies2 =
+                mock(org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.authorization.policies.Policies.class);
+        when(innerPolicies2.getResource()).thenReturn(resource2);
+        final Permissions permissions2 = mock(Permissions.class);
+        when(permissions2.getRole()).thenReturn("dog");
+        when(permissions2.getActions()).thenReturn(actionsList);
+        when(innerPolicies2.getPermissions()).thenReturn(Lists.newArrayList(permissions2));
+        when(innerPolicies2.getDescription()).thenReturn("Specialized Rule");
+        List<org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.authorization.policies.Policies> policiesList =
+                Lists.newArrayList(innerPolicies, innerPolicies2);
+        final Policies policies = mock(Policies.class);
+        when(policies.getPolicies()).thenReturn(policiesList);
+        final HttpAuthorization httpAuthorization = mock(HttpAuthorization.class);
+        when(httpAuthorization.getPolicies()).thenReturn(policies);
+        final Optional<DataObject> dataObjectOptional = mock(Optional.class);
+        when(dataObjectOptional.get()).thenReturn(httpAuthorization);
+        when(dataObjectOptional.isPresent()).thenReturn(true);
+        final CheckedFuture<Optional<DataObject>, ReadFailedException> cf = mock(CheckedFuture.class);
+        when(cf.get()).thenReturn(dataObjectOptional);
+        final ReadOnlyTransaction rot = mock(ReadOnlyTransaction.class);
+        when(rot.read(any(), any())).thenReturn(cf);
+        final DataBroker dataBroker = mock(DataBroker.class);
+        when(dataBroker.newReadOnlyTransaction()).thenReturn(rot);
+
+        final Subject subject = mock(Subject.class);
+        final MDSALDynamicAuthorizationFilter filter = new MDSALDynamicAuthorizationFilter() {
+            @Override
+            protected Subject getSubject(final ServletRequest request, final ServletResponse servletResponse) {
+                return subject;
+            }
+        };
+
+        final HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getRequestURI()).thenReturn("/abc");
+        when(request.getMethod()).thenReturn("Put");
+        when(subject.hasRole("admin")).thenReturn(true);
+
+        //
+        // Test Case 1:
+        //
+        // In the setup, two rules were added.  First, make sure that the first rule is working.
+        assertTrue(filter.isAccessAllowed(request, null, null, dataBroker));
+
+        //
+        // Test Case 2:
+        //
+        // Both rules would technically match the input request URI.  We want to make sure that
+        // order is respected.  We do this by ensuring access is granted (i.e., the first rule is matched).
+        when(request.getRequestURI()).thenReturn("/specialendpoint");
+        assertTrue(filter.isAccessAllowed(request, null, null, dataBroker));
+        when(request.getRequestURI()).thenReturn("/specialendpoint/");
+        assertTrue(filter.isAccessAllowed(request, null, null, dataBroker));
+        when(request.getRequestURI()).thenReturn("/specialendpoint/somewhatextended");
+        assertTrue(filter.isAccessAllowed(request, null, null, dataBroker));
+
+        //
+        // Test Case 3:
+        //
+        // Now reverse the ordering of the rules, and ensure that access is denied (except for
+        // the first non-applicable rule, which should still be allowed).  This is
+        // because the Subject making the request is not granted the "dog" role.
+        policiesList = Lists.newArrayList(innerPolicies2, innerPolicies);
+        when(policies.getPolicies()).thenReturn(policiesList);
+        when(request.getRequestURI()).thenReturn("/abc");
+        assertTrue(filter.isAccessAllowed(request, null, null, dataBroker));
+        when(request.getRequestURI()).thenReturn("/specialendpoint");
+        assertFalse(filter.isAccessAllowed(request, null, null, dataBroker));
+        when(request.getRequestURI()).thenReturn("/specialendpoint/");
+        assertFalse(filter.isAccessAllowed(request, null, null, dataBroker));
+        when(request.getRequestURI()).thenReturn("/specialendpoint/somewhatextended");
+        assertFalse(filter.isAccessAllowed(request, null, null, dataBroker));
+    }
+
+    @Test
+    public void testMultiplePolicies() throws Exception {
+        // admin can do anything
+        final String role = "admin";
+        final String resource = "/**";
+        final String description = "Test description";
+        final List<Permissions.Actions> actionsList = Lists.newArrayList(
+                Permissions.Actions.Get, Permissions.Actions.Put, Permissions.Actions.Delete,
+                Permissions.Actions.Patch, Permissions.Actions.Post
+        );
+        final Permissions permissions = mock(Permissions.class);
+        when(permissions.getRole()).thenReturn(role);
+        when(permissions.getActions()).thenReturn(actionsList);
+        final Permissions permissions2 = mock(Permissions.class);
+        when(permissions2.getRole()).thenReturn("user");
+        when(permissions2.getActions()).thenReturn(Lists.newArrayList(Permissions.Actions.Get));
+        final List<Permissions> permissionsList = Lists.newArrayList(permissions, permissions2);
+        final org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.authorization.policies.Policies innerPolicies =
+                mock(org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.authorization.policies.Policies.class);
+        when(innerPolicies.getResource()).thenReturn(resource);
+        when(innerPolicies.getDescription()).thenReturn(description);
+        when(innerPolicies.getPermissions()).thenReturn(permissionsList);
+        final List<org.opendaylight.yang.gen.v1.urn.opendaylight.params.xml.ns.yang.aaa.rev161214.http.authorization.policies.Policies> policiesList =
+                Lists.newArrayList(innerPolicies);
+        final Policies policies = mock(Policies.class);
+        when(policies.getPolicies()).thenReturn(policiesList);
+        final HttpAuthorization httpAuthorization = mock(HttpAuthorization.class);
+        when(httpAuthorization.getPolicies()).thenReturn(policies);
+        final Optional<DataObject> dataObjectOptional = mock(Optional.class);
+        when(dataObjectOptional.get()).thenReturn(httpAuthorization);
+        when(dataObjectOptional.isPresent()).thenReturn(true);
+        final CheckedFuture<Optional<DataObject>, ReadFailedException> cf = mock(CheckedFuture.class);
+        when(cf.get()).thenReturn(dataObjectOptional);
+        final ReadOnlyTransaction rot = mock(ReadOnlyTransaction.class);
+        when(rot.read(any(), any())).thenReturn(cf);
+        final DataBroker dataBroker = mock(DataBroker.class);
+        when(dataBroker.newReadOnlyTransaction()).thenReturn(rot);
+
+        final Subject subject = mock(Subject.class);
+        final MDSALDynamicAuthorizationFilter filter = new MDSALDynamicAuthorizationFilter() {
+            @Override
+            protected Subject getSubject(final ServletRequest request, final ServletResponse servletResponse) {
+                return subject;
+            }
+        };
+
+        final HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getRequestURI()).thenReturn("/abc");
+        when(request.getMethod()).thenReturn("Put");
+        when(subject.hasRole("admin")).thenReturn(false);
+        when(subject.hasRole("user")).thenReturn(true);
+
+        assertFalse(filter.isAccessAllowed(request, null, null, dataBroker));
+        when(request.getMethod()).thenReturn("Get");
+        assertTrue(filter.isAccessAllowed(request, null, null, dataBroker));
+
+    }
+}
\ No newline at end of file
index af486e1659a2bb306e25f9e884e1f6c259865b40..9f036d87fe1b387eebb77b9a63f94098b6e52ac3 100644 (file)
@@ -19,7 +19,7 @@ import org.junit.Test;
 import org.opendaylight.aaa.shiro.realm.mapping.api.GroupsToRolesMappingStrategy;
 
 /**
- * Test ODL's default groups->roles mapping strategy.
+ * Test ODL's default groups to roles mapping strategy.
  *
  * @author Ryan Goulding (ryandgoulding@gmail.com)
  */
index 0f8026e89ac663984e758bbae82f42fbb5819dff..29406fa84d0635893ebe3249df9deee372f4ec57 100644 (file)
         <dependency>
             <groupId>org.opendaylight.aaa</groupId>
             <artifactId>aaa-shiro</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.opendaylight.aaa</groupId>
+            <artifactId>aaa-shiro-api</artifactId>
+            <version>${project.version}</version>
         </dependency>
         <dependency>
             <groupId>org.opendaylight.aaa</groupId>
index 144b99eebc1a2ff8035dbf0ad8f35bc1c175a314..a82d01136cf21aa9b8bc442b9a4215748a6f965e 100644 (file)
         <dependency>
             <groupId>org.opendaylight.aaa</groupId>
             <artifactId>aaa-shiro</artifactId>
-            <version>0.5.0-SNAPSHOT</version>
+            <version>${project.version}</version>
             <type>cfg</type>
             <classifier>configuration</classifier>
         </dependency>
         <dependency>
             <groupId>org.opendaylight.aaa</groupId>
             <artifactId>aaa-shiro</artifactId>
+            <version>0.5.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.opendaylight.aaa</groupId>
+            <artifactId>aaa-shiro-api</artifactId>
         </dependency>
         <dependency>
               <groupId>org.opendaylight.aaa</groupId>
index e8cf5f9f61c1b6f310df254876bbae86a012a046..3a5ae8597663236e0117d2293875aa4d86f699fb 100644 (file)
@@ -32,6 +32,7 @@
         <bundle>wrap:mvn:org.json/json/{{VERSION}}</bundle>
         <bundle>mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.commons-beanutils/{{VERSION}}</bundle>
 
+        <bundle>mvn:org.opendaylight.aaa/aaa-shiro-api/{{VERSION}}</bundle>
         <bundle>mvn:org.opendaylight.aaa/aaa-shiro/{{VERSION}}</bundle>
 
         <!-- AAA configuration file -->