From aad198494e6ae3ff83562fb89adc5913adff47cf Mon Sep 17 00:00:00 2001
From: brinn <brinn>
Date: Tue, 24 Jul 2012 22:30:59 +0000
Subject: [PATCH] [BIS-137] Implement a more secure SampleListPredicate and
 replace SamplePredicate with a version that uses this new code. The new code
 checks all aspects of the Sample object, rather than just checking the sample
 identifier.

SVN: 26183
---
 .../api/v1/GeneralInformationService.java     |   7 +-
 .../AuthorizationServiceUtils.java            |  47 +++--
 .../predicate/SampleListPredicate.java        | 193 ++++++++++++++++++
 .../predicate/SamplePredicate.java            |  25 ++-
 .../remoteapitest/RemoteApiTestCase.java      |   1 +
 5 files changed, 247 insertions(+), 26 deletions(-)
 create mode 100644 openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/SampleListPredicate.java
 rename openbis/source/java/ch/systemsx/cisd/openbis/generic/{shared => server}/authorization/predicate/SamplePredicate.java (51%)

diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/api/v1/GeneralInformationService.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/api/v1/GeneralInformationService.java
index a4e6fab166d..5f9ce1a4b68 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/api/v1/GeneralInformationService.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/api/v1/GeneralInformationService.java
@@ -39,6 +39,8 @@ import ch.systemsx.cisd.common.exceptions.UserFailureException;
 import ch.systemsx.cisd.common.spring.IInvocationLoggerContext;
 import ch.systemsx.cisd.openbis.generic.server.AbstractServer;
 import ch.systemsx.cisd.openbis.generic.server.ComponentNames;
+import ch.systemsx.cisd.openbis.generic.server.authorization.predicate.SampleListPredicate;
+import ch.systemsx.cisd.openbis.generic.server.authorization.predicate.SamplePredicate;
 import ch.systemsx.cisd.openbis.generic.server.business.IPropertiesBatchManager;
 import ch.systemsx.cisd.openbis.generic.server.business.bo.ICommonBusinessObjectFactory;
 import ch.systemsx.cisd.openbis.generic.server.business.bo.fetchoptions.datasetlister.DataSetLister;
@@ -76,7 +78,6 @@ import ch.systemsx.cisd.openbis.generic.shared.authorization.annotation.RolesAll
 import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.ExperimentIdentifierPredicate;
 import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.ExperimentPredicate;
 import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.ProjectPredicate;
-import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.SamplePredicate;
 import ch.systemsx.cisd.openbis.generic.shared.authorization.validator.DataSetByExperimentIdentifierValidator;
 import ch.systemsx.cisd.openbis.generic.shared.authorization.validator.ExperimentByIdentiferValidator;
 import ch.systemsx.cisd.openbis.generic.shared.authorization.validator.ProjectByIdentiferValidator;
