From ab61c85ddc5eae33e68cf41f1d472b311097ca7e Mon Sep 17 00:00:00 2001
From: cramakri <cramakri>
Date: Tue, 21 Sep 2010 08:04:12 +0000
Subject: [PATCH] LMS-1702 Annotation-based authentication for DSS Services.

SVN: 17923
---
 .../DssServiceRpcAuthorizationAdvisor.java    |  76 +++++++-
 .../client/api/v1/impl/DssComponentTest.java  | 178 ++++++++++++++++--
 2 files changed, 236 insertions(+), 18 deletions(-)

diff --git a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DssServiceRpcAuthorizationAdvisor.java b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DssServiceRpcAuthorizationAdvisor.java
index 5c1ed2a384f..e5a5c410265 100644
--- a/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DssServiceRpcAuthorizationAdvisor.java
+++ b/datastore_server/source/java/ch/systemsx/cisd/openbis/dss/generic/server/DssServiceRpcAuthorizationAdvisor.java
@@ -18,16 +18,32 @@ package ch.systemsx.cisd.openbis.dss.generic.server;
 
 import org.aopalliance.intercept.MethodInterceptor;
 import org.aopalliance.intercept.MethodInvocation;
+import org.apache.log4j.Logger;
 import org.springframework.aop.support.DefaultPointcutAdvisor;
 import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
 
+import ch.systemsx.cisd.common.exceptions.AuthorizationFailureException;
+import ch.systemsx.cisd.common.logging.LogCategory;
+import ch.systemsx.cisd.common.logging.LogFactory;
+import ch.systemsx.cisd.common.utilities.MethodUtils;
 import ch.systemsx.cisd.openbis.dss.generic.shared.api.authorization.DataSetAccessGuard;
+import ch.systemsx.cisd.openbis.dss.generic.shared.api.v1.DataSetFileDTO;
 
 /**
  * The advisor for authorization in the DSS RPC interfaces.
  * <p>
- * This AOP advisor ensures that invocations to the DSS RPC services pass the authorization
- * requirements.
+ * This AOP advisor ensures that invocations of methods on DSS RPC services with the
+ * {@link DataSetAccessGuard} annotation conform to the following the authorization requirements:
+ * <ul>
+ * <li>The session token (String) is the the first parameter</li>
+ * <li>The second parameter is either a data set code (String) or a DataSetFileDTO object</li>
+ * <li>The user who owns the session token (first argument) has access to the specified data set
+ * code (second argument)</li>
+ * </ul>
+ * <p>
+ * It does this check by invoking the method
+ * {@link AbstractDssServiceRpc#isDatasetAccessible(String, String)} on the receiver of the method
+ * containing the join point.
  * <p>
  * Though it is not necessary to subclass DefaultPointcutAdvisor for the implementation, we subclass
  * here because to make the configuration in spring a bit simpler.
@@ -38,6 +54,9 @@ public class DssServiceRpcAuthorizationAdvisor extends DefaultPointcutAdvisor
 {
     private static final long serialVersionUID = 1L;
 
+    private static final Logger authorizationLog =
+            LogFactory.getLogger(LogCategory.AUTH, DssServiceRpcAuthorizationAdvisor.class);
+
     /**
      * The public constructor.
      */
@@ -61,9 +80,62 @@ public class DssServiceRpcAuthorizationAdvisor extends DefaultPointcutAdvisor
 
         public Object invoke(MethodInvocation methodInvocation) throws Throwable
         {
+            final Object[] args = methodInvocation.getArguments();
+            String sessionToken = getSessionToken(args);
+            String dataSetCode = getDataSetCode(args, methodInvocation);
+            AbstractDssServiceRpc recv = (AbstractDssServiceRpc) methodInvocation.getThis();
+            if (false == recv.isDatasetAccessible(sessionToken, dataSetCode))
+            {
+                authorizationLog.info(String.format(
+                        "[SESSION:'%s' DATA_SET:%s]: Authorization failure while "
+                                + "invoking method '%s'", sessionToken, dataSetCode, MethodUtils
+                                .describeMethod(methodInvocation.getMethod())));
+
+                throw new AuthorizationFailureException("User does not have access to data set "
+                        + dataSetCode);
+            }
+
             return methodInvocation.proceed();
         }
 
