This is an automated email from the ASF dual-hosted git repository.

rafael pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cloudstack.git


The following commit(s) were added to refs/heads/master by this push:
     new 70bd177  Fix concurrency problem when moving ACL rules with drag&drop 
(#2703)
70bd177 is described below

commit 70bd177064edda2d89e696e953cfe2392333d869
Author: Rafael Weingärtner <[email protected]>
AuthorDate: Thu Jul 19 12:47:54 2018 -0300

    Fix concurrency problem when moving ACL rules with drag&drop (#2703)
    
    There was a concurrency problem with the “moveNetworkAclItem” API method. 
If two users were changing the ACL rules order at the same time, this could 
lead to inconsistent actions.
    To solve the problem we added a “consistency check ” parameter, which is 
used to hold the consistency hash. This hash is created using an MD5 hash 
function on a String that is created with all ACL rules UUIDs concatenated in 
their order, which is defined via the ‘number’ field.
    We also lock the editing of the ACL while executing the upgrade. This 
allows us to handle race conditions nicely, and present a good feedback for the 
user.
---
 .../org/apache/cloudstack/api/ApiConstants.java    |  1 +
 .../user/network/MoveNetworkAclItemCmd.java        |  9 ++-
 .../cloud/network/vpc/NetworkACLServiceImpl.java   | 55 +++++++++++--
 .../network/vpc/NetworkACLServiceImplTest.java     | 89 +++++++++++++++++++++-
 ui/scripts/vpc.js                                  | 12 ++-
 5 files changed, 156 insertions(+), 10 deletions(-)

diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java 
b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
index 1a57313..275a3cc 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -155,6 +155,7 @@ public class ApiConstants {
     public static final String IDS = "ids";
     public static final String PREVIOUS_ACL_RULE_ID = "previousaclruleid";
     public static final String NEXT_ACL_RULE_ID = "nextaclruleid";
+    public static final String MOVE_ACL_CONSISTENCY_HASH = 
"aclconsistencyhash";
     public static final String INTERNAL_DNS1 = "internaldns1";
     public static final String INTERNAL_DNS2 = "internaldns2";
     public static final String INTERVAL_TYPE = "intervaltype";
diff --git 
a/api/src/main/java/org/apache/cloudstack/api/command/user/network/MoveNetworkAclItemCmd.java
 
b/api/src/main/java/org/apache/cloudstack/api/command/user/network/MoveNetworkAclItemCmd.java
index aaa9c18..0343e50 100644
--- 
a/api/src/main/java/org/apache/cloudstack/api/command/user/network/MoveNetworkAclItemCmd.java
+++ 
b/api/src/main/java/org/apache/cloudstack/api/command/user/network/MoveNetworkAclItemCmd.java
@@ -43,6 +43,9 @@ public class MoveNetworkAclItemCmd extends 
BaseAsyncCustomIdCmd {
     @Parameter(name = ApiConstants.NEXT_ACL_RULE_ID, type = 
CommandType.STRING, description = "The ID of the rule that is right after the 
new position where the rule being moved is going to be placed. This value can 
be 'NULL' if the rule is being moved to the last position of the network ACL 
list.")
     private String nextAclRuleUuid;
 
+    @Parameter(name = ApiConstants.MOVE_ACL_CONSISTENCY_HASH, type = 
CommandType.STRING, description = "Md5 hash used to check the consistency of 
the ACL rule list before applying the ACL rule move. This check is useful to 
manage concurrency problems that may happen when multiple users are editing the 
same ACL rule listing. The parameter is not required. Therefore, if the user 
does not send it, he/she is assuming the risk of moving ACL rules without 
checking the consistency of the access [...]
+    private String aclConsistencyHash;
+
     @Override
     public void execute() {
         CallContext.current().setEventDetails(getEventDescription());
@@ -93,4 +96,8 @@ public class MoveNetworkAclItemCmd extends 
BaseAsyncCustomIdCmd {
         Account caller = CallContext.current().getCallingAccount();
         return caller.getAccountId();
     }
-}
+
+    public String getAclConsistencyHash() {
+        return aclConsistencyHash;
+    }
+}
\ No newline at end of file
diff --git 
a/server/src/main/java/com/cloud/network/vpc/NetworkACLServiceImpl.java 
b/server/src/main/java/com/cloud/network/vpc/NetworkACLServiceImpl.java
index 674910d..8734ec6 100644
--- a/server/src/main/java/com/cloud/network/vpc/NetworkACLServiceImpl.java
+++ b/server/src/main/java/com/cloud/network/vpc/NetworkACLServiceImpl.java
@@ -33,6 +33,7 @@ import 
org.apache.cloudstack.api.command.user.network.MoveNetworkAclItemCmd;
 import org.apache.cloudstack.api.command.user.network.UpdateNetworkACLItemCmd;
 import org.apache.cloudstack.api.command.user.network.UpdateNetworkACLListCmd;
 import org.apache.cloudstack.context.CallContext;
+import org.apache.commons.codec.digest.DigestUtils;
 import org.apache.commons.collections.CollectionUtils;
 import org.apache.commons.lang.StringUtils;
 import org.apache.log4j.Logger;
@@ -58,6 +59,7 @@ import com.cloud.tags.ResourceTagVO;
 import com.cloud.tags.dao.ResourceTagDao;
 import com.cloud.user.Account;
 import com.cloud.user.AccountManager;
+import com.cloud.user.User;
 import com.cloud.utils.Pair;
 import com.cloud.utils.Ternary;
 import com.cloud.utils.component.ManagerBase;
@@ -958,15 +960,56 @@ public class NetworkACLServiceImpl extends ManagerBase 
implements NetworkACLServ
 
         validateMoveAclRulesData(ruleBeingMoved, previousRule, nextRule);
 
-        List<NetworkACLItemVO> allAclRules = 
getAllAclRulesSortedByNumber(ruleBeingMoved.getAclId());
-        if (previousRule == null) {
-            return moveRuleToTheTop(ruleBeingMoved, allAclRules);
+        try {
+            NetworkACLVO lockedAcl = 
_networkACLDao.acquireInLockTable(ruleBeingMoved.getAclId());
+            List<NetworkACLItemVO> allAclRules = 
getAllAclRulesSortedByNumber(lockedAcl.getId());
+            validateAclConsistency(moveNetworkAclItemCmd, lockedAcl, 
allAclRules);
+
+            if (previousRule == null) {
+                return moveRuleToTheTop(ruleBeingMoved, allAclRules);
+            }
+            if (nextRule == null) {
+                return moveRuleToTheBottom(ruleBeingMoved, allAclRules);
+            }
+            return moveRuleBetweenAclRules(ruleBeingMoved, allAclRules, 
previousRule, nextRule);
+        } finally {
+            _networkACLDao.releaseFromLockTable(ruleBeingMoved.getAclId());
         }
-        if (nextRule == null) {
-            return moveRuleToTheBottom(ruleBeingMoved, allAclRules);
+    }
+
+    /**
+     * Validates the consistency of the ACL; the validation process is the 
following.
+     * <ul>
+     *  <li> If the ACL does not have rules yet, we do not have any validation 
to perform;
+     *  <li> we will check first if the user provided a consistency hash; if 
not, we will log a warning message informing administrators that the user is 
performing the call is assuming the risks of applying ACL replacement without a 
consistency check;
+     *  <li> if the ACL consistency hash is entered by the user, we check if 
it is the same as we currently have in the database. If it is different we 
throw an exception.
+     * </ul>
+     *
+     * If the consistency hash sent by the user is the same as the one we get 
with the database data we should be safe to proceed.
+     */
+    protected void validateAclConsistency(MoveNetworkAclItemCmd 
moveNetworkAclItemCmd, NetworkACLVO lockedAcl, List<NetworkACLItemVO> 
allAclRules) {
+        if (CollectionUtils.isEmpty(allAclRules)) {
+            s_logger.debug(String.format("No ACL rules for [id=%s, name=%s]. 
Therefore, there is no need for consistency validation.", lockedAcl.getUuid(), 
lockedAcl.getName()));
+            return;
         }
+        String aclConsistencyHash = 
moveNetworkAclItemCmd.getAclConsistencyHash();
+        if (StringUtils.isBlank(aclConsistencyHash)) {
+            User callingUser = CallContext.current().getCallingUser();
+            Account callingAccount = CallContext.current().getCallingAccount();
 
-        return moveRuleBetweenAclRules(ruleBeingMoved, allAclRules, 
previousRule, nextRule);
+            s_logger.warn(String.format(
+                    "User [id=%s, name=%s] from Account [id=%s, name=%s] has 
not entered an ACL consistency hash to execute the replacement of an ACL rule. 
Therefore, she/he is assuming all of the risks of procedding without this 
validation.",
+                    callingUser.getUuid(), callingUser.getUsername(), 
callingAccount.getUuid(), callingAccount.getAccountName()));
+            return;
+        }
+        String aclRulesUuids = StringUtils.EMPTY;
+        for (NetworkACLItemVO rule : allAclRules) {
+            aclRulesUuids += rule.getUuid();
+        }
+        String md5UuidsSortedByNumber = DigestUtils.md5Hex(aclRulesUuids);
+        if (!md5UuidsSortedByNumber.equals(aclConsistencyHash)) {
+            throw new InvalidParameterValueException("It seems that the access 
control list in the database is not in the state that you used to apply the 
changed. Could you try it again?");
+        }
     }
 
     /**
diff --git 
a/server/src/test/java/com/cloud/network/vpc/NetworkACLServiceImplTest.java 
b/server/src/test/java/com/cloud/network/vpc/NetworkACLServiceImplTest.java
index 5980574..9815526 100644
--- a/server/src/test/java/com/cloud/network/vpc/NetworkACLServiceImplTest.java
+++ b/server/src/test/java/com/cloud/network/vpc/NetworkACLServiceImplTest.java
@@ -56,6 +56,7 @@ import com.cloud.network.vpc.NetworkACLItem.TrafficType;
 import com.cloud.network.vpc.dao.NetworkACLDao;
 import com.cloud.user.Account;
 import com.cloud.user.AccountManager;
+import com.cloud.user.User;
 import com.cloud.utils.db.EntityManager;
 import com.cloud.utils.exception.CloudRuntimeException;
 
@@ -113,10 +114,15 @@ public class NetworkACLServiceImplTest {
     private NetworkACLItemVO nextAclRuleMock;
     private String nextAclRuleUuid = "uuidNextAclRule";
 
+    @Mock
+    private CallContext callContextMock;
+
     @Before
     public void befoteTest() {
         PowerMockito.mockStatic(CallContext.class);
-        
PowerMockito.when(CallContext.current()).thenReturn(Mockito.mock(CallContext.class));
+        PowerMockito.when(CallContext.current()).thenReturn(callContextMock);
+        
Mockito.doReturn(Mockito.mock(User.class)).when(callContextMock).getCallingUser();
+        
Mockito.doReturn(Mockito.mock(Account.class)).when(callContextMock).getCallingAccount();
 
         
Mockito.when(networkAclDaoMock.findById(networkAclListId)).thenReturn(networkACLVOMock);
         Mockito.when(createNetworkAclCmdMock.getNetworkId()).thenReturn(1L);
@@ -936,6 +942,8 @@ public class NetworkACLServiceImplTest {
         Mockito.verify(networkAclServiceImpl, 
Mockito.times(0)).moveRuleToTheBottom(Mockito.eq(aclRuleBeingMovedMock), 
Mockito.anyListOf(NetworkACLItemVO.class));
         Mockito.verify(networkAclServiceImpl, 
Mockito.times(0)).moveRuleBetweenAclRules(Mockito.eq(aclRuleBeingMovedMock), 
Mockito.anyListOf(NetworkACLItemVO.class), Mockito.eq(previousAclRuleMock),
                 Mockito.eq(nextAclRuleMock));
+        Mockito.verify(networkAclServiceImpl, 
Mockito.times(1)).validateAclConsistency(Mockito.any(MoveNetworkAclItemCmd.class),
 Mockito.any(NetworkACLVO.class),
+                Mockito.anyListOf(NetworkACLItemVO.class));
     }
 
     @Test
@@ -957,6 +965,8 @@ public class NetworkACLServiceImplTest {
         Mockito.verify(networkAclServiceImpl, 
Mockito.times(1)).moveRuleToTheBottom(Mockito.eq(aclRuleBeingMovedMock), 
Mockito.anyListOf(NetworkACLItemVO.class));
         Mockito.verify(networkAclServiceImpl, 
Mockito.times(0)).moveRuleBetweenAclRules(Mockito.eq(aclRuleBeingMovedMock), 
Mockito.anyListOf(NetworkACLItemVO.class), Mockito.eq(previousAclRuleMock),
                 Mockito.eq(nextAclRuleMock));
+        Mockito.verify(networkAclServiceImpl, 
Mockito.times(1)).validateAclConsistency(Mockito.any(MoveNetworkAclItemCmd.class),
 Mockito.any(NetworkACLVO.class),
+                Mockito.anyListOf(NetworkACLItemVO.class));
     }
 
     @Test
@@ -978,11 +988,17 @@ public class NetworkACLServiceImplTest {
         Mockito.verify(networkAclServiceImpl, 
Mockito.times(0)).moveRuleToTheBottom(Mockito.eq(aclRuleBeingMovedMock), 
Mockito.anyListOf(NetworkACLItemVO.class));
         Mockito.verify(networkAclServiceImpl, 
Mockito.times(1)).moveRuleBetweenAclRules(Mockito.eq(aclRuleBeingMovedMock), 
Mockito.anyListOf(NetworkACLItemVO.class), Mockito.eq(previousAclRuleMock),
                 Mockito.eq(nextAclRuleMock));
+        Mockito.verify(networkAclServiceImpl, 
Mockito.times(1)).validateAclConsistency(Mockito.any(MoveNetworkAclItemCmd.class),
 Mockito.any(NetworkACLVO.class),
+                Mockito.anyListOf(NetworkACLItemVO.class));
     }
 
     private void configureMoveMethodsToDoNothing() {
-        Mockito.doReturn(new 
ArrayList<>()).when(networkAclServiceImpl).getAllAclRulesSortedByNumber(networkAclMockId);
+        
Mockito.doReturn(networkACLVOMock).when(networkAclDaoMock).acquireInLockTable(Mockito.anyLong());
+        
Mockito.doReturn(true).when(networkAclDaoMock).releaseFromLockTable(Mockito.anyLong());
+
+        
Mockito.doNothing().when(networkAclServiceImpl).validateAclConsistency(Mockito.any(MoveNetworkAclItemCmd.class),
 Mockito.any(NetworkACLVO.class), Mockito.anyListOf(NetworkACLItemVO.class));
 
+        Mockito.doReturn(new 
ArrayList<>()).when(networkAclServiceImpl).getAllAclRulesSortedByNumber(networkAclMockId);
         
Mockito.doReturn(aclRuleBeingMovedMock).when(networkAclServiceImpl).moveRuleToTheTop(Mockito.eq(aclRuleBeingMovedMock),
 Mockito.anyListOf(NetworkACLItemVO.class));
         
Mockito.doReturn(aclRuleBeingMovedMock).when(networkAclServiceImpl).moveRuleToTheBottom(Mockito.eq(aclRuleBeingMovedMock),
 Mockito.anyListOf(NetworkACLItemVO.class));
         
Mockito.doReturn(aclRuleBeingMovedMock).when(networkAclServiceImpl).moveRuleBetweenAclRules(Mockito.eq(aclRuleBeingMovedMock),
 Mockito.anyListOf(NetworkACLItemVO.class),
@@ -1281,4 +1297,73 @@ public class NetworkACLServiceImplTest {
         Assert.assertEquals(14, networkACLItemVO13.getNumber());
         Assert.assertEquals(15, networkACLItemVO14.getNumber());
     }
+
+    @Test
+    public void validateAclConsistencyTestRuleListEmpty() {
+        
networkAclServiceImpl.validateAclConsistency(moveNetworkAclItemCmdMock, 
networkACLVOMock, new ArrayList<>());
+
+        Mockito.verify(moveNetworkAclItemCmdMock, 
Mockito.times(0)).getAclConsistencyHash();
+    }
+
+    @Test
+    public void validateAclConsistencyTestRuleListNull() {
+        
networkAclServiceImpl.validateAclConsistency(moveNetworkAclItemCmdMock, 
networkACLVOMock, null);
+
+        Mockito.verify(moveNetworkAclItemCmdMock, 
Mockito.times(0)).getAclConsistencyHash();
+    }
+
+    @Test
+    public void validateAclConsistencyTestAclConsistencyHashIsNull() {
+        
Mockito.doReturn(null).when(moveNetworkAclItemCmdMock).getAclConsistencyHash();
+
+        validateAclConsistencyTestAclConsistencyHashBlank();
+    }
+
+    @Test
+    public void validateAclConsistencyTestAclConsistencyHashIsEmpty() {
+        
Mockito.doReturn("").when(moveNetworkAclItemCmdMock).getAclConsistencyHash();
+
+        validateAclConsistencyTestAclConsistencyHashBlank();
+    }
+
+    @Test
+    public void validateAclConsistencyTestAclConsistencyHashIsBlank() {
+        Mockito.doReturn("            
").when(moveNetworkAclItemCmdMock).getAclConsistencyHash();
+
+        validateAclConsistencyTestAclConsistencyHashBlank();
+    }
+
+    private void validateAclConsistencyTestAclConsistencyHashBlank() {
+        ArrayList<NetworkACLItemVO> allAclRules = new ArrayList<>();
+        allAclRules.add(networkAclItemVoMock);
+
+        
networkAclServiceImpl.validateAclConsistency(moveNetworkAclItemCmdMock, 
networkACLVOMock, allAclRules);
+
+        Mockito.verify(moveNetworkAclItemCmdMock, 
Mockito.times(1)).getAclConsistencyHash();
+        Mockito.verify(callContextMock, Mockito.times(1)).getCallingAccount();
+        Mockito.verify(callContextMock, Mockito.times(1)).getCallingUser();
+    }
+
+    @Test(expected = InvalidParameterValueException.class)
+    public void 
validateAclConsistencyTestAclConsistencyHashIsNotEqualsToDatabaseHash() {
+        
Mockito.doReturn("differentHash").when(moveNetworkAclItemCmdMock).getAclConsistencyHash();
+
+        ArrayList<NetworkACLItemVO> allAclRules = new ArrayList<>();
+        allAclRules.add(networkAclItemVoMock);
+
+        
networkAclServiceImpl.validateAclConsistency(moveNetworkAclItemCmdMock, 
networkACLVOMock, allAclRules);
+    }
+
+    @Test
+    public void validateAclConsistencyTest() {
+        
Mockito.doReturn("eac527fe45c77232ef06d9c7eb8abd94").when(moveNetworkAclItemCmdMock).getAclConsistencyHash();
+
+        ArrayList<NetworkACLItemVO> allAclRules = new ArrayList<>();
+        allAclRules.add(networkAclItemVoMock);
+
+        Mockito.doReturn("someUuid").when(networkAclItemVoMock).getUuid();
+        
networkAclServiceImpl.validateAclConsistency(moveNetworkAclItemCmdMock, 
networkACLVOMock, allAclRules);
+
+        Mockito.verify(moveNetworkAclItemCmdMock, 
Mockito.times(1)).getAclConsistencyHash();
+    }
 }
\ No newline at end of file
diff --git a/ui/scripts/vpc.js b/ui/scripts/vpc.js
index 8a0bb96..04125ff 100644
--- a/ui/scripts/vpc.js
+++ b/ui/scripts/vpc.js
@@ -15,6 +15,10 @@
 // specific language governing permissions and limitations
 // under the License.
 (function($, cloudStack) {
+    //The drag and drop function to order ACL rules does not have access to 
the whole ACL.
+    //Therefore, we store the "state-hash" of the list being displayed for use 
in the drag and drop function.
+    var accessControlListConsistentyHashForDragAndDropFunction = "";
+
     var isNumeric = function (n) {
         return !isNaN(parseFloat(n));
     };
@@ -544,7 +548,8 @@
                         data: {
                             id: rule.id,
                             previousaclruleid: previousRuleId, 
-                            nextaclruleid: nextRuleId
+                            nextaclruleid: nextRuleId,
+                            aclconsistencyhash: 
accessControlListConsistentyHashForDragAndDropFunction
                         },
                         success: function(json) {
                             var pollTimer = setInterval(function() {
@@ -1415,6 +1420,11 @@
     
                                                             return acl;
                                                         });
+                                                        var allUuids = '';
+                                                        
items.forEach(function(aclRule){
+                                                                            
allUuids += aclRule.id;
+                                                                      });
+                                                        
accessControlListConsistentyHashForDragAndDropFunction = $.md5(allUuids);
                                                     }
 
                                                     args.response.success({

Reply via email to