From 213991d493438352b7cd93c17797f4740f210032 Mon Sep 17 00:00:00 2001 From: Ryan Goulding Date: Wed, 18 Jan 2017 20:18:58 -0500 Subject: [PATCH] Addition of dynamic http authorization based on particular http operations 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 --- aaa-shiro/api/src/main/yang/aaa-shiro.yang | 9 - aaa-shiro/api/src/main/yang/aaa.yang | 45 ++ aaa-shiro/impl/pom.xml | 5 + .../aaa/impl/AAAShiroProvider.java | 46 +- .../MDSALDynamicAuthorizationFilter.java | 130 ++++++ .../opendaylight/blueprint/impl-blueprint.xml | 2 +- aaa-shiro/impl/src/main/resources/shiro.ini | 13 +- .../MDSALDynamicAuthorizationFilterTest.java | 399 ++++++++++++++++++ ...ttemptGroupToRolesMappingStrategyTest.java | 2 +- features/authn/pom.xml | 6 + features/shiro/pom.xml | 7 +- features/shiro/src/main/features/features.xml | 1 + 12 files changed, 646 insertions(+), 19 deletions(-) delete mode 100644 aaa-shiro/api/src/main/yang/aaa-shiro.yang create mode 100644 aaa-shiro/api/src/main/yang/aaa.yang create mode 100644 aaa-shiro/impl/src/main/java/org/opendaylight/aaa/impl/shiro/realm/MDSALDynamicAuthorizationFilter.java create mode 100644 aaa-shiro/impl/src/test/java/org/opendaylight/aaa/impl/shiro/realm/MDSALDynamicAuthorizationFilterTest.java 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 index 063c2b62b..000000000 --- a/aaa-shiro/api/src/main/yang/aaa-shiro.yang +++ /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 index 000000000..31b29f67d --- /dev/null +++ b/aaa-shiro/api/src/main/yang/aaa.yang @@ -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; + } + } + } +} diff --git a/aaa-shiro/impl/pom.xml b/aaa-shiro/impl/pom.xml index 88a6b675b..4c5848693 100644 --- a/aaa-shiro/impl/pom.xml +++ b/aaa-shiro/impl/pom.xml @@ -113,6 +113,11 @@ and is available at http://www.eclipse.org/legal/epl-v10.html logback-classic test + + org.apache.shiro + shiro-core + 1.3.1 + diff --git a/aaa-shiro/impl/src/main/java/org/opendaylight/aaa/impl/AAAShiroProvider.java b/aaa-shiro/impl/src/main/java/org/opendaylight/aaa/impl/AAAShiroProvider.java index 953695476..d21b353b5 100644 --- a/aaa-shiro/impl/src/main/java/org/opendaylight/aaa/impl/AAAShiroProvider.java +++ b/aaa-shiro/impl/src/main/java/org/opendaylight/aaa/impl/AAAShiroProvider.java @@ -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 index 000000000..7066d27a3 --- /dev/null +++ b/aaa-shiro/impl/src/main/java/org/opendaylight/aaa/impl/shiro/realm/MDSALDynamicAuthorizationFilter.java @@ -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. aaa.yang 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 authcBasic + * + * @author Ryan Goulding (ryandgoulding@gmail.com) + */ +public class MDSALDynamicAuthorizationFilter extends AuthorizationFilter { + + private static final Logger LOG = LoggerFactory.getLogger(MDSALDynamicAuthorizationFilter.class); + + private static final InstanceIdentifier AUTHZ_CONTAINER_IID = + InstanceIdentifier.builder(HttpAuthorization.class).build(); + + public static Optional getHttpAuthzContainer(final DataBroker dataBroker) + throws ExecutionException, InterruptedException { + + final ReadOnlyTransaction ro = dataBroker.newReadOnlyTransaction(); + final CheckedFuture, 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 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 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 = policy.getPermissions(); + for (Permissions permission : permissions) { + final String role = permission.getRole(); + LOG.trace("role={}", role); + final List 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 diff --git a/aaa-shiro/impl/src/main/resources/org/opendaylight/blueprint/impl-blueprint.xml b/aaa-shiro/impl/src/main/resources/org/opendaylight/blueprint/impl-blueprint.xml index cfffb8405..8f20f127d 100644 --- a/aaa-shiro/impl/src/main/resources/org/opendaylight/blueprint/impl-blueprint.xml +++ b/aaa-shiro/impl/src/main/resources/org/opendaylight/blueprint/impl-blueprint.xml @@ -16,7 +16,7 @@ and is available at http://www.eclipse.org/legal/epl-v10.html odl:type="default" /> diff --git a/aaa-shiro/impl/src/main/resources/shiro.ini b/aaa-shiro/impl/src/main/resources/shiro.ini index 57203cde5..e41132606 100644 --- a/aaa-shiro/impl/src/main/resources/shiro.ini +++ b/aaa-shiro/impl/src/main/resources/shiro.ini @@ -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 index 000000000..338bcb997 --- /dev/null +++ b/aaa-shiro/impl/src/test/java/org/opendaylight/aaa/impl/shiro/realm/MDSALDynamicAuthorizationFilterTest.java @@ -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 actionsList = Lists.newArrayList(actions); + final Permissions permissions = mock(Permissions.class); + when(permissions.getRole()).thenReturn(role); + when(permissions.getActions()).thenReturn(actionsList); + final List 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 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 dataObjectOptional = mock(Optional.class); + when(dataObjectOptional.get()).thenReturn(httpAuthorization); + when(dataObjectOptional.isPresent()).thenReturn(true); + final CheckedFuture, 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 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 dataObjectOptional = mock(Optional.class); + when(dataObjectOptional.isPresent()).thenReturn(false); + final CheckedFuture, 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 dataObjectOptional = mock(Optional.class); + when(dataObjectOptional.isPresent()).thenReturn(false); + final CheckedFuture, 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 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 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 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 dataObjectOptional = mock(Optional.class); + when(dataObjectOptional.get()).thenReturn(httpAuthorization); + when(dataObjectOptional.isPresent()).thenReturn(true); + final CheckedFuture, 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 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 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 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 dataObjectOptional = mock(Optional.class); + when(dataObjectOptional.get()).thenReturn(httpAuthorization); + when(dataObjectOptional.isPresent()).thenReturn(true); + final CheckedFuture, 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 diff --git a/aaa-shiro/impl/src/test/java/org/opendaylight/aaa/shiro/realm/mapping/impl/BestAttemptGroupToRolesMappingStrategyTest.java b/aaa-shiro/impl/src/test/java/org/opendaylight/aaa/shiro/realm/mapping/impl/BestAttemptGroupToRolesMappingStrategyTest.java index af486e165..9f036d87f 100644 --- a/aaa-shiro/impl/src/test/java/org/opendaylight/aaa/shiro/realm/mapping/impl/BestAttemptGroupToRolesMappingStrategyTest.java +++ b/aaa-shiro/impl/src/test/java/org/opendaylight/aaa/shiro/realm/mapping/impl/BestAttemptGroupToRolesMappingStrategyTest.java @@ -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) */ diff --git a/features/authn/pom.xml b/features/authn/pom.xml index 0f8026e89..29406fa84 100644 --- a/features/authn/pom.xml +++ b/features/authn/pom.xml @@ -155,6 +155,12 @@ org.opendaylight.aaa aaa-shiro + ${project.version} + + + org.opendaylight.aaa + aaa-shiro-api + ${project.version} org.opendaylight.aaa diff --git a/features/shiro/pom.xml b/features/shiro/pom.xml index 144b99eeb..a82d01136 100644 --- a/features/shiro/pom.xml +++ b/features/shiro/pom.xml @@ -48,13 +48,18 @@ org.opendaylight.aaa aaa-shiro - 0.5.0-SNAPSHOT + ${project.version} cfg configuration org.opendaylight.aaa aaa-shiro + 0.5.0-SNAPSHOT + + + org.opendaylight.aaa + aaa-shiro-api org.opendaylight.aaa diff --git a/features/shiro/src/main/features/features.xml b/features/shiro/src/main/features/features.xml index e8cf5f9f6..3a5ae8597 100644 --- a/features/shiro/src/main/features/features.xml +++ b/features/shiro/src/main/features/features.xml @@ -32,6 +32,7 @@ wrap:mvn:org.json/json/{{VERSION}} mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.commons-beanutils/{{VERSION}} + mvn:org.opendaylight.aaa/aaa-shiro-api/{{VERSION}} mvn:org.opendaylight.aaa/aaa-shiro/{{VERSION}} -- 2.36.6