+        /**
+         * Get the session token from the MethodInvocation
+         */
+        private static String getSessionToken(final Object[] args)
+        {
+            final int len = args.length;
+            assert len > 0 : "The session token should be the first argument.";
+            final Object firstObject = args[0];
+            assert firstObject instanceof String : "The session token should be the first argument.";
+
+            return (String) firstObject;
+        }
+
+        private static String getDataSetCode(final Object[] args, MethodInvocation methodInvocation)
+        {
+            final int len = args.length;
+            assert len > 1 : "The data set code should be contained in the second argument.";
+            final Object secondObject = args[1];
+            String dataSetCode;
+            if (secondObject instanceof String)
+            {
+                dataSetCode = (String) secondObject;
+            } else if (secondObject instanceof DataSetFileDTO)
+            {
+                DataSetFileDTO dsFile = (DataSetFileDTO) secondObject;
+                dataSetCode = dsFile.getDataSetCode();
+            } else
+            {
+                String methodDesc = MethodUtils.describeMethod(methodInvocation.getMethod());
+                authorizationLog.info(String.format("Authorization failure while "
+                        + "invoking method '%s': %s", methodDesc));
+
+                throw new IllegalArgumentException("Method (" + methodDesc
+                        + ") does not include the data set code as an argument");
+            }
+
+            return dataSetCode;
+        }
     }
 
 }
diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/client/api/v1/impl/DssComponentTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/client/api/v1/impl/DssComponentTest.java
index c6d1f33bcee..fa350052e22 100644
--- a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/client/api/v1/impl/DssComponentTest.java
+++ b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/dss/client/api/v1/impl/DssComponentTest.java
@@ -27,29 +27,47 @@ import java.util.Random;
 import org.apache.commons.io.IOUtils;
 import org.jmock.Expectations;
 import org.jmock.Mockery;
+import org.springframework.aop.Advisor;
+import org.springframework.aop.TargetSource;
+import org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.testng.annotations.AfterMethod;
 import org.testng.annotations.BeforeMethod;
 import org.testng.annotations.Test;
 
+import ch.systemsx.cisd.base.exceptions.IOExceptionUnchecked;
 import ch.systemsx.cisd.base.tests.AbstractFileSystemTestCase;
+import ch.systemsx.cisd.common.api.IRpcService;
 import ch.systemsx.cisd.common.api.IRpcServiceFactory;
 import ch.systemsx.cisd.common.api.RpcServiceInterfaceDTO;
 import ch.systemsx.cisd.common.api.RpcServiceInterfaceVersionDTO;
+import ch.systemsx.cisd.common.exceptions.AuthorizationFailureException;
 import ch.systemsx.cisd.openbis.dss.client.api.v1.IDataSetDss;
 import ch.systemsx.cisd.openbis.dss.client.api.v1.impl.DssComponent;
+import ch.systemsx.cisd.openbis.dss.generic.server.AbstractDssServiceRpc;
+import ch.systemsx.cisd.openbis.dss.generic.server.DssServiceRpcAuthorizationAdvisor;
+import ch.systemsx.cisd.openbis.dss.generic.shared.IEncapsulatedOpenBISService;
+import ch.systemsx.cisd.openbis.dss.generic.shared.api.v1.DataSetFileDTO;
 import ch.systemsx.cisd.openbis.dss.generic.shared.api.v1.DataStoreApiUrlUtilities;
 import ch.systemsx.cisd.openbis.dss.generic.shared.api.v1.FileInfoDssBuilder;
 import ch.systemsx.cisd.openbis.dss.generic.shared.api.v1.FileInfoDssDTO;
 import ch.systemsx.cisd.openbis.dss.generic.shared.api.v1.IDssServiceRpcGeneric;
