From de3e99d6933ed3dc07fc70daa35dfbbd7ba2a05d Mon Sep 17 00:00:00 2001
From: pkupczyk <pkupczyk>
Date: Sun, 21 Jan 2018 17:14:02 +0000
Subject: [PATCH] SSDM-4997 : Project Authorization bugfixes: - project user
 cannot create/update a project sample - deleting a project that is used in a
 project role assignment of the currently logged user breaks the project grid
 - project user can turn a sample attached to an experiment or a project into
 a space sample even though he/she does not have access to that space - v3 api
 allows to fetch/modify project role assignments even if project authorization
 feature is disabled

SVN: 39119
---
 ...edicateWithSampleIdentifierSystemTest.java | 80 -------------------
 ...edicateWithSampleIdentifierSystemTest.java | 70 ----------------
 .../RoleAssignmentAuthorizationExecutor.java  | 24 ++++--
 .../executor/sample/UpdateSampleExecutor.java | 23 ++++++
 .../translator/AbstractCachingTranslator.java | 12 +++
 .../RoleAssignmentTranslator.java             | 59 ++++++++++----
 .../generic/server/AbstractServer.java        | 24 +++---
 .../predicate/NewSamplePredicate.java         | 13 +--
 .../SampleUpdatesCollectionPredicate.java     |  8 +-
 .../predicate/SampleUpdatesPredicate.java     |  5 --
 .../generic/server/business/bo/SampleBO.java  | 10 +++
 .../server/business/bo/SampleTable.java       | 20 ++++-
 .../systemtest/asapi/v3/UpdateSampleTest.java | 27 +++++++
 .../ProjectAuthorizationUser.java             | 32 ++++++--
 14 files changed, 201 insertions(+), 206 deletions(-)
 delete mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/datastoreserver/systemtests/authorization/predicate/sample/SampleUpdatesCollectionPredicateWithSampleIdentifierSystemTest.java
 delete mode 100644 datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/datastoreserver/systemtests/authorization/predicate/sample/SampleUpdatesPredicateWithSampleIdentifierSystemTest.java

diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/datastoreserver/systemtests/authorization/predicate/sample/SampleUpdatesCollectionPredicateWithSampleIdentifierSystemTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/datastoreserver/systemtests/authorization/predicate/sample/SampleUpdatesCollectionPredicateWithSampleIdentifierSystemTest.java
deleted file mode 100644
index 180e98511a4..00000000000
--- a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/datastoreserver/systemtests/authorization/predicate/sample/SampleUpdatesCollectionPredicateWithSampleIdentifierSystemTest.java
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright 2017 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.datastoreserver.systemtests.authorization.predicate.sample;
-
-import java.util.List;
-
-import ch.systemsx.cisd.common.exceptions.UserFailureException;
-import ch.systemsx.cisd.openbis.datastoreserver.systemtests.authorization.ProjectAuthorizationUser;
-import ch.systemsx.cisd.openbis.datastoreserver.systemtests.authorization.predicate.CommonPredicateSystemTestAssertions;
-import ch.systemsx.cisd.openbis.datastoreserver.systemtests.authorization.predicate.CommonPredicateSystemTestAssertionsDelegate;
-import ch.systemsx.cisd.openbis.generic.shared.dto.SampleUpdatesDTO;
-import ch.systemsx.cisd.openbis.systemtest.authorization.predicate.sample.SamplePredicateTestService;
-
-/**
- * @author pkupczyk
- */
-public class SampleUpdatesCollectionPredicateWithSampleIdentifierSystemTest extends SampleUpdatesPredicateWithSampleIdentifierSystemTest
-{
-
-    @Override
-    protected boolean isCollectionPredicate()
-    {
-        return true;
-    }
-
-    @Override
-    protected void evaluateObjects(ProjectAuthorizationUser user, List<SampleUpdatesDTO> objects, Object param)
-    {
-        getBean(SamplePredicateTestService.class).testSampleUpdatesCollectionPredicate(user.getSessionProvider(), objects);
-    }
-
-    @Override
-    protected CommonPredicateSystemTestAssertions<SampleUpdatesDTO> getAssertions()
-    {
-        return new CommonPredicateSystemTestAssertionsDelegate<SampleUpdatesDTO>(super.getAssertions())
-            {
-                @Override
-                public void assertWithNullObject(ProjectAuthorizationUser user, Throwable t, Object param)
-                {
-                    if (user.isDisabledProjectUser())
-                    {
-                        assertAuthorizationFailureExceptionThatNoRoles(t);
-                    } else if (user.isInstanceUser())
-                    {
-                        assertNoException(t);
-                    } else
-                    {
-                        assertException(t, UserFailureException.class, "No sample updates specified.");
-                    }
-                }
-
-                @Override
-                public void assertWithNullCollection(ProjectAuthorizationUser user, Throwable t, Object param)
-                {
-                    if (user.isDisabledProjectUser())
-                    {
-                        assertAuthorizationFailureExceptionThatNoRoles(t);
-                    } else
-                    {
-                        assertException(t, UserFailureException.class, "No sample updates collection specified.");
-                    }
-                }
-            };
-    }
-
-}
diff --git a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/datastoreserver/systemtests/authorization/predicate/sample/SampleUpdatesPredicateWithSampleIdentifierSystemTest.java b/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/datastoreserver/systemtests/authorization/predicate/sample/SampleUpdatesPredicateWithSampleIdentifierSystemTest.java
deleted file mode 100644
index c65d9d414f3..00000000000
--- a/datastore_server/sourceTest/java/ch/systemsx/cisd/openbis/datastoreserver/systemtests/authorization/predicate/sample/SampleUpdatesPredicateWithSampleIdentifierSystemTest.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright 2017 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.datastoreserver.systemtests.authorization.predicate.sample;
-
-import ch.systemsx.cisd.openbis.datastoreserver.systemtests.authorization.ProjectAuthorizationUser;
-import ch.systemsx.cisd.openbis.datastoreserver.systemtests.authorization.common.SampleIdentifierUtil;
-import ch.systemsx.cisd.openbis.datastoreserver.systemtests.authorization.predicate.CommonPredicateSystemTestAssertions;
-import ch.systemsx.cisd.openbis.datastoreserver.systemtests.authorization.predicate.CommonPredicateSystemTestAssertionsDelegate;
-import ch.systemsx.cisd.openbis.generic.shared.dto.ProjectPE;
-import ch.systemsx.cisd.openbis.generic.shared.dto.SampleUpdatesDTO;
-import ch.systemsx.cisd.openbis.generic.shared.dto.SpacePE;
-import ch.systemsx.cisd.openbis.generic.shared.dto.identifier.SampleIdentifier;
-
-/**
- * @author pkupczyk
- */
-public class SampleUpdatesPredicateWithSampleIdentifierSystemTest extends SampleUpdatesPredicateWithSampleSystemTest
-{
-
-    @Override
-    protected SampleUpdatesDTO createNonexistentObject(Object param)
-    {
-        SampleIdentifier identifier = SampleIdentifierUtil.createNonexistentObject(param);
-        return new SampleUpdatesDTO(null, null, null, null, null, 0, identifier, null, null);
-    }
-
-    @Override
-    protected SampleUpdatesDTO createObject(SpacePE spacePE, ProjectPE projectPE, Object param)
-    {
-        SampleIdentifier identifier = SampleIdentifierUtil.createObject(this, spacePE, projectPE, param);
-        return new SampleUpdatesDTO(null, null, null, null, null, 0, identifier, null, null);
-    }
-
-    @Override
-    protected CommonPredicateSystemTestAssertions<SampleUpdatesDTO> getAssertions()
-    {
-        return new CommonPredicateSystemTestAssertionsDelegate<SampleUpdatesDTO>(super.getAssertions())
-            {
-                @Override
-                public void assertWithNonexistentObject(ProjectAuthorizationUser user, Throwable t, Object param)
-                {
-                    if (user.isDisabledProjectUser())
-                    {
-                        assertAuthorizationFailureExceptionThatNoRoles(t);
-                    } else if (user.isInstanceUser())
-                    {
-                        assertNoException(t);
-                    } else
-                    {
-                        assertAuthorizationFailureExceptionThatNotEnoughPrivileges(t);
-                    }
-                }
-            };
-    }
-
-}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/roleassignment/RoleAssignmentAuthorizationExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/roleassignment/RoleAssignmentAuthorizationExecutor.java
index 0e2c1f9aa6d..39cdc810fca 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/roleassignment/RoleAssignmentAuthorizationExecutor.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/roleassignment/RoleAssignmentAuthorizationExecutor.java
@@ -16,27 +16,31 @@
 
 package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.roleassignment;
 
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.IOperationContext;
+import ch.systemsx.cisd.common.exceptions.UserFailureException;
 import ch.systemsx.cisd.openbis.generic.server.authorization.annotation.AuthorizationGuard;
 import ch.systemsx.cisd.openbis.generic.server.authorization.annotation.Capability;
 import ch.systemsx.cisd.openbis.generic.server.authorization.annotation.RolesAllowed;
 import ch.systemsx.cisd.openbis.generic.server.authorization.predicate.ProjectPEPredicate;
 import ch.systemsx.cisd.openbis.generic.server.authorization.predicate.SpacePEPredicate;
