Skip to content

Commit 514f51d

Browse files
Fix concurrency problem when moving ACL rules with drag&drop
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.
1 parent 5c28a2a commit 514f51d

File tree

5 files changed

+155
-9
lines changed

5 files changed

+155
-9
lines changed

api/src/main/java/org/apache/cloudstack/api/ApiConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ public class ApiConstants {
155155
public static final String IDS = "ids";
156156
public static final String PREVIOUS_ACL_RULE_ID = "previousaclruleid";
157157
public static final String NEXT_ACL_RULE_ID = "nextaclruleid";
158+
public static final String MOVE_ACL_CONSISTENCY_HASH = "aclconsistencyhash";
158159
public static final String INTERNAL_DNS1 = "internaldns1";
159160
public static final String INTERNAL_DNS2 = "internaldns2";
160161
public static final String INTERVAL_TYPE = "intervaltype";

api/src/main/java/org/apache/cloudstack/api/command/user/network/MoveNetworkAclItemCmd.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ public class MoveNetworkAclItemCmd extends BaseAsyncCustomIdCmd {
4343
@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.")
4444
private String nextAclRuleUuid;
4545

46+
@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 control list before executing the move. We use MD5 hash function on a String that is composed of all UUIDs of the ACL rules in concatenated in their respective order (order defined via 'number' field).")
47+
private String aclConsistencyHash;
48+
4649
@Override
4750
public void execute() {
4851
CallContext.current().setEventDetails(getEventDescription());
@@ -93,4 +96,8 @@ public long getEntityOwnerId() {
9396
Account caller = CallContext.current().getCallingAccount();
9497
return caller.getAccountId();
9598
}
99+
100+
public String getAclConsistencyHash() {
101+
return aclConsistencyHash;
102+
}
96103
}

server/src/main/java/com/cloud/network/vpc/NetworkACLServiceImpl.java

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.apache.cloudstack.api.command.user.network.UpdateNetworkACLItemCmd;
3434
import org.apache.cloudstack.api.command.user.network.UpdateNetworkACLListCmd;
3535
import org.apache.cloudstack.context.CallContext;
36+
import org.apache.commons.codec.digest.DigestUtils;
3637
import org.apache.commons.collections.CollectionUtils;
3738
import org.apache.commons.lang.StringUtils;
3839
import org.apache.log4j.Logger;
@@ -58,6 +59,7 @@
5859
import com.cloud.tags.dao.ResourceTagDao;
5960
import com.cloud.user.Account;
6061
import com.cloud.user.AccountManager;
62+
import com.cloud.user.User;
6163
import com.cloud.utils.Pair;
6264
import com.cloud.utils.Ternary;
6365
import com.cloud.utils.component.ManagerBase;
@@ -958,15 +960,56 @@ public NetworkACLItem moveNetworkAclRuleToNewPosition(MoveNetworkAclItemCmd move
958960

959961
validateMoveAclRulesData(ruleBeingMoved, previousRule, nextRule);
960962

961-
List<NetworkACLItemVO> allAclRules = getAllAclRulesSortedByNumber(ruleBeingMoved.getAclId());
962-
if (previousRule == null) {
963-
return moveRuleToTheTop(ruleBeingMoved, allAclRules);
963+
try {
964+
NetworkACLVO lockedAcl = _networkACLDao.acquireInLockTable(ruleBeingMoved.getAclId());
965+
List<NetworkACLItemVO> allAclRules = getAllAclRulesSortedByNumber(lockedAcl.getId());
966+
validateAclConsistency(moveNetworkAclItemCmd, lockedAcl, allAclRules);
967+
968+
if (previousRule == null) {
969+
return moveRuleToTheTop(ruleBeingMoved, allAclRules);
970+
}
971+
if (nextRule == null) {
972+
return moveRuleToTheBottom(ruleBeingMoved, allAclRules);
973+
}
974+
return moveRuleBetweenAclRules(ruleBeingMoved, allAclRules, previousRule, nextRule);
975+
} finally {
976+
_networkACLDao.releaseFromLockTable(ruleBeingMoved.getAclId());
964977
}
965-
if (nextRule == null) {
966-
return moveRuleToTheBottom(ruleBeingMoved, allAclRules);
978+
}
979+
980+
/**
981+
* Validates the consistency of the ACL; the validation process is the following.
982+
* <ul>
983+
* <li> If the ACL does not have rules yet, we do not have any validation to perform;
984+
* <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;
985+
* <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.
986+
* </ul>
987+
*
988+
* 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.
989+
*/
990+
protected void validateAclConsistency(MoveNetworkAclItemCmd moveNetworkAclItemCmd, NetworkACLVO lockedAcl, List<NetworkACLItemVO> allAclRules) {
991+
if (CollectionUtils.isEmpty(allAclRules)) {
992+
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()));
993+
return;
967994
}
995+
String aclConsistencyHash = moveNetworkAclItemCmd.getAclConsistencyHash();
996+
if (StringUtils.isBlank(aclConsistencyHash)) {
997+
User callingUser = CallContext.current().getCallingUser();
998+
Account callingAccount = CallContext.current().getCallingAccount();
968999

969-
return moveRuleBetweenAclRules(ruleBeingMoved, allAclRules, previousRule, nextRule);
1000+
s_logger.warn(String.format(
1001+
"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.",
1002+
callingUser.getUuid(), callingUser.getUsername(), callingAccount.getUuid(), callingAccount.getAccountName()));
1003+
return;
1004+
}
1005+
String aclRulesUuids = StringUtils.EMPTY;
1006+
for (NetworkACLItemVO rule : allAclRules) {
1007+
aclRulesUuids += rule.getUuid();
1008+
}
1009+
String md5UuidsSortedByNumber = DigestUtils.md5Hex(aclRulesUuids);
1010+
if (!md5UuidsSortedByNumber.equals(aclConsistencyHash)) {
1011+
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?");
1012+
}
9701013
}
9711014

9721015
/**

server/src/test/java/com/cloud/network/vpc/NetworkACLServiceImplTest.java

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import com.cloud.network.vpc.dao.NetworkACLDao;
5757
import com.cloud.user.Account;
5858
import com.cloud.user.AccountManager;
59+
import com.cloud.user.User;
5960
import com.cloud.utils.db.EntityManager;
6061
import com.cloud.utils.exception.CloudRuntimeException;
6162

@@ -113,10 +114,15 @@ public class NetworkACLServiceImplTest {
113114
private NetworkACLItemVO nextAclRuleMock;
114115
private String nextAclRuleUuid = "uuidNextAclRule";
115116

117+
@Mock
118+
private CallContext callContextMock;
119+
116120
@Before
117121
public void befoteTest() {
118122
PowerMockito.mockStatic(CallContext.class);
119-
PowerMockito.when(CallContext.current()).thenReturn(Mockito.mock(CallContext.class));
123+
PowerMockito.when(CallContext.current()).thenReturn(callContextMock);
124+
Mockito.doReturn(Mockito.mock(User.class)).when(callContextMock).getCallingUser();
125+
Mockito.doReturn(Mockito.mock(Account.class)).when(callContextMock).getCallingAccount();
120126

121127
Mockito.when(networkAclDaoMock.findById(networkAclListId)).thenReturn(networkACLVOMock);
122128
Mockito.when(createNetworkAclCmdMock.getNetworkId()).thenReturn(1L);
@@ -936,6 +942,8 @@ public void moveNetworkAclRuleToNewPositionTestMoveRuleToTop() {
936942
Mockito.verify(networkAclServiceImpl, Mockito.times(0)).moveRuleToTheBottom(Mockito.eq(aclRuleBeingMovedMock), Mockito.anyListOf(NetworkACLItemVO.class));
937943
Mockito.verify(networkAclServiceImpl, Mockito.times(0)).moveRuleBetweenAclRules(Mockito.eq(aclRuleBeingMovedMock), Mockito.anyListOf(NetworkACLItemVO.class), Mockito.eq(previousAclRuleMock),
938944
Mockito.eq(nextAclRuleMock));
945+
Mockito.verify(networkAclServiceImpl, Mockito.times(1)).validateAclConsistency(Mockito.any(MoveNetworkAclItemCmd.class), Mockito.any(NetworkACLVO.class),
946+
Mockito.anyListOf(NetworkACLItemVO.class));
939947
}
940948

941949
@Test
@@ -957,6 +965,8 @@ public void moveNetworkAclRuleToNewPositionTestMoveRuleToBottom() {
957965
Mockito.verify(networkAclServiceImpl, Mockito.times(1)).moveRuleToTheBottom(Mockito.eq(aclRuleBeingMovedMock), Mockito.anyListOf(NetworkACLItemVO.class));
958966
Mockito.verify(networkAclServiceImpl, Mockito.times(0)).moveRuleBetweenAclRules(Mockito.eq(aclRuleBeingMovedMock), Mockito.anyListOf(NetworkACLItemVO.class), Mockito.eq(previousAclRuleMock),
959967
Mockito.eq(nextAclRuleMock));
968+
Mockito.verify(networkAclServiceImpl, Mockito.times(1)).validateAclConsistency(Mockito.any(MoveNetworkAclItemCmd.class), Mockito.any(NetworkACLVO.class),
969+
Mockito.anyListOf(NetworkACLItemVO.class));
960970
}
961971

962972
@Test
@@ -978,11 +988,17 @@ public void moveNetworkAclRuleToNewPositionTestMoveBetweenAclRules() {
978988
Mockito.verify(networkAclServiceImpl, Mockito.times(0)).moveRuleToTheBottom(Mockito.eq(aclRuleBeingMovedMock), Mockito.anyListOf(NetworkACLItemVO.class));
979989
Mockito.verify(networkAclServiceImpl, Mockito.times(1)).moveRuleBetweenAclRules(Mockito.eq(aclRuleBeingMovedMock), Mockito.anyListOf(NetworkACLItemVO.class), Mockito.eq(previousAclRuleMock),
980990
Mockito.eq(nextAclRuleMock));
991+
Mockito.verify(networkAclServiceImpl, Mockito.times(1)).validateAclConsistency(Mockito.any(MoveNetworkAclItemCmd.class), Mockito.any(NetworkACLVO.class),
992+
Mockito.anyListOf(NetworkACLItemVO.class));
981993
}
982994

983995
private void configureMoveMethodsToDoNothing() {
984-
Mockito.doReturn(new ArrayList<>()).when(networkAclServiceImpl).getAllAclRulesSortedByNumber(networkAclMockId);
996+
Mockito.doReturn(networkACLVOMock).when(networkAclDaoMock).acquireInLockTable(Mockito.anyLong());
997+
Mockito.doReturn(true).when(networkAclDaoMock).releaseFromLockTable(Mockito.anyLong());
998+
999+
Mockito.doNothing().when(networkAclServiceImpl).validateAclConsistency(Mockito.any(MoveNetworkAclItemCmd.class), Mockito.any(NetworkACLVO.class), Mockito.anyListOf(NetworkACLItemVO.class));
9851000

1001+
Mockito.doReturn(new ArrayList<>()).when(networkAclServiceImpl).getAllAclRulesSortedByNumber(networkAclMockId);
9861002
Mockito.doReturn(aclRuleBeingMovedMock).when(networkAclServiceImpl).moveRuleToTheTop(Mockito.eq(aclRuleBeingMovedMock), Mockito.anyListOf(NetworkACLItemVO.class));
9871003
Mockito.doReturn(aclRuleBeingMovedMock).when(networkAclServiceImpl).moveRuleToTheBottom(Mockito.eq(aclRuleBeingMovedMock), Mockito.anyListOf(NetworkACLItemVO.class));
9881004
Mockito.doReturn(aclRuleBeingMovedMock).when(networkAclServiceImpl).moveRuleBetweenAclRules(Mockito.eq(aclRuleBeingMovedMock), Mockito.anyListOf(NetworkACLItemVO.class),
@@ -1281,4 +1297,73 @@ public void updateAclRuleToNewPositionAndExecuteShiftIfNecessaryTest() {
12811297
Assert.assertEquals(14, networkACLItemVO13.getNumber());
12821298
Assert.assertEquals(15, networkACLItemVO14.getNumber());
12831299
}
1300+
1301+
@Test
1302+
public void validateAclConsistencyTestRuleListEmpty() {
1303+
networkAclServiceImpl.validateAclConsistency(moveNetworkAclItemCmdMock, networkACLVOMock, new ArrayList<>());
1304+
1305+
Mockito.verify(moveNetworkAclItemCmdMock, Mockito.times(0)).getAclConsistencyHash();
1306+
}
1307+
1308+
@Test
1309+
public void validateAclConsistencyTestRuleListNull() {
1310+
networkAclServiceImpl.validateAclConsistency(moveNetworkAclItemCmdMock, networkACLVOMock, null);
1311+
1312+
Mockito.verify(moveNetworkAclItemCmdMock, Mockito.times(0)).getAclConsistencyHash();
1313+
}
1314+
1315+
@Test
1316+
public void validateAclConsistencyTestAclConsistencyHashIsNull() {
1317+
Mockito.doReturn(null).when(moveNetworkAclItemCmdMock).getAclConsistencyHash();
1318+
1319+
validateAclConsistencyTestAclConsistencyHashBlank();
1320+
}
1321+
1322+
@Test
1323+
public void validateAclConsistencyTestAclConsistencyHashIsEmpty() {
1324+
Mockito.doReturn("").when(moveNetworkAclItemCmdMock).getAclConsistencyHash();
1325+
1326+
validateAclConsistencyTestAclConsistencyHashBlank();
1327+
}
1328+
1329+
@Test
1330+
public void validateAclConsistencyTestAclConsistencyHashIsBlank() {
1331+
Mockito.doReturn(" ").when(moveNetworkAclItemCmdMock).getAclConsistencyHash();
1332+
1333+
validateAclConsistencyTestAclConsistencyHashBlank();
1334+
}
1335+
1336+
private void validateAclConsistencyTestAclConsistencyHashBlank() {
1337+
ArrayList<NetworkACLItemVO> allAclRules = new ArrayList<>();
1338+
allAclRules.add(networkAclItemVoMock);
1339+
1340+
networkAclServiceImpl.validateAclConsistency(moveNetworkAclItemCmdMock, networkACLVOMock, allAclRules);
1341+
1342+
Mockito.verify(moveNetworkAclItemCmdMock, Mockito.times(1)).getAclConsistencyHash();
1343+
Mockito.verify(callContextMock, Mockito.times(1)).getCallingAccount();
1344+
Mockito.verify(callContextMock, Mockito.times(1)).getCallingUser();
1345+
}
1346+
1347+
@Test(expected = InvalidParameterValueException.class)
1348+
public void validateAclConsistencyTestAclConsistencyHashIsNotEqualsToDatabaseHash() {
1349+
Mockito.doReturn("differentHash").when(moveNetworkAclItemCmdMock).getAclConsistencyHash();
1350+
1351+
ArrayList<NetworkACLItemVO> allAclRules = new ArrayList<>();
1352+
allAclRules.add(networkAclItemVoMock);
1353+
1354+
networkAclServiceImpl.validateAclConsistency(moveNetworkAclItemCmdMock, networkACLVOMock, allAclRules);
1355+
}
1356+
1357+
@Test
1358+
public void validateAclConsistencyTest() {
1359+
Mockito.doReturn("eac527fe45c77232ef06d9c7eb8abd94").when(moveNetworkAclItemCmdMock).getAclConsistencyHash();
1360+
1361+
ArrayList<NetworkACLItemVO> allAclRules = new ArrayList<>();
1362+
allAclRules.add(networkAclItemVoMock);
1363+
1364+
Mockito.doReturn("someUuid").when(networkAclItemVoMock).getUuid();
1365+
networkAclServiceImpl.validateAclConsistency(moveNetworkAclItemCmdMock, networkACLVOMock, allAclRules);
1366+
1367+
Mockito.verify(moveNetworkAclItemCmdMock, Mockito.times(1)).getAclConsistencyHash();
1368+
}
12841369
}

ui/scripts/vpc.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717
(function($, cloudStack) {
18+
//The drag and drop function to order ACL rules does not have access to the whole ACL.
19+
//Therefore, we store the "state-hash" of the list being displayed for use in the drag and drop function.
20+
var accessControlListConsistentyHashForDragAndDropFunction = "";
21+
1822
var isNumeric = function (n) {
1923
return !isNaN(parseFloat(n));
2024
};
@@ -544,7 +548,8 @@
544548
data: {
545549
id: rule.id,
546550
previousaclruleid: previousRuleId,
547-
nextaclruleid: nextRuleId
551+
nextaclruleid: nextRuleId,
552+
aclconsistencyhash: accessControlListConsistentyHashForDragAndDropFunction
548553
},
549554
success: function(json) {
550555
var pollTimer = setInterval(function() {
@@ -1415,6 +1420,11 @@
14151420

14161421
return acl;
14171422
});
1423+
var allUuids = '';
1424+
items.forEach(function(aclRule){
1425+
allUuids += aclRule.id;
1426+
});
1427+
accessControlListConsistentyHashForDragAndDropFunction = $.md5(allUuids);
14181428
}
14191429

14201430
args.response.success({

0 commit comments

Comments
 (0)