@@ -422,7 +423,7 @@ public class GeneralInformationService extends AbstractServer<IGeneralInformatio
     @RolesAllowed(RoleWithHierarchy.SPACE_OBSERVER)
     @ReturnValueFilter(validatorClass = DataSetByExperimentIdentifierValidator.class)
     public List<DataSet> listDataSets(String sessionToken,
-            @AuthorizationGuard(guardClass = SamplePredicate.class)
+            @AuthorizationGuard(guardClass = SampleListPredicate.class)
             List<Sample> samples)
     {
         return listDataSets(sessionToken, samples, EnumSet.noneOf(Connections.class));
@@ -651,7 +652,7 @@ public class GeneralInformationService extends AbstractServer<IGeneralInformatio
     @RolesAllowed(RoleWithHierarchy.SPACE_OBSERVER)
     @ReturnValueFilter(validatorClass = DataSetByExperimentIdentifierValidator.class)
     public List<DataSet> listDataSets(String sessionToken,
-            @AuthorizationGuard(guardClass = SamplePredicate.class)
+            @AuthorizationGuard(guardClass = SampleListPredicate.class)
             List<Sample> samples, EnumSet<Connections> connections)
     {
         checkSession(sessionToken);
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/AuthorizationServiceUtils.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/AuthorizationServiceUtils.java
index 03a828a5359..269bac9b05a 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/AuthorizationServiceUtils.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/AuthorizationServiceUtils.java
@@ -24,13 +24,16 @@ import ch.systemsx.cisd.common.exceptions.StatusFlag;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDAOFactory;
 import ch.systemsx.cisd.openbis.generic.shared.authorization.RoleWithIdentifier;
 import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.DataSetCodePredicate;
+import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.DelegatedPredicate;
 import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.ExperimentIdentifierPredicate;
-import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.SamplePredicate;
+import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.SampleOwnerIdentifierPredicate;
 import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.SpaceIdentifierPredicate;
 import ch.systemsx.cisd.openbis.generic.shared.basic.IIdentifierHolder;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.RoleWithHierarchy;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.RoleWithHierarchy.RoleLevel;
 import ch.systemsx.cisd.openbis.generic.shared.dto.PersonPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifierFactory;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleOwnerIdentifier;
 import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SpaceIdentifier;
 
 /**
@@ -175,13 +178,15 @@ public class AuthorizationServiceUtils
 
     public List<String> filterSampleIds(String user, List<String> sampleIds)
     {
-        PersonPE person = getUserByName(user);
-        List<RoleWithIdentifier> userRoles = DefaultAccessController.getUserRoles(person);
+        final DelegatedPredicate<SampleOwnerIdentifier, IIdentifierHolder> predicate =
+                createSampleOwnerPredicate();
+        final PersonPE person = getUserByName(user);
+        final List<RoleWithIdentifier> userRoles = DefaultAccessController.getUserRoles(person);
 
-        LinkedList<String> resultList = new LinkedList<String>();
+        final LinkedList<String> resultList = new LinkedList<String>();
         for (String sampleIdentifier : sampleIds)
         {
-            if (canAccessSample(person, userRoles, sampleIdentifier))
+            if (canAccessSample(predicate, person, userRoles, sampleIdentifier))
             {
                 resultList.add(sampleIdentifier);
             }
@@ -189,14 +194,11 @@ public class AuthorizationServiceUtils
         return resultList;
     }
 
-    private boolean canAccessSample(PersonPE person, List<RoleWithIdentifier> allowedRoles,
+    private boolean canAccessSample(
+            final DelegatedPredicate<SampleOwnerIdentifier, IIdentifierHolder> predicate,
+            final PersonPE person, final List<RoleWithIdentifier> allowedRoles,
             final String sampleIdentifier)
     {
-
-        SamplePredicate predicate = new SamplePredicate();
-
-        predicate.init(new AuthorizationDataProvider(daoFactory));
-
         final Status status = predicate.evaluate(person, allowedRoles, new IIdentifierHolder()
             {
                 @Override
@@ -206,7 +208,28 @@ public class AuthorizationServiceUtils
                 }
             });
 
-        return (status.getFlag().equals(StatusFlag.OK));
+        return Status.OK.equals(status);
     }
 
+    private DelegatedPredicate<SampleOwnerIdentifier, IIdentifierHolder> createSampleOwnerPredicate()
+    {
+        final DelegatedPredicate<SampleOwnerIdentifier, IIdentifierHolder> predicate =
+                new DelegatedPredicate<SampleOwnerIdentifier, IIdentifierHolder>(
+                        new SampleOwnerIdentifierPredicate())
+                    {
+                        @Override
+                        public SampleOwnerIdentifier tryConvert(IIdentifierHolder value)
+                        {
+                            return SampleIdentifierFactory.parse(value.getIdentifier());
+                        }
+
+                        @Override
+                        public String getCandidateDescription()
+                        {
+                            return "sample";
+                        }
+                    };
+        predicate.init(new AuthorizationDataProvider(daoFactory));
+        return predicate;
+    }
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/SampleListPredicate.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/SampleListPredicate.java
new file mode 100644
index 00000000000..69f0d8eeee9
--- /dev/null
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/SampleListPredicate.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2010 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.systemsx.cisd.openbis.generic.server.authorization.predicate;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import net.lemnik.eodsql.BaseQuery;
+import net.lemnik.eodsql.QueryTool;
+import net.lemnik.eodsql.Select;
+
+import ch.systemsx.cisd.common.exceptions.AuthorizationFailureException;
+import ch.systemsx.cisd.common.exceptions.Status;
+import ch.systemsx.cisd.openbis.generic.server.dataaccess.db.LongArrayMapper;
+import ch.systemsx.cisd.openbis.generic.server.dataaccess.db.StringArrayMapper;
+import ch.systemsx.cisd.openbis.generic.shared.api.v1.dto.Sample;
+import ch.systemsx.cisd.openbis.generic.shared.authorization.IAuthorizationDataProvider;
+import ch.systemsx.cisd.openbis.generic.shared.authorization.RoleWithIdentifier;
+import ch.systemsx.cisd.openbis.generic.shared.authorization.annotation.ShouldFlattenCollections;
+import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.AbstractSpacePredicate;
+import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.SampleOwnerIdentifierPredicate;
+import ch.systemsx.cisd.openbis.generic.shared.dto.PersonPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifierFactory;
+import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleOwnerIdentifier;
+
+/**
+ * A predicate for lists of entities of {@link Sample}s. This
+ * predicate authorizes for read-only access, i.e. it will allow access to shared samples for all
+ * users.
+ * <p>
+ * <i>This is an internal class. Do not use it as a user of the API.</i>
+ * 
+ * @author Bernd Rinn
+ */
+@ShouldFlattenCollections(value = false)
+public class SampleListPredicate extends AbstractSpacePredicate<List<Sample>>
+{
+    private final SampleOwnerIdentifierPredicate idOwnerPredicate;
+
+    public SampleListPredicate()
+    {
+        idOwnerPredicate = new SampleOwnerIdentifierPredicate();
+    }
+
+    //
+    // AbstractPredicate
+    //
+
+    @Override
+    public final void init(IAuthorizationDataProvider provider)
+    {
+        super.init(provider);
+        idOwnerPredicate.init(provider);
+    }
+
+    @Override
+    public String getCandidateDescription()
+    {
+        return "sample";
+    }
+
+    @Override
+    protected Status doEvaluation(PersonPE person, List<RoleWithIdentifier> allowedRoles,
+            List<Sample> samples)
+    {
+        // All fields relevant for authorization are expected to be filled:
+        // - technical id
+        // - permanent id
+        // - space code
+        // - space identifier
+        final List<Long> ids = new ArrayList<Long>(samples.size());
+        final List<String> permIds = new ArrayList<String>(samples.size());
+        for (Sample sample : samples)
+        {
+            if (sample.getId() == null)
+            {
+                throw new AuthorizationFailureException("id is undefined.");
+            }
+            ids.add(sample.getId());
+            if (sample.getPermId() == null)
+            {
+                throw new AuthorizationFailureException("permId is undefined.");
+            }
+            permIds.add(sample.getPermId());
+
+            if (sample.getSpaceCode() != null) // == null represents a shared sample
+            {
+                final Status status =
+                        evaluate(person, allowedRoles, authorizationDataProvider
+                                .getHomeDatabaseInstance(), sample.getSpaceCode());
+                if (Status.OK.equals(status) == false)
+                {
+                    return status;
+                }
+            }
+
+            final SampleOwnerIdentifier idOwner =
+                    SampleIdentifierFactory.parse(sample.getIdentifier());
+            final Status status = idOwnerPredicate.evaluate(person, allowedRoles, idOwner);
+            if (Status.OK.equals(status) == false)
+            {
+                return status;
+            }
+        }
+        for (Long spaceId : getSampleSpaceIds(ids, permIds))
+        {
+            if (spaceId == null || spaceId == 0)
+            {
+                continue; // Shared samples will return a spaceId of null (or 0 in EoDSQL).
+            }
+            final Status status =
+                    evaluate(person, allowedRoles, authorizationDataProvider
+                            .getHomeDatabaseInstance(), spaceId);
+            if (Status.OK.equals(status) == false)
+            {
+                return status;
+            }
+        }
+        return Status.OK;
+    }
+
+    private final static int ARRAY_SIZE_LIMIT = 999;
+
+    interface ISampleToSpaceQuery extends BaseQuery
+    {
+        @Select(sql = "select distinct space_id from samples where id = any(?{1}) "
+                + "union select distinct space_id from samples where perm_id = any(?{2})", parameterBindings =
+            { LongArrayMapper.class, StringArrayMapper.class })
+        public List<Long> getSampleSpaceIds(long[] sampleIds, String[] samplePermIds);
+    }
+
+    private Collection<Long> getSampleSpaceIds(final List<Long> ids, final List<String> permIds)
+    {
+        if (ids.size() != permIds.size())
+        {
+            throw new IllegalArgumentException("Expect to get the same number of ids and permIds.");
+        }
+        final int size = ids.size();
+        if (size == 0)
+        {
+            return Collections.emptyList();
+        }
+        final ISampleToSpaceQuery query =
+                QueryTool.getQuery(authorizationDataProvider.getConnection(),
+                        ISampleToSpaceQuery.class);
+        if (size > ARRAY_SIZE_LIMIT)
+        {
+            final Set<Long> spaceIds = new HashSet<Long>(size);
+            for (int startIdx = 0; startIdx < size; startIdx += ARRAY_SIZE_LIMIT)
+            {
+                final List<Long> idSubList = ids.subList(startIdx,
+                        Math.min(size, startIdx + ARRAY_SIZE_LIMIT));
+                final List<String> permIdSubList = permIds.subList(startIdx,
+                        Math.min(size, startIdx + ARRAY_SIZE_LIMIT));
+                spaceIds.addAll(query.getSampleSpaceIds(toArray(idSubList),
+                        permIdSubList.toArray(new String[permIdSubList.size()])));
+            }
+            return spaceIds;
+        } else
+        {
+            return query.getSampleSpaceIds(toArray(ids), permIds.toArray(new String[size]));
+        }
+    }
+
+    private long[] toArray(List<Long> list)
+    {
+        final long[] result = new long[list.size()];
+        for (int i = 0; i < result.length; ++i)
+        {
+            result[i] = list.get(i);
+        }
+        return result;
+    }
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/authorization/predicate/SamplePredicate.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/SamplePredicate.java
similarity index 51%
rename from openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/authorization/predicate/SamplePredicate.java
rename to openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/SamplePredicate.java
index 4f750a63768..b983bf8e80d 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/shared/authorization/predicate/SamplePredicate.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/SamplePredicate.java
@@ -14,29 +14,32 @@
  * limitations under the License.
  */
 
-package ch.systemsx.cisd.openbis.generic.shared.authorization.predicate;
+package ch.systemsx.cisd.openbis.generic.server.authorization.predicate;
 
-import ch.systemsx.cisd.openbis.generic.shared.api.v1.dto.Experiment;
-import ch.systemsx.cisd.openbis.generic.shared.basic.IIdentifierHolder;
-import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifierFactory;
-import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleOwnerIdentifier;
+import java.util.Collections;
+import java.util.List;
+
+import ch.systemsx.cisd.openbis.generic.shared.api.v1.dto.Sample;
+import ch.systemsx.cisd.openbis.generic.shared.authorization.predicate.DelegatedPredicate;
 
 /**
- * Predicate based on {@link Experiment}.
+ * Predicate based on {@link ch.systemsx.cisd.openbis.generic.shared.api.v1.dto.Sample}. This
+ * predicate authorizes for read-only access, i.e. it will allow access to shared samples for all
+ * users.
  * 
- * @author Franz-Josef Elmer
+ * @author Bernd Rinn
  */
-public class SamplePredicate extends DelegatedPredicate<SampleOwnerIdentifier, IIdentifierHolder>
+public class SamplePredicate extends DelegatedPredicate<List<Sample>, Sample>
 {
     public SamplePredicate()
     {
-        super(new SampleOwnerIdentifierPredicate());
+        super(new SampleListPredicate());
     }
 
     @Override
-    public SampleOwnerIdentifier tryConvert(IIdentifierHolder value)
+    public List<Sample> tryConvert(Sample value)
     {
-        return SampleIdentifierFactory.parse(value.getIdentifier());
+        return Collections.singletonList(value);
     }
 
     @Override
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/remoteapitest/RemoteApiTestCase.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/remoteapitest/RemoteApiTestCase.java
index f78e77f67fc..99baab190a4 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/remoteapitest/RemoteApiTestCase.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/remoteapitest/RemoteApiTestCase.java
@@ -53,6 +53,7 @@ public class RemoteApiTestCase extends AbstractTransactionalTestNGSpringContextT
         server = new Server();
         Connector connector = new SelectChannelConnector();
         connector.setPort(8888);
+        connector.setMaxIdleTime(360000);
         server.addConnector(connector);
         DispatcherServlet dispatcherServlet = new DispatcherServlet()
             {
-- 
GitLab