Skip to content

Commit a86160b

Browse files
bwswyadvr
authored andcommitted
Cloudstack 10170: Fix resource tags security bugs and add account tags support (#2350)
This PR introduces several features and fixes some bugs: - account tags feature - fixed resource tags bugs which happened during tags search (found wrong entries because of mysql string to number translation - see #905, but this PR does more and fixes also resource access - vulnerability during list resource tags) - some marvin improvements (speed, sanity) Improved resource tags code: 1. Enhanced listTags security 2. Added support for account tags (account tags are required to support tags common for all users of an account) 3. Improved the tag management code (refactoring and cleanup) Marvin: 1. Fixed Marvin wait timeout between async pools. To decrease polling interval and improve CI speed. 2. Fixed /tmp/ to /tmp in zone configuration files. 3. Fixed + to os.path.join in log class. 4. Fixed + to os.path.join in deployDataCenter class. 5. Fixed typos in tag tests. 6. Modified Tags base class delete method. Deploy Datacenter script: 1. Improved deployDatacenter. Added option logdir to specify where script places results of evaluation. ConfigurationManagerImpl: 1. Added logging to ConfigurationManagerImpl to log when vlan is not found. Added test stubs for tags. Found accidental exception during simulator running after CI. tests_tags.py: 1. Fixed stale undeleted tags. 2. Changed region:India to scope:TestName.
1 parent 35b4339 commit a86160b

13 files changed

Lines changed: 740 additions & 265 deletions

File tree

api/src/com/cloud/server/ResourceTag.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public enum ResourceObjectType {
3838
SecurityGroupRule(true, false),
3939
PublicIpAddress(true, true),
4040
Project(true, false),
41+
Account(true, false),
4142
Vpc(true, true),
4243
NetworkACL(true, true),
4344
StaticRoute(true, false),

server/src/com/cloud/configuration/ConfigurationManagerImpl.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3804,6 +3804,9 @@ public boolean releasePublicIpRange(final ReleasePublicIpRangeCmd cmd) {
38043804
@DB
38053805
public boolean releasePublicIpRange(final long vlanDbId, final long userId, final Account caller) {
38063806
VlanVO vlan = _vlanDao.findById(vlanDbId);
3807+
if(vlan == null) {
3808+
s_logger.warn("VLAN information for Account '" + caller + "', User '" + userId + "' VLAN '" + vlanDbId + "' is null. This is NPE situation.");
3809+
}
38073810

38083811
// Verify range is dedicated
38093812
boolean isAccountSpecific = false;

server/src/com/cloud/tags/TaggedResourceManagerImpl.java

Lines changed: 99 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,13 @@
1616
// under the License.
1717
package com.cloud.tags;
1818

19-
import java.util.ArrayList;
20-
import java.util.HashMap;
21-
import java.util.List;
22-
import java.util.Map;
23-
24-
import javax.inject.Inject;
25-
import javax.naming.ConfigurationException;
26-
27-
import org.apache.cloudstack.api.Identity;
28-
import org.apache.cloudstack.api.InternalIdentity;
29-
import org.apache.cloudstack.context.CallContext;
30-
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
31-
import org.apache.commons.lang.StringUtils;
32-
import org.apache.log4j.Logger;
33-
34-
import com.cloud.api.query.dao.ResourceTagJoinDao;
3519
import com.cloud.dc.DataCenterVO;
3620
import com.cloud.domain.PartOf;
3721
import com.cloud.event.ActionEvent;
3822
import com.cloud.event.EventTypes;
3923
import com.cloud.exception.InvalidParameterValueException;
4024
import com.cloud.exception.PermissionDeniedException;
25+
import com.cloud.offerings.NetworkOfferingVO;
4126
import com.cloud.network.LBHealthCheckPolicyVO;
4227
import com.cloud.network.as.AutoScaleVmGroupVO;
4328
import com.cloud.network.as.AutoScaleVmProfileVO;
@@ -58,7 +43,6 @@
5843
import com.cloud.network.vpc.StaticRouteVO;
5944
import com.cloud.network.vpc.VpcOfferingVO;
6045
import com.cloud.network.vpc.VpcVO;
61-
import com.cloud.offerings.NetworkOfferingVO;
6246
import com.cloud.projects.ProjectVO;
6347
import com.cloud.server.ResourceTag;
6448
import com.cloud.server.ResourceTag.ResourceObjectType;
@@ -72,6 +56,7 @@
7256
import com.cloud.tags.dao.ResourceTagDao;
7357
import com.cloud.user.Account;
7458
import com.cloud.user.AccountManager;
59+
import com.cloud.user.AccountVO;
7560
import com.cloud.user.DomainManager;
7661
import com.cloud.user.OwnedBy;
7762
import com.cloud.user.UserVO;
@@ -89,11 +74,28 @@
8974
import com.cloud.vm.NicVO;
9075
import com.cloud.vm.UserVmVO;
9176
import com.cloud.vm.snapshot.VMSnapshotVO;
77+
import org.apache.cloudstack.api.Identity;
78+
import org.apache.cloudstack.api.InternalIdentity;
79+
import org.apache.cloudstack.context.CallContext;
80+
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
81+
import org.apache.commons.lang.StringUtils;
82+
import org.apache.log4j.Logger;
83+
84+
import org.apache.commons.collections.MapUtils;
85+
86+
import javax.inject.Inject;
87+
import javax.naming.ConfigurationException;
88+
import java.util.ArrayList;
89+
import java.util.HashMap;
90+
import java.util.List;
91+
import java.util.Map;
92+
import java.util.Objects;
93+
import java.util.stream.Collectors;
9294

9395
public class TaggedResourceManagerImpl extends ManagerBase implements TaggedResourceService {
9496
public static final Logger s_logger = Logger.getLogger(TaggedResourceManagerImpl.class);
9597

96-
private static final Map<ResourceObjectType, Class<?>> s_typeMap = new HashMap<ResourceObjectType, Class<?>>();
98+
private static final Map<ResourceObjectType, Class<?>> s_typeMap = new HashMap<>();
9799
static {
98100
s_typeMap.put(ResourceObjectType.UserVm, UserVmVO.class);
99101
s_typeMap.put(ResourceObjectType.Volume, VolumeVO.class);
@@ -108,6 +110,7 @@ public class TaggedResourceManagerImpl extends ManagerBase implements TaggedReso
108110
s_typeMap.put(ResourceObjectType.SecurityGroupRule, SecurityGroupRuleVO.class);
109111
s_typeMap.put(ResourceObjectType.PublicIpAddress, IPAddressVO.class);
110112
s_typeMap.put(ResourceObjectType.Project, ProjectVO.class);
113+
s_typeMap.put(ResourceObjectType.Account, AccountVO.class);
111114
s_typeMap.put(ResourceObjectType.Vpc, VpcVO.class);
112115
s_typeMap.put(ResourceObjectType.Nic, NicVO.class);
113116
s_typeMap.put(ResourceObjectType.NetworkACL, NetworkACLItemVO.class);
@@ -140,8 +143,6 @@ public class TaggedResourceManagerImpl extends ManagerBase implements TaggedReso
140143
@Inject
141144
ResourceTagDao _resourceTagDao;
142145
@Inject
143-
ResourceTagJoinDao _resourceTagJoinDao;
144-
@Inject
145146
DomainManager _domainMgr;
146147
@Inject
147148
AccountDao _accountDao;
@@ -194,6 +195,12 @@ private Pair<Long, Long> getAccountDomain(long resourceId, ResourceObjectType re
194195
domainId = ((SecurityGroupVO)SecurityGroup).getDomainId();
195196
}
196197

198+
if (resourceType == ResourceObjectType.Account) {
199+
AccountVO account = (AccountVO)entity;
200+
accountId = account.getId();
201+
domainId = account.getDomainId();
202+
}
203+
197204
// if the resource type is network acl, get the accountId and domainId from VPC following: NetworkACLItem -> NetworkACL -> VPC
198205
if (resourceType == ResourceObjectType.NetworkACL) {
199206
NetworkACLItemVO aclItem = (NetworkACLItemVO)entity;
@@ -223,7 +230,23 @@ private Pair<Long, Long> getAccountDomain(long resourceId, ResourceObjectType re
223230
if ((domainId == null) || ((accountId != null) && (domainId.longValue() == -1))) {
224231
domainId = _accountDao.getDomainIdForGivenAccountId(accountId);
225232
}
226-
return new Pair<Long, Long>(accountId, domainId);
233+
return new Pair<>(accountId, domainId);
234+
}
235+
236+
private void checkResourceAccessible(Long accountId, Long domainId, String exceptionMessage) {
237+
Account caller = CallContext.current().getCallingAccount();
238+
if (Objects.equals(domainId, -1))
239+
{
240+
throw new CloudRuntimeException("Invalid DomainId: -1");
241+
}
242+
if (accountId != null) {
243+
_accountMgr.checkAccess(caller, null, false, _accountMgr.getAccount(accountId));
244+
} else if (domainId != null && !_accountMgr.isNormalUser(caller.getId())) {
245+
//check permissions;
246+
_accountMgr.checkAccess(caller, _domainMgr.getDomain(domainId));
247+
} else {
248+
throw new PermissionDeniedException(exceptionMessage);
249+
}
227250
}
228251

229252
@Override
@@ -237,13 +260,29 @@ public ResourceObjectType getResourceType(String resourceTypeStr) {
237260
throw new InvalidParameterValueException("Invalid resource type " + resourceTypeStr);
238261
}
239262

263+
@Override
264+
public String getUuid(String resourceId, ResourceObjectType resourceType) {
265+
if (!StringUtils.isNumeric(resourceId)) {
266+
return resourceId;
267+
}
268+
269+
Class<?> clazz = s_typeMap.get(resourceType);
270+
271+
Object entity = _entityMgr.findById(clazz, resourceId);
272+
if (entity != null && entity instanceof Identity) {
273+
return ((Identity)entity).getUuid();
274+
}
275+
276+
return resourceId;
277+
}
278+
240279
@Override
241280
@DB
242281
@ActionEvent(eventType = EventTypes.EVENT_TAGS_CREATE, eventDescription = "creating resource tags")
243282
public List<ResourceTag> createTags(final List<String> resourceIds, final ResourceObjectType resourceType, final Map<String, String> tags, final String customer) {
244283
final Account caller = CallContext.current().getCallingAccount();
245284

246-
final List<ResourceTag> resourceTags = new ArrayList<ResourceTag>(tags.size());
285+
final List<ResourceTag> resourceTags = new ArrayList<>(tags.size());
247286

248287
Transaction.execute(new TransactionCallbackNoReturn() {
249288
@Override
@@ -261,17 +300,8 @@ public void doInTransactionWithoutResult(TransactionStatus status) {
261300
Long domainId = accountDomainPair.second();
262301
Long accountId = accountDomainPair.first();
263302

264-
if ((domainId != null) && (domainId == -1)) {
265-
throw new CloudRuntimeException("Invalid DomainId : -1");
266-
}
267-
if (accountId != null) {
268-
_accountMgr.checkAccess(caller, null, false, _accountMgr.getAccount(accountId));
269-
} else if (domainId != null && !_accountMgr.isNormalUser(caller.getId())) {
270-
//check permissions;
271-
_accountMgr.checkAccess(caller, _domainMgr.getDomain(domainId));
272-
} else {
273-
throw new PermissionDeniedException("Account " + caller + " doesn't have permissions to create tags" + " for resource " + key);
274-
}
303+
checkResourceAccessible(accountId, domainId, "Account '" + caller +
304+
"' doesn't have permissions to create tags" + " for resource '" + id + "(" + key + ")'.");
275305

276306
String value = tags.get(key);
277307

@@ -290,83 +320,77 @@ public void doInTransactionWithoutResult(TransactionStatus status) {
290320
return resourceTags;
291321
}
292322

293-
@Override
294-
public String getUuid(String resourceId, ResourceObjectType resourceType) {
295-
if (!StringUtils.isNumeric(resourceId)) {
296-
return resourceId;
297-
}
298-
299-
Class<?> clazz = s_typeMap.get(resourceType);
300-
301-
Object entity = _entityMgr.findById(clazz, resourceId);
302-
if (entity != null && entity instanceof Identity) {
303-
return ((Identity)entity).getUuid();
304-
}
323+
private List<? extends ResourceTag> searchResourceTags(List<String> resourceIds, ResourceObjectType resourceType) {
324+
List<String> resourceUuids = resourceIds.stream().map(resourceId -> getUuid(resourceId, resourceType)).collect(Collectors.toList());
325+
SearchBuilder<ResourceTagVO> sb = _resourceTagDao.createSearchBuilder();
326+
sb.and("resourceUuid", sb.entity().getResourceUuid(), SearchCriteria.Op.IN);
327+
sb.and("resourceType", sb.entity().getResourceType(), SearchCriteria.Op.EQ);
305328

306-
return resourceId;
329+
SearchCriteria<ResourceTagVO> sc = sb.create();
330+
sc.setParameters("resourceUuid", resourceUuids.toArray());
331+
sc.setParameters("resourceType", resourceType);
332+
return _resourceTagDao.search(sc, null);
307333
}
308334

309335
@Override
310336
@DB
311337
@ActionEvent(eventType = EventTypes.EVENT_TAGS_DELETE, eventDescription = "deleting resource tags")
312338
public boolean deleteTags(List<String> resourceIds, ResourceObjectType resourceType, Map<String, String> tags) {
313339
Account caller = CallContext.current().getCallingAccount();
314-
315-
SearchBuilder<ResourceTagVO> sb = _resourceTagDao.createSearchBuilder();
316-
sb.and().op("resourceId", sb.entity().getResourceId(), SearchCriteria.Op.IN);
317-
sb.or("resourceUuid", sb.entity().getResourceUuid(), SearchCriteria.Op.IN);
318-
sb.cp();
319-
sb.and("resourceType", sb.entity().getResourceType(), SearchCriteria.Op.EQ);
320-
321-
SearchCriteria<ResourceTagVO> sc = sb.create();
322-
sc.setParameters("resourceId", resourceIds.toArray());
323-
sc.setParameters("resourceUuid", resourceIds.toArray());
324-
sc.setParameters("resourceType", resourceType);
325-
326-
List<? extends ResourceTag> resourceTags = _resourceTagDao.search(sc, null);
327-
;
328-
final List<ResourceTag> tagsToRemove = new ArrayList<ResourceTag>();
340+
if(s_logger.isDebugEnabled()) {
341+
s_logger.debug("ResourceIds to Find " + String.join(", ", resourceIds));
342+
}
343+
List<? extends ResourceTag> resourceTags = searchResourceTags(resourceIds, resourceType);
344+
final List<ResourceTag> tagsToDelete = new ArrayList<>();
329345

330346
// Finalize which tags should be removed
331347
for (ResourceTag resourceTag : resourceTags) {
332348
//1) validate the permissions
349+
if(s_logger.isDebugEnabled()) {
350+
s_logger.debug("Resource Tag Id: " + resourceTag.getResourceId());
351+
s_logger.debug("Resource Tag AccountId: " + resourceTag.getAccountId());
352+
}
333353
Account owner = _accountMgr.getAccount(resourceTag.getAccountId());
354+
if(s_logger.isDebugEnabled()) {
355+
s_logger.debug("Resource Owner: " + owner);
356+
}
334357
_accountMgr.checkAccess(caller, null, false, owner);
335358
//2) Only remove tag if it matches key value pairs
336-
if (tags != null && !tags.isEmpty()) {
359+
if (MapUtils.isEmpty(tags)) {
360+
tagsToDelete.add(resourceTag);
361+
} else {
337362
for (String key : tags.keySet()) {
338-
boolean canBeRemoved = false;
363+
boolean deleteTag = false;
339364
if (resourceTag.getKey().equalsIgnoreCase(key)) {
340365
String value = tags.get(key);
341366
if (value != null) {
342367
if (resourceTag.getValue().equalsIgnoreCase(value)) {
343-
canBeRemoved = true;
368+
deleteTag = true;
344369
}
345370
} else {
346-
canBeRemoved = true;
371+
deleteTag = true;
347372
}
348-
if (canBeRemoved) {
349-
tagsToRemove.add(resourceTag);
373+
if (deleteTag) {
374+
tagsToDelete.add(resourceTag);
350375
break;
351376
}
352377
}
353378
}
354-
} else {
355-
tagsToRemove.add(resourceTag);
356379
}
357380
}
358381

359-
if (tagsToRemove.isEmpty()) {
360-
throw new InvalidParameterValueException("Unable to find tags by parameters specified");
382+
if (tagsToDelete.isEmpty()) {
383+
throw new InvalidParameterValueException("Unable to find any tags which conform to specified delete parameters.");
361384
}
362385

363386
//Remove the tags
364387
Transaction.execute(new TransactionCallbackNoReturn() {
365388
@Override
366389
public void doInTransactionWithoutResult(TransactionStatus status) {
367-
for (ResourceTag tagToRemove : tagsToRemove) {
390+
for (ResourceTag tagToRemove : tagsToDelete) {
368391
_resourceTagDao.remove(tagToRemove.getId());
369-
s_logger.debug("Removed the tag " + tagToRemove);
392+
s_logger.debug("Removed the tag '" + tagToRemove + "' for resources (" +
393+
String.join(", ", resourceIds) + ")");
370394
}
371395
}
372396
});
@@ -375,7 +399,7 @@ public void doInTransactionWithoutResult(TransactionStatus status) {
375399
}
376400

377401
@Override
378-
public List<? extends ResourceTag> listByResourceTypeAndId(ResourceObjectType type, long resourceId) {
379-
return _resourceTagDao.listBy(resourceId, type);
402+
public List<? extends ResourceTag> listByResourceTypeAndId(ResourceObjectType resourceType, long resourceId) {
403+
return _resourceTagDao.listBy(resourceId, resourceType);
380404
}
381405
}

setup/dev/advanced.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@
154154
},
155155
"logger":
156156
{
157-
"LogFolderPath": "/tmp/"
157+
"LogFolderPath": "/tmp"
158158
},
159159
"globalConfig": [
160160
{

setup/dev/advancedsg.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
},
110110
"logger":
111111
{
112-
"LogFolderPath": "/tmp/"
112+
"LogFolderPath": "/tmp"
113113
},
114114
"globalConfig": [
115115
{

setup/dev/basic.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@
110110
},
111111
"logger":
112112
{
113-
"LogFolderPath": "/tmp/"
113+
"LogFolderPath": "/tmp"
114114
},
115115
"globalConfig": [
116116
{

setup/dev/local.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
},
2626
"logger":
2727
{
28-
"LogFolderPath": "/tmp/"
28+
"LogFolderPath": "/tmp"
2929
},
3030
"mgtSvr": [
3131
{

setup/dev/s3.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@
115115
],
116116
"logger":
117117
{
118-
"LogFolderPath": "/tmp/"
118+
"LogFolderPath": "/tmp"
119119
},
120120
"mgtSvr": [
121121
{

0 commit comments

Comments
 (0)