+import ch.systemsx.cisd.openbis.dss.generic.shared.api.v1.NewDataSetDTO;
 import ch.systemsx.cisd.openbis.generic.shared.IETLLIMSService;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.DataStore;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.ExternalData;
 import ch.systemsx.cisd.openbis.generic.shared.dto.SessionContextDTO;
 
 /**
+ * A test of the DSS component.
+ * 
  * @author Chandrasekhar Ramakrishnan
  */
 public class DssComponentTest extends AbstractFileSystemTestCase
 {
+    private static final String DUMMY_DATA_SET_CODE = "DummyDataSetCode";
+
     private Mockery context;
 
     private IETLLIMSService openBisService;
@@ -65,6 +83,34 @@ public class DssComponentTest extends AbstractFileSystemTestCase
     private static final String DUMMY_DSS_URL =
             DataStoreApiUrlUtilities.getDataStoreUrlFromServerUrl("http://localhost/");
 
+    private IDssServiceRpcGeneric dssService;
+
+    @SuppressWarnings("unchecked")
+    public static <T extends IRpcService> T getAdvisedDssService(final T service)
+    {
+        final Advisor advisor = new DssServiceRpcAuthorizationAdvisor();
+        final BeanPostProcessor processor = new AbstractAutoProxyCreator()
+            {
+                private static final long serialVersionUID = 1L;
+
+                //
+                // AbstractAutoProxyCreator
+                //
+                @Override
+                protected final Object[] getAdvicesAndAdvisorsForBean(final Class beanClass,
+                        final String beanName, final TargetSource customTargetSource)
+                        throws BeansException
+                {
+                    return new Object[]
+                        { advisor };
+                }
+            };
+        final Object proxy =
+                processor.postProcessAfterInitialization(service, "proxy of "
+                        + service.getClass().getName());
+        return (T) proxy;
+    }
+
     public DssComponentTest()
     {
 
@@ -82,6 +128,13 @@ public class DssComponentTest extends AbstractFileSystemTestCase
         randomDataFile = getFileWithRandomData(1);
     }
 
+    @AfterMethod
+    public void tearDown()
+    {
+        // Clear the dss service
+        dssService = null;
+    }
+
     @Test
     public void testLogin()
     {
@@ -106,7 +159,7 @@ public class DssComponentTest extends AbstractFileSystemTestCase
         setupExpectations();
 
         dssComponent.login("foo", "bar");
-        IDataSetDss dataSetProxy = dssComponent.getDataSet("DummyDataSetCode");
+        IDataSetDss dataSetProxy = dssComponent.getDataSet(DUMMY_DATA_SET_CODE);
         FileInfoDssDTO[] fileInfos = dataSetProxy.listFiles("/", true);
         assertEquals(1, fileInfos.length);
 
@@ -118,22 +171,41 @@ public class DssComponentTest extends AbstractFileSystemTestCase
     {
         dssComponent = new DssComponent(openBisService, dssServiceFactory, DUMMY_SESSSION_TOKEN);
         setupExpectationsNoLogin();
-        IDataSetDss dataSetProxy = dssComponent.getDataSet("DummyDataSetCode");
+        IDataSetDss dataSetProxy = dssComponent.getDataSet(DUMMY_DATA_SET_CODE);
         FileInfoDssDTO[] fileInfos = dataSetProxy.listFiles("/", true);
         assertEquals(1, fileInfos.length);
 
         context.assertIsSatisfied();
     }
 
+    @Test
+    public void testListDataSetFilesUnauthorized() throws IOException
+    {
+        setupExpectations(false);
+        dssComponent.login("foo", "bar");
+        try
+        {
+            IDataSetDss dataSetProxy = dssComponent.getDataSet(DUMMY_DATA_SET_CODE);
+            dataSetProxy.listFiles("/", true);
+            fail("Unauthorized access to data set should have thrown an exception.");
+        } catch (AuthorizationFailureException ex)
+        {
+            assertEquals("Authorization failure: User does not have access to data set "
+                    + DUMMY_DATA_SET_CODE + ".", ex.getMessage());
+        }
+
+        context.assertIsSatisfied();
+    }
+
     @Test
     public void testUnsupportedInterface() throws IOException
     {
-        setupExpectations("Some Server Interface", true);
+        setupExpectations("Some Server Interface", true, true);
 
         dssComponent.login("foo", "bar");
         try
         {
-            dssComponent.getDataSet("DummyDataSetCode");
+            dssComponent.getDataSet(DUMMY_DATA_SET_CODE);
             fail("Should have thrown an IllegalArgumentException");
         } catch (IllegalArgumentException e)
         {
@@ -149,7 +221,7 @@ public class DssComponentTest extends AbstractFileSystemTestCase
         setupExpectations();
 
         dssComponent.login("foo", "bar");
-        IDataSetDss dataSetProxy = dssComponent.getDataSet("DummyDataSetCode");
+        IDataSetDss dataSetProxy = dssComponent.getDataSet(DUMMY_DATA_SET_CODE);
         FileInfoDssDTO[] fileInfos = dataSetProxy.listFiles("/", true);
         FileInfoDssDTO fileFileInfo = null;
         for (FileInfoDssDTO fid : fileInfos)
@@ -178,22 +250,28 @@ public class DssComponentTest extends AbstractFileSystemTestCase
 
     private void setupExpectations() throws IOException
     {
-        setupExpectations(IDssServiceRpcGeneric.DSS_SERVICE_NAME, true);
+        setupExpectations(true);
+    }
+
+    private void setupExpectations(boolean isDataSetAccessible) throws IOException
+    {
+        setupExpectations(IDssServiceRpcGeneric.DSS_SERVICE_NAME, true, isDataSetAccessible);
     }
 
     private void setupExpectationsNoLogin() throws IOException
     {
-        setupExpectations(IDssServiceRpcGeneric.DSS_SERVICE_NAME, false);
+        setupExpectations(IDssServiceRpcGeneric.DSS_SERVICE_NAME, false, true);
     }
 
-    private void setupExpectations(String serviceName, final boolean needsLogin) throws IOException
+    private void setupExpectations(String serviceName, final boolean needsLogin,
+            boolean isDataSetAccessible) throws IOException
     {
         final SessionContextDTO session = getDummySession();
         final ExternalData dataSetExternalData = new ExternalData();
         DataStore dataStore = new DataStore();
         dataStore.setDownloadUrl(DUMMY_DSS_URL);
         dataSetExternalData.setDataStore(dataStore);
-        final IDssServiceRpcGeneric dssService = context.mock(IDssServiceRpcGeneric.class);
+
         ArrayList<FileInfoDssDTO> list = new ArrayList<FileInfoDssDTO>();
         FileInfoDssBuilder builder =
                 new FileInfoDssBuilder(workingDirectory.getCanonicalPath(), workingDirectory
@@ -202,6 +280,10 @@ public class DssComponentTest extends AbstractFileSystemTestCase
         final FileInfoDssDTO[] fileInfos = new FileInfoDssDTO[list.size()];
         list.toArray(fileInfos);
 
+        dssService =
+                getAdvisedDssService(new MockDssServiceRpc(null, fileInfos, new FileInputStream(
+                        randomDataFile), isDataSetAccessible));
+
         final ArrayList<RpcServiceInterfaceDTO> ifaces = new ArrayList<RpcServiceInterfaceDTO>(1);
         final RpcServiceInterfaceDTO iface = new RpcServiceInterfaceDTO(serviceName);
         final RpcServiceInterfaceVersionDTO ifaceVersion =
@@ -214,7 +296,7 @@ public class DssComponentTest extends AbstractFileSystemTestCase
         context.checking(new Expectations()
             {
                 {
-                    final String dataSetCode = "DummyDataSetCode";
+                    final String dataSetCode = DUMMY_DATA_SET_CODE;
 
                     if (needsLogin)
                     {
@@ -228,12 +310,6 @@ public class DssComponentTest extends AbstractFileSystemTestCase
                     allowing(dssServiceFactory).getService(ifaceVersion,
                             IDssServiceRpcGeneric.class, DUMMY_DSS_URL, false);
                     will(returnValue(dssService));
-                    allowing(dssService).listFilesForDataSet(DUMMY_SESSSION_TOKEN, dataSetCode,
-                            "/", true);
-                    will(returnValue(fileInfos));
-                    allowing(dssService).getFileForDataSet(DUMMY_SESSSION_TOKEN, dataSetCode,
-                            "/random.txt");
-                    will(returnValue(new FileInputStream(randomDataFile)));
                 }
             });
     }
@@ -273,4 +349,74 @@ public class DssComponentTest extends AbstractFileSystemTestCase
         return file;
     }
 
+    private static class MockDssServiceRpc extends AbstractDssServiceRpc implements
+            IDssServiceRpcGeneric
+    {
+        private final FileInfoDssDTO[] fileInfos;
+
+        private final FileInputStream fileInputStream;
+
+        private final boolean isDataSetAccessible;
+
+        /**
+         * @param openBISService
+         */
+        public MockDssServiceRpc(IEncapsulatedOpenBISService openBISService,
+                FileInfoDssDTO[] fileInfos, FileInputStream fileInputStream,
+                boolean isDataSetAccessible)
+        {
+            super(openBISService);
+            this.fileInfos = fileInfos;
+            this.fileInputStream = fileInputStream;
+            this.isDataSetAccessible = isDataSetAccessible;
+        }
+
+        public InputStream getFileForDataSet(String sessionToken, DataSetFileDTO fileOrFolder)
+                throws IOExceptionUnchecked, IllegalArgumentException
+        {
+            return fileInputStream;
+        }
+
+        public InputStream getFileForDataSet(String sessionToken, String dataSetCode, String path)
+                throws IOExceptionUnchecked, IllegalArgumentException
+        {
+            return fileInputStream;
+        }
+
+        public FileInfoDssDTO[] listFilesForDataSet(String sessionToken, DataSetFileDTO fileOrFolder)
+                throws IOExceptionUnchecked, IllegalArgumentException
+        {
+            return fileInfos;
+        }
+
+        public FileInfoDssDTO[] listFilesForDataSet(String sessionToken, String dataSetCode,
+                String path, boolean isRecursive) throws IOExceptionUnchecked,
+                IllegalArgumentException
+        {
+            return fileInfos;
+        }
+
+        public String putDataSet(String sessionToken, NewDataSetDTO newDataset,
+                InputStream inputStream) throws IOExceptionUnchecked, IllegalArgumentException
+        {
+            return null;
+        }
+
+        public int getMajorVersion()
+        {
+            return 1;
+        }
+
+        public int getMinorVersion()
+        {
+            return 0;
+        }
+
+        @Override
+        protected boolean isDatasetAccessible(String sessionToken, String dataSetCode)
+        {
+            return isDataSetAccessible;
+        }
+    }
+
 }
-- 
GitLab