+import ch.systemsx.cisd.openbis.generic.shared.authorization.IAuthorizationConfig;
 import ch.systemsx.cisd.openbis.generic.shared.basic.dto.RoleWithHierarchy;
 import ch.systemsx.cisd.openbis.generic.shared.dto.ProjectPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.SpacePE;
 
 /**
- * 
- *
  * @author Franz-Josef Elmer
  */
 @Component
 public class RoleAssignmentAuthorizationExecutor implements IRoleAssignmentAuthorizationExecutor
 {
 
+    @Autowired
+    private IAuthorizationConfig authorizationConfig;
+
     @Override
     @RolesAllowed(RoleWithHierarchy.PROJECT_ADMIN)
     @Capability("GET_ROLE_ASSIGNMENT")
@@ -54,7 +58,7 @@ public class RoleAssignmentAuthorizationExecutor implements IRoleAssignmentAutho
     @Override
     @RolesAllowed(RoleWithHierarchy.SPACE_ADMIN)
     @Capability("CREATE_SPACE_ROLE")
-    public void canCreateSpaceRole(IOperationContext context, 
+    public void canCreateSpaceRole(IOperationContext context,
             @AuthorizationGuard(guardClass = SpacePEPredicate.class) SpacePE space)
     {
     }
@@ -62,9 +66,13 @@ public class RoleAssignmentAuthorizationExecutor implements IRoleAssignmentAutho
     @Override
     @RolesAllowed(RoleWithHierarchy.PROJECT_ADMIN)
     @Capability("CREATE_PROJECT_ROLE")
-    public void canCreateProjectRole(IOperationContext context, 
+    public void canCreateProjectRole(IOperationContext context,
             @AuthorizationGuard(guardClass = ProjectPEPredicate.class) ProjectPE project)
     {
+        if (false == authorizationConfig.isProjectLevelEnabled())
+        {
+            throw new UserFailureException("Project authorization is disabled. Project level roles cannot be manipulated.");
+        }
     }
 
     @Override
@@ -84,7 +92,7 @@ public class RoleAssignmentAuthorizationExecutor implements IRoleAssignmentAutho
     @Override
     @RolesAllowed(RoleWithHierarchy.SPACE_ADMIN)
     @Capability("DELETE_SPACE_ROLE")
-    public void canDeleteSpaceRole(IOperationContext context, 
+    public void canDeleteSpaceRole(IOperationContext context,
             @AuthorizationGuard(guardClass = SpacePEPredicate.class) SpacePE space)
     {
     }
@@ -92,9 +100,13 @@ public class RoleAssignmentAuthorizationExecutor implements IRoleAssignmentAutho
     @Override
     @RolesAllowed(RoleWithHierarchy.PROJECT_ADMIN)
     @Capability("DELETE_PROJECT_ROLE")
-    public void canDeleteProjectRole(IOperationContext context, 
+    public void canDeleteProjectRole(IOperationContext context,
             @AuthorizationGuard(guardClass = ProjectPEPredicate.class) ProjectPE project)
     {
+        if (false == authorizationConfig.isProjectLevelEnabled())
+        {
+            throw new UserFailureException("Project authorization is disabled. Project level roles cannot be manipulated.");
+        }
     }
 
 }
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/sample/UpdateSampleExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/sample/UpdateSampleExecutor.java
index 8e82dd36d76..7a0a071c884 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/sample/UpdateSampleExecutor.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/sample/UpdateSampleExecutor.java
@@ -16,6 +16,7 @@
 
 package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.sample;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
 import java.util.List;
@@ -36,6 +37,7 @@ import ch.ethz.sis.openbis.generic.server.asapi.v3.helper.common.batch.MapBatch;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.helper.common.batch.MapBatchProcessor;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.helper.entity.progress.UpdateRelationProgress;
 import ch.systemsx.cisd.common.exceptions.UserFailureException;
+import ch.systemsx.cisd.openbis.generic.server.business.IRelationshipService;
 import ch.systemsx.cisd.openbis.generic.server.business.bo.DataAccessExceptionTranslator;
 import ch.systemsx.cisd.openbis.generic.server.dataaccess.IDAOFactory;
 import ch.systemsx.cisd.openbis.generic.shared.dto.PersonPE;
@@ -81,6 +83,9 @@ public class UpdateSampleExecutor extends AbstractUpdateEntityExecutor<SampleUpd
     @Autowired
     private IUpdateSampleAttachmentExecutor updateSampleAttachmentExecutor;
 
+    @Autowired
+    protected IRelationshipService relationshipService;
+
     @Override
     protected ISampleId getId(SampleUpdate update)
     {
@@ -111,6 +116,16 @@ public class UpdateSampleExecutor extends AbstractUpdateEntityExecutor<SampleUpd
     @Override
     protected void updateBatch(IOperationContext context, MapBatch<SampleUpdate, SamplePE> batch)
     {
+        Collection<SamplePE> experimentOrProjectSamples = new ArrayList<SamplePE>();
+
+        for (SamplePE entity : batch.getObjects().values())
+        {
+            if (entity.getExperiment() != null || entity.getProject() != null)
+            {
+                experimentOrProjectSamples.add(entity);
+            }
+        }
+
         updateSampleSpaceExecutor.update(context, batch);
         updateSampleProjectExecutor.update(context, batch);
         updateSampleExperimentExecutor.update(context, batch);
@@ -118,6 +133,14 @@ public class UpdateSampleExecutor extends AbstractUpdateEntityExecutor<SampleUpd
         updateTags(context, batch);
         updateAttachments(context, batch);
 
+        for (SamplePE entity : experimentOrProjectSamples)
+        {
+            if (entity.getExperiment() == null && entity.getProject() == null)
+            {
+                relationshipService.assignSampleToSpace(context.getSession(), entity, entity.getSpace());
+            }
+        }
+
         PersonPE person = context.getSession().tryGetPerson();
         Date timeStamp = daoFactory.getTransactionTimestamp();
 
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/AbstractCachingTranslator.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/AbstractCachingTranslator.java
index cc67d6fd3f2..b75aefb8d65 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/AbstractCachingTranslator.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/AbstractCachingTranslator.java
@@ -87,6 +87,8 @@ public abstract class AbstractCachingTranslator<I, O, F extends FetchOptions<?>>
             }
         }
 
+        filterTranslated(context, translated);
+
         return translated;
     }
 
@@ -264,6 +266,16 @@ public abstract class AbstractCachingTranslator<I, O, F extends FetchOptions<?>>
         return true;
     }
 
+    /**
+     * Override this method if you want to filter out some translated objects (e.g. because they should not be visible to a user the translation is
+     * performed for). Because of performance overhead related with unnecessary translation do use this method only when the number of objects is
+     * small. In other cases do use shouldTranslate instead.
+     */
+    protected void filterTranslated(TranslationContext context, Map<I, O> translated)
+    {
+
+    }
+
     /**
      * Implementation of this method should create a translated version of the input object. Only basic attributes of the input object should be
      * translated here. Parts that have a corresponding fetch option should be translated in the
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/roleassignment/RoleAssignmentTranslator.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/roleassignment/RoleAssignmentTranslator.java
index 50bf247df77..1c643c58d61 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/roleassignment/RoleAssignmentTranslator.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/translator/roleassignment/RoleAssignmentTranslator.java
@@ -18,6 +18,7 @@ package ch.ethz.sis.openbis.generic.server.asapi.v3.translator.roleassignment;
 
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.Map;
 import java.util.Set;
 
 import org.springframework.beans.factory.annotation.Autowired;
@@ -34,38 +35,40 @@ import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.AbstractCachingTra
 import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.TranslationContext;
 import ch.ethz.sis.openbis.generic.server.asapi.v3.translator.TranslationResults;
 import ch.systemsx.cisd.common.exceptions.AuthorizationFailureException;
+import ch.systemsx.cisd.openbis.generic.shared.authorization.IAuthorizationConfig;
 
 /**
- * 
- *
  * @author Franz-Josef Elmer
  */
 @Component
-public class RoleAssignmentTranslator 
-        extends AbstractCachingTranslator<Long, RoleAssignment, RoleAssignmentFetchOptions> 
+public class RoleAssignmentTranslator
+        extends AbstractCachingTranslator<Long, RoleAssignment, RoleAssignmentFetchOptions>
         implements IRoleAssignmentTranslator
 {
     @Autowired
     private IRoleAssignmentAuthorizationExecutor authorizationExecutor;
-    
+
     @Autowired
     private IRoleAssignmentBaseTranslator baseTranslator;
-    
+
     @Autowired
     private IRoleAssignmentRegistratorTranslator registratorTranslator;
-    
+
     @Autowired
     private IRoleAssignmentPersonTranslator userTranslator;
-    
+
     @Autowired
     private IRoleAssignmentAuthorizationGroupTranslator authorizationGroupTranslator;
-    
+
     @Autowired
     private IRoleAssignmentSpaceTranslator spaceTranslator;
-    
+
     @Autowired
     private IRoleAssignmentProjectTranslator projectTranslator;
 
+    @Autowired
+    private IAuthorizationConfig authorizationConfig;
+
     @Override
     protected Set<Long> shouldTranslate(TranslationContext context, Collection<Long> inputs, RoleAssignmentFetchOptions fetchOptions)
     {
@@ -79,6 +82,30 @@ public class RoleAssignmentTranslator
         }
     }
 
+    @Override
+    protected void filterTranslated(TranslationContext context, Map<Long, RoleAssignment> translated)
+    {
+        if (authorizationConfig.isProjectLevelEnabled())
+        {
+            return;
+        }
+
+        Collection<Long> projectRoleIds = new HashSet<Long>();
+
+        for (Map.Entry<Long, RoleAssignment> entry : translated.entrySet())
+        {
+            if (RoleLevel.PROJECT.equals(entry.getValue().getRoleLevel()))
+            {
+                projectRoleIds.add(entry.getKey());
+            }
+        }
+
+        for (Long projectRoleId : projectRoleIds)
+        {
+            translated.remove(projectRoleId);
+        }
+    }
+
     @Override
     protected RoleAssignment createObject(TranslationContext context, Long input, RoleAssignmentFetchOptions fetchOptions)
     {
@@ -91,11 +118,12 @@ public class RoleAssignmentTranslator
     protected Object getObjectsRelations(TranslationContext context, Collection<Long> inputs, RoleAssignmentFetchOptions fetchOptions)
     {
         TranslationResults relations = new TranslationResults();
-        
+
         relations.put(IRoleAssignmentBaseTranslator.class, baseTranslator.translate(context, inputs, null));
         if (fetchOptions.hasRegistrator())
         {
-            relations.put(IRoleAssignmentRegistratorTranslator.class, registratorTranslator.translate(context, inputs, fetchOptions.withRegistrator()));
+            relations.put(IRoleAssignmentRegistratorTranslator.class,
+                    registratorTranslator.translate(context, inputs, fetchOptions.withRegistrator()));
         }
         if (fetchOptions.hasUser())
         {
@@ -103,7 +131,8 @@ public class RoleAssignmentTranslator
         }
         if (fetchOptions.hasAuthorizationGroup())
         {
-            relations.put(IRoleAssignmentAuthorizationGroupTranslator.class, authorizationGroupTranslator.translate(context, inputs, fetchOptions.withAuthorizationGroup()));
+            relations.put(IRoleAssignmentAuthorizationGroupTranslator.class,
+                    authorizationGroupTranslator.translate(context, inputs, fetchOptions.withAuthorizationGroup()));
         }
         if (fetchOptions.hasSpace())
         {
@@ -113,7 +142,7 @@ public class RoleAssignmentTranslator
         {
             relations.put(IRoleAssignmentProjectTranslator.class, projectTranslator.translate(context, inputs, fetchOptions.withProject()));
         }
-        
+
         return relations;
     }
 
@@ -127,7 +156,7 @@ public class RoleAssignmentTranslator
         output.setRole(Role.valueOf(baseRecord.role_code));
         output.setRoleLevel(extractRoleLevel(baseRecord));
         output.setRegistrationDate(baseRecord.registrationDate);
-        
+
         if (fetchOptions.hasRegistrator())
         {
             output.setRegistrator(relations.get(IRoleAssignmentRegistratorTranslator.class, input));
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java
index d8333c424c1..d17c567bd75 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/AbstractServer.java
@@ -800,8 +800,11 @@ public abstract class AbstractServer<T> extends AbstractServiceWithLogger<T> imp
                 final PersonPE person = session.tryGetPerson();
                 if (person != null)
                 {
-                    getDAOFactory().getPersonDAO().lock(person);
-                    displaySettingsProvider.executeActionWithPersonLock(person, new IDelegatedActionWithResult<Void>()
+                    org.hibernate.Session hibernateSession = getDAOFactory().getSessionFactory().getCurrentSession();
+                    PersonPE attachedPerson = (PersonPE) hibernateSession.get(PersonPE.class, person.getId());
+
+                    getDAOFactory().getPersonDAO().lock(attachedPerson);
+                    displaySettingsProvider.executeActionWithPersonLock(attachedPerson, new IDelegatedActionWithResult<Void>()
                         {
                             @Override
                             public Void execute(boolean didOperationSucceed)
@@ -815,8 +818,8 @@ public abstract class AbstractServer<T> extends AbstractServiceWithLogger<T> imp
                                         visits.remove(i);
                                     }
                                 }
-                                displaySettingsProvider.replaceRegularDisplaySettings(person, displaySettings);
-                                getDAOFactory().getPersonDAO().updatePerson(person);
+                                displaySettingsProvider.replaceRegularDisplaySettings(attachedPerson, displaySettings);
+                                getDAOFactory().getPersonDAO().updatePerson(attachedPerson);
                                 return null;
                             }
                         });
@@ -845,19 +848,22 @@ public abstract class AbstractServer<T> extends AbstractServiceWithLogger<T> imp
                 final PersonPE person = session.tryGetPerson();
                 if (person != null)
                 {
-                    getDAOFactory().getPersonDAO().lock(person);
-                    displaySettingsProvider.executeActionWithPersonLock(person, new IDelegatedActionWithResult<Void>()
+                    org.hibernate.Session hibernateSession = getDAOFactory().getSessionFactory().getCurrentSession();
+                    PersonPE attachedPerson = (PersonPE) hibernateSession.get(PersonPE.class, person.getId());
+
+                    getDAOFactory().getPersonDAO().lock(attachedPerson);
+                    displaySettingsProvider.executeActionWithPersonLock(attachedPerson, new IDelegatedActionWithResult<Void>()
                         {
                             @Override
                             public Void execute(boolean didOperationSucceed)
                             {
                                 DisplaySettings currentDisplaySettings =
-                                        displaySettingsProvider.getCurrentDisplaySettings(person);
+                                        displaySettingsProvider.getCurrentDisplaySettings(attachedPerson);
                                 DisplaySettings newDisplaySettings =
                                         displaySettingsUpdate.update(currentDisplaySettings);
-                                displaySettingsProvider.replaceCurrentDisplaySettings(person,
+                                displaySettingsProvider.replaceCurrentDisplaySettings(attachedPerson,
                                         newDisplaySettings);
-                                getDAOFactory().getPersonDAO().updatePerson(person);
+                                getDAOFactory().getPersonDAO().updatePerson(attachedPerson);
                                 return null;
                             }
                         });
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/NewSamplePredicate.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/NewSamplePredicate.java
index 1058c287970..01f65db4f20 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/NewSamplePredicate.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/NewSamplePredicate.java
@@ -69,13 +69,16 @@ public final class NewSamplePredicate extends AbstractPredicate<NewSample>
 
         Status status = Status.OK;
 
-        if (value.getIdentifier() != null)
+        if (value.getProjectIdentifier() == null && value.getExperimentIdentifier() == null)
         {
-            status = newSampleIdentifierPredicate.doEvaluation(person, allowedRoles, value);
-
-            if (false == status.equals(Status.OK))
+            if (value.getIdentifier() != null)
             {
-                return status;
+                status = newSampleIdentifierPredicate.doEvaluation(person, allowedRoles, value);
+
+                if (false == status.equals(Status.OK))
+                {
+                    return status;
+                }
             }
         }
 
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/SampleUpdatesCollectionPredicate.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/SampleUpdatesCollectionPredicate.java
index c7d3b48987c..c121aaaf3f1 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/SampleUpdatesCollectionPredicate.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/SampleUpdatesCollectionPredicate.java
@@ -81,7 +81,6 @@ public class SampleUpdatesCollectionPredicate extends AbstractPredicate<List<Sam
         IServiceConversationProgressListener progressListener =
                 ServiceConversationsThreadContext.getProgressListener();
         List<TechId> techIds = new ArrayList<TechId>(value.size());
-        List<SampleIdentifier> sampleIdentifiers = new ArrayList<SampleIdentifier>(value.size());
         List<SampleIdentifier> containerIdentifiers = new ArrayList<SampleIdentifier>();
 
         int index = 0;
@@ -102,11 +101,6 @@ public class SampleUpdatesCollectionPredicate extends AbstractPredicate<List<Sam
             {
                 return result;
             }
-            SampleIdentifier sampleIdentifier = sampleUpdates.getSampleIdentifier();
-            if (sampleIdentifier != null)
-            {
-                sampleIdentifiers.add(sampleIdentifier);
-            }
 
             if (sampleUpdates.getContainerIdentifierOrNull() != null)
             {
@@ -132,7 +126,7 @@ public class SampleUpdatesCollectionPredicate extends AbstractPredicate<List<Sam
             }
         }
 
-        return sampleIdentifierCollectionPredicate.doEvaluation(person, allowedRoles, sampleIdentifiers);
+        return result;
     }
 
 }
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/SampleUpdatesPredicate.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/SampleUpdatesPredicate.java
index dec331cf900..35b05ba3e48 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/SampleUpdatesPredicate.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/authorization/predicate/SampleUpdatesPredicate.java
@@ -95,11 +95,6 @@ public class SampleUpdatesPredicate extends AbstractSamplePredicate<SampleUpdate
             }
         }
 
-        if (updates.getSampleIdentifier() != null)
-        {
-            status = sampleIdentifierPredicate.doEvaluation(person, allowedRoles, updates.getSampleIdentifier());
-        }
-
         return status;
     }
 
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/SampleBO.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/SampleBO.java
index ffa01b3ee19..f2f75d8a227 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/SampleBO.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/SampleBO.java
@@ -40,6 +40,7 @@ import ch.systemsx.cisd.openbis.generic.shared.basic.dto.id.sample.SampleTechIdI
 import ch.systemsx.cisd.openbis.generic.shared.dto.AttachmentPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.ExperimentPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.PersonPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.ProjectPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.SamplePE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.SamplePropertyPE;
 import ch.systemsx.cisd.openbis.generic.shared.dto.SampleTypePE;
@@ -297,6 +298,10 @@ public final class SampleBO extends AbstractSampleBusinessObject implements ISam
         {
             throwModifiedEntityException("Sample");
         }
+
+        ExperimentPE oldExperiment = sample.getExperiment();
+        ProjectPE oldProject = sample.getProject();
+
         updateProperties(sample.getSampleType(), updates.getProperties(), extractPropertiesCodes(updates.getProperties()), sample, sample);
         spaceUpdated = updateSpace(sample, updates.getSampleIdentifier(), null);
         if (updates.isUpdateExperimentLink())
@@ -310,6 +315,11 @@ public final class SampleBO extends AbstractSampleBusinessObject implements ISam
         updateParents(updates);
         setMetaprojects(sample, updates.getMetaprojectsOrNull());
 
+        if ((oldExperiment != null || oldProject != null) && (sample.getExperiment() == null && sample.getProject() == null))
+        {
+            relationshipService.assignSampleToSpace(session, sample, sample.getSpace());
+        }
+
         dataChanged = true;
     }
 
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/SampleTable.java b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/SampleTable.java
index f388d7ee44f..2432d8f1e8f 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/SampleTable.java
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/generic/server/business/bo/SampleTable.java
@@ -271,6 +271,10 @@ public final class SampleTable extends AbstractSampleBusinessObject implements I
                     "No sample could be found with given identifier '%s'.",
                     updates.getOldSampleIdentifierOrNull());
         }
+
+        ExperimentPE oldExperiment = sample.getExperiment();
+        ProjectPE oldProject = sample.getProject();
+
         SampleBatchUpdateDetails details = updates.getDetails();
         batchUpdateProperties(sample, updates.getProperties(), details.getPropertiesToUpdate());
         checkPropertiesBusinessRules(sample, propertiesCache);
@@ -307,6 +311,11 @@ public final class SampleTable extends AbstractSampleBusinessObject implements I
         {
             checkContainerBusinessRules(sample);
         }
+
+        if ((oldExperiment != null || oldProject != null) && (sample.getExperiment() == null && sample.getProject() == null))
+        {
+            relationshipService.assignSampleToSpace(session, sample, sample.getSpace());
+        }
     }
 
     /**
@@ -325,6 +334,9 @@ public final class SampleTable extends AbstractSampleBusinessObject implements I
                     updates.getSampleIdentifier());
         }
 
+        ExperimentPE oldExperiment = sample.getExperiment();
+        ProjectPE oldProject = sample.getProject();
+
         updateProperties(sample, updates.getProperties());
         checkPropertiesBusinessRules(sample, propertiesCache);
 
@@ -335,7 +347,7 @@ public final class SampleTable extends AbstractSampleBusinessObject implements I
             updateExperiment(sample, experimentIdentifier, experimentCache);
             checkExperimentBusinessRules(sample);
         }
-        
+
         if (experimentIdentifier == null)
         {
             updateProject(sample, updates.getProjectIdentifier(), projectCache);
@@ -357,6 +369,12 @@ public final class SampleTable extends AbstractSampleBusinessObject implements I
         {
             checkContainerBusinessRules(sample);
         }
+
+        if ((oldExperiment != null || oldProject != null) && (sample.getExperiment() == null && sample.getProject() == null))
+        {
+            relationshipService.assignSampleToSpace(session, sample, sample.getSpace());
+        }
+
         RelationshipUtils.updateModificationDateAndModifier(sample, session, getTransactionTimeStamp());
     }
 
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/UpdateSampleTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/UpdateSampleTest.java
index 1c6f755fa57..23bca7c6a96 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/UpdateSampleTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/UpdateSampleTest.java
@@ -518,6 +518,33 @@ public class UpdateSampleTest extends AbstractSampleTest
         Assert.assertNull(sample.getExperiment());
     }
 
+    @Test
+    public void testUpdateWithExperimentSampleTurnedIntoSpaceSampleAsProjectUser()
+    {
+        String sessionToken = v3api.login(ProjectAuthorizationUser.TEST_PROJECT_PA_ON, PASSWORD);
+
+        SampleCreation creation = new SampleCreation();
+        creation.setCode("SAMPLE");
+        creation.setTypeId(new EntityTypePermId("CELL_PLATE"));
+        creation.setSpaceId(new SpacePermId("TEST-SPACE"));
+        creation.setExperimentId(new ExperimentIdentifier("/TEST-SPACE/TEST-PROJECT/EXP-SPACE-TEST"));
+
+        List<SamplePermId> ids = v3api.createSamples(sessionToken, Arrays.asList(creation));
+
+        SampleUpdate update = new SampleUpdate();
+        update.setSampleId(ids.get(0));
+        update.setExperimentId(null);
+
+        assertAuthorizationFailureException(new IDelegatedAction()
+            {
+                @Override
+                public void execute()
+                {
+                    v3api.updateSamples(sessionToken, Arrays.asList(update));
+                }
+            });
+    }
+
     @Test
     public void testUpdateWithExperimentNullForSampleWithDataSets()
     {
diff --git a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/authorization/ProjectAuthorizationUser.java b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/authorization/ProjectAuthorizationUser.java
index 05f2fcd2e5d..7b3c1fe83fc 100644
--- a/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/authorization/ProjectAuthorizationUser.java
+++ b/openbis/sourceTest/java/ch/systemsx/cisd/openbis/systemtest/authorization/ProjectAuthorizationUser.java
@@ -28,6 +28,22 @@ import org.testng.annotations.DataProvider;
 public class ProjectAuthorizationUser
 {
 
+    public static final String INSTANCE_ADMIN = "instance_admin";
+
+    public static final String TEST_SPACE_PA_ON = "test_space_pa_on";
+
+    public static final String TEST_SPACE_PA_OFF = "test_space_pa_off";
+
+    public static final String TEST_PROJECT_PA_ON = "test_project_pa_on";
+
+    public static final String TEST_PROJECT_PA_OFF = "test_project_pa_off";
+
+    public static final String ETL_SERVER = "etlserver";
+
+    public static final String TEST_SPACE_ETL_SERVER = "test_space_etl_server";
+
+    public static final String TEST_GROUP_ETL_SERVER = "test_group_etl_server";
+
     public static final String PROVIDER = "project-authorization-users-provider";
 
     public static final String PROVIDER_WITH_ETL = "project-authorization-users-provider-with-etl";
@@ -149,20 +165,20 @@ public class ProjectAuthorizationUser
     @DataProvider(name = PROVIDER)
     public static Object[][] providerUsers()
     {
-        ProjectAuthorizationUser instanceAdmin = new ProjectAuthorizationUser("instance_admin");
+        ProjectAuthorizationUser instanceAdmin = new ProjectAuthorizationUser(INSTANCE_ADMIN);
         instanceAdmin.setInstanceUser(true);
 
-        ProjectAuthorizationUser testSpacePAOff = new ProjectAuthorizationUser("test_space_pa_off");
+        ProjectAuthorizationUser testSpacePAOff = new ProjectAuthorizationUser(TEST_SPACE_PA_OFF);
         testSpacePAOff.setTestSpaceUser(true);
 
-        ProjectAuthorizationUser testSpacePAOn = new ProjectAuthorizationUser("test_space_pa_on");
+        ProjectAuthorizationUser testSpacePAOn = new ProjectAuthorizationUser(TEST_SPACE_PA_ON);
         testSpacePAOn.setTestSpaceUser(true);
         testSpacePAOn.setPAEnabled(true);
 
-        ProjectAuthorizationUser testProjectPAOff = new ProjectAuthorizationUser("test_project_pa_off");
+        ProjectAuthorizationUser testProjectPAOff = new ProjectAuthorizationUser(TEST_PROJECT_PA_OFF);
         testProjectPAOff.setTestProjectUser(true);
 
-        ProjectAuthorizationUser testProjectPAOn = new ProjectAuthorizationUser("test_project_pa_on");
+        ProjectAuthorizationUser testProjectPAOn = new ProjectAuthorizationUser(TEST_PROJECT_PA_ON);
         testProjectPAOn.setTestProjectUser(true);
         testProjectPAOn.setPAEnabled(true);
 
@@ -182,15 +198,15 @@ public class ProjectAuthorizationUser
     @DataProvider(name = PROVIDER_WITH_ETL)
     public static Object[][] provideUsersWithETL()
     {
-        ProjectAuthorizationUser instanceETLServer = new ProjectAuthorizationUser("etlserver");
+        ProjectAuthorizationUser instanceETLServer = new ProjectAuthorizationUser(ETL_SERVER);
         instanceETLServer.setInstanceUser(true);
         instanceETLServer.setETLServerUser(true);
 
-        ProjectAuthorizationUser testSpaceETLServer = new ProjectAuthorizationUser("test_space_etl_server");
+        ProjectAuthorizationUser testSpaceETLServer = new ProjectAuthorizationUser(TEST_SPACE_ETL_SERVER);
         testSpaceETLServer.setTestSpaceUser(true);
         testSpaceETLServer.setETLServerUser(true);
 
-        ProjectAuthorizationUser testGroupETLServer = new ProjectAuthorizationUser("test_group_etl_server");
+        ProjectAuthorizationUser testGroupETLServer = new ProjectAuthorizationUser(TEST_GROUP_ETL_SERVER);
         testGroupETLServer.setTestGroupUser(true);
         testGroupETLServer.setETLServerUser(true);
 
-- 
GitLab