diff --git a/api/src/main/java/com/cloud/user/AccountService.java b/api/src/main/java/com/cloud/user/AccountService.java index c0ebcf09f59b..4cc418e8fc9b 100644 --- a/api/src/main/java/com/cloud/user/AccountService.java +++ b/api/src/main/java/com/cloud/user/AccountService.java @@ -36,6 +36,7 @@ import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; +import org.apache.cloudstack.backup.BackupOffering; public interface AccountService { @@ -115,6 +116,8 @@ User createUser(String userName, String password, String firstName, String lastN void checkAccess(Account account, VpcOffering vof, DataCenter zone) throws PermissionDeniedException; + void checkAccess(Account account, BackupOffering bof) throws PermissionDeniedException; + void checkAccess(User user, ControlledEntity entity); void checkAccess(Account account, AccessType accessType, boolean sameOwner, String apiName, ControlledEntity... entities) throws PermissionDeniedException; diff --git a/api/src/main/java/org/apache/cloudstack/acl/SecurityChecker.java b/api/src/main/java/org/apache/cloudstack/acl/SecurityChecker.java index 82a8ec5fe932..fa17df7c6ed4 100644 --- a/api/src/main/java/org/apache/cloudstack/acl/SecurityChecker.java +++ b/api/src/main/java/org/apache/cloudstack/acl/SecurityChecker.java @@ -27,6 +27,8 @@ import com.cloud.user.User; import com.cloud.utils.component.Adapter; +import org.apache.cloudstack.backup.BackupOffering; + /** * SecurityChecker checks the ownership and access control to objects within */ @@ -145,4 +147,6 @@ boolean checkAccess(Account caller, AccessType accessType, String action, Contro boolean checkAccess(Account account, NetworkOffering nof, DataCenter zone) throws PermissionDeniedException; boolean checkAccess(Account account, VpcOffering vof, DataCenter zone) throws PermissionDeniedException; + + boolean checkAccess(Account account, BackupOffering bof) throws PermissionDeniedException; } diff --git a/api/src/main/java/org/apache/cloudstack/api/BaseBackupListCmd.java b/api/src/main/java/org/apache/cloudstack/api/BaseBackupListCmd.java index 0aa8366bcd5c..2a64a1fb6fd8 100644 --- a/api/src/main/java/org/apache/cloudstack/api/BaseBackupListCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/BaseBackupListCmd.java @@ -25,7 +25,7 @@ import org.apache.cloudstack.backup.BackupOffering; import org.apache.cloudstack.context.CallContext; -public abstract class BaseBackupListCmd extends BaseListCmd { +public abstract class BaseBackupListCmd extends BaseListAccountResourcesCmd { protected void setupResponseBackupOfferingsList(final List offerings, final Integer count) { final ListResponse response = new ListResponse<>(); diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java index 7d3902bc4902..18aeade9e5ba 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/ImportBackupOfferingCmd.java @@ -27,6 +27,7 @@ import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.BackupOfferingResponse; +import org.apache.cloudstack.api.response.DomainResponse; import org.apache.cloudstack.api.response.ZoneResponse; import org.apache.cloudstack.backup.BackupManager; import org.apache.cloudstack.backup.BackupOffering; @@ -40,6 +41,11 @@ import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.ResourceUnavailableException; import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.commons.collections.CollectionUtils; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; @APICommand(name = "importBackupOffering", description = "Imports a backup offering using a backup provider", @@ -76,6 +82,13 @@ public class ImportBackupOfferingCmd extends BaseAsyncCmd { description = "Whether users are allowed to create adhoc backups and backup schedules", required = true) private Boolean userDrivenBackups; + @Parameter(name = ApiConstants.DOMAIN_ID, + type = CommandType.LIST, + collectionType = CommandType.UUID, + entityType = DomainResponse.class, + description = "the ID of the containing domain(s), null for public offerings") + private List domainIds; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -100,6 +113,15 @@ public Boolean getUserDrivenBackups() { return userDrivenBackups == null ? false : userDrivenBackups; } + public List getDomainIds() { + if (CollectionUtils.isNotEmpty(domainIds)) { + Set set = new LinkedHashSet<>(domainIds); + domainIds.clear(); + domainIds.addAll(set); + } + return domainIds; + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/UpdateBackupOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/UpdateBackupOfferingCmd.java index 9de06715ee74..0c9a412b1ed1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/UpdateBackupOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/backup/UpdateBackupOfferingCmd.java @@ -25,19 +25,24 @@ import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.offering.DomainAndZoneIdResolver; import org.apache.cloudstack.api.response.BackupOfferingResponse; import org.apache.cloudstack.backup.BackupManager; import org.apache.cloudstack.backup.BackupOffering; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; +import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import com.cloud.exception.InvalidParameterValueException; import com.cloud.user.Account; import com.cloud.utils.exception.CloudRuntimeException; +import java.util.List; +import java.util.function.LongFunction; + @APICommand(name = "updateBackupOffering", description = "Updates a backup offering.", responseObject = BackupOfferingResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.16.0") -public class UpdateBackupOfferingCmd extends BaseCmd { +public class UpdateBackupOfferingCmd extends BaseCmd implements DomainAndZoneIdResolver { @Inject private BackupManager backupManager; @@ -57,6 +62,13 @@ public class UpdateBackupOfferingCmd extends BaseCmd { @Parameter(name = ApiConstants.ALLOW_USER_DRIVEN_BACKUPS, type = CommandType.BOOLEAN, description = "Whether to allow user driven backups or not") private Boolean allowUserDrivenBackups; + @Parameter(name = ApiConstants.DOMAIN_ID, + type = CommandType.STRING, + description = "the ID of the containing domain(s) as comma separated string, public for public offerings", + since = "4.23.0", + length = 4096) + private String domainIds; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -82,7 +94,7 @@ public Boolean getAllowUserDrivenBackups() { @Override public void execute() { try { - if (StringUtils.isAllEmpty(getName(), getDescription()) && getAllowUserDrivenBackups() == null) { + if (StringUtils.isAllEmpty(getName(), getDescription()) && getAllowUserDrivenBackups() == null && CollectionUtils.isEmpty(getDomainIds())) { throw new InvalidParameterValueException(String.format("Can't update Backup Offering [id: %s] because there are no parameters to be updated, at least one of the", "following should be informed: name, description or allowUserDrivenBackups.", id)); } @@ -103,6 +115,18 @@ public void execute() { } } + public List getDomainIds() { + // backupManager may be null in unit tests where the command is spied without injection. + // Avoid creating a method reference to a null receiver which causes NPE. When backupManager + // is null, pass null as the defaultDomainsProvider so resolveDomainIds will simply return + // an empty list or parse the explicit domainIds string. + LongFunction> defaultDomainsProvider = null; + if (backupManager != null) { + defaultDomainsProvider = backupManager::getBackupOfferingDomains; + } + return resolveDomainIds(domainIds, id, defaultDomainsProvider, "backup offering"); + } + @Override public long getEntityOwnerId() { return Account.ACCOUNT_ID_SYSTEM; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateNetworkOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateNetworkOfferingCmd.java index 75fb45e1f115..67a8896eb908 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateNetworkOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/UpdateNetworkOfferingCmd.java @@ -16,7 +16,6 @@ // under the License. package org.apache.cloudstack.api.command.admin.network; -import java.util.ArrayList; import java.util.List; import org.apache.cloudstack.api.APICommand; @@ -26,18 +25,16 @@ import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.offering.DomainAndZoneIdResolver; import org.apache.cloudstack.api.response.NetworkOfferingResponse; -import org.apache.commons.lang3.StringUtils; -import com.cloud.dc.DataCenter; -import com.cloud.domain.Domain; -import com.cloud.exception.InvalidParameterValueException; + import com.cloud.offering.NetworkOffering; import com.cloud.user.Account; @APICommand(name = "updateNetworkOffering", description = "Updates a network offering.", responseObject = NetworkOfferingResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) -public class UpdateNetworkOfferingCmd extends BaseCmd { +public class UpdateNetworkOfferingCmd extends BaseCmd implements DomainAndZoneIdResolver { ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// @@ -129,63 +126,11 @@ public String getTags() { } public List getDomainIds() { - List validDomainIds = new ArrayList<>(); - if (StringUtils.isNotEmpty(domainIds)) { - if (domainIds.contains(",")) { - String[] domains = domainIds.split(","); - for (String domain : domains) { - Domain validDomain = _entityMgr.findByUuid(Domain.class, domain.trim()); - if (validDomain != null) { - validDomainIds.add(validDomain.getId()); - } else { - throw new InvalidParameterValueException("Failed to create network offering because invalid domain has been specified."); - } - } - } else { - domainIds = domainIds.trim(); - if (!domainIds.matches("public")) { - Domain validDomain = _entityMgr.findByUuid(Domain.class, domainIds.trim()); - if (validDomain != null) { - validDomainIds.add(validDomain.getId()); - } else { - throw new InvalidParameterValueException("Failed to create network offering because invalid domain has been specified."); - } - } - } - } else { - validDomainIds.addAll(_configService.getNetworkOfferingDomains(id)); - } - return validDomainIds; + return resolveDomainIds(domainIds, id, _configService::getNetworkOfferingDomains, "network offering"); } public List getZoneIds() { - List validZoneIds = new ArrayList<>(); - if (StringUtils.isNotEmpty(zoneIds)) { - if (zoneIds.contains(",")) { - String[] zones = zoneIds.split(","); - for (String zone : zones) { - DataCenter validZone = _entityMgr.findByUuid(DataCenter.class, zone.trim()); - if (validZone != null) { - validZoneIds.add(validZone.getId()); - } else { - throw new InvalidParameterValueException("Failed to create network offering because invalid zone has been specified."); - } - } - } else { - zoneIds = zoneIds.trim(); - if (!zoneIds.matches("all")) { - DataCenter validZone = _entityMgr.findByUuid(DataCenter.class, zoneIds.trim()); - if (validZone != null) { - validZoneIds.add(validZone.getId()); - } else { - throw new InvalidParameterValueException("Failed to create network offering because invalid zone has been specified."); - } - } - } - } else { - validZoneIds.addAll(_configService.getNetworkOfferingZones(id)); - } - return validZoneIds; + return resolveZoneIds(zoneIds, id, _configService::getNetworkOfferingZones, "network offering"); } ///////////////////////////////////////////////////// diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateDiskOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateDiskOfferingCmd.java index 370453804cfc..685022bdae46 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateDiskOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateDiskOfferingCmd.java @@ -16,7 +16,6 @@ // under the License. package org.apache.cloudstack.api.command.admin.offering; -import java.util.ArrayList; import java.util.List; import com.cloud.offering.DiskOffering.State; @@ -27,19 +26,18 @@ import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.offering.DomainAndZoneIdResolver; import org.apache.cloudstack.api.response.DiskOfferingResponse; import org.apache.commons.lang3.EnumUtils; import org.apache.commons.lang3.StringUtils; -import com.cloud.dc.DataCenter; -import com.cloud.domain.Domain; import com.cloud.exception.InvalidParameterValueException; import com.cloud.offering.DiskOffering; import com.cloud.user.Account; @APICommand(name = "updateDiskOffering", description = "Updates a disk offering.", responseObject = DiskOfferingResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) -public class UpdateDiskOfferingCmd extends BaseCmd { +public class UpdateDiskOfferingCmd extends BaseCmd implements DomainAndZoneIdResolver { ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// @@ -151,63 +149,11 @@ public Boolean getDisplayOffering() { } public List getDomainIds() { - List validDomainIds = new ArrayList<>(); - if (StringUtils.isNotEmpty(domainIds)) { - if (domainIds.contains(",")) { - String[] domains = domainIds.split(","); - for (String domain : domains) { - Domain validDomain = _entityMgr.findByUuid(Domain.class, domain.trim()); - if (validDomain != null) { - validDomainIds.add(validDomain.getId()); - } else { - throw new InvalidParameterValueException("Failed to create disk offering because invalid domain has been specified."); - } - } - } else { - domainIds = domainIds.trim(); - if (!domainIds.matches("public")) { - Domain validDomain = _entityMgr.findByUuid(Domain.class, domainIds.trim()); - if (validDomain != null) { - validDomainIds.add(validDomain.getId()); - } else { - throw new InvalidParameterValueException("Failed to create disk offering because invalid domain has been specified."); - } - } - } - } else { - validDomainIds.addAll(_configService.getDiskOfferingDomains(id)); - } - return validDomainIds; + return resolveDomainIds(domainIds, id, _configService::getDiskOfferingDomains, "disk offering"); } public List getZoneIds() { - List validZoneIds = new ArrayList<>(); - if (StringUtils.isNotEmpty(zoneIds)) { - if (zoneIds.contains(",")) { - String[] zones = zoneIds.split(","); - for (String zone : zones) { - DataCenter validZone = _entityMgr.findByUuid(DataCenter.class, zone.trim()); - if (validZone != null) { - validZoneIds.add(validZone.getId()); - } else { - throw new InvalidParameterValueException("Failed to create disk offering because invalid zone has been specified."); - } - } - } else { - zoneIds = zoneIds.trim(); - if (!zoneIds.matches("all")) { - DataCenter validZone = _entityMgr.findByUuid(DataCenter.class, zoneIds.trim()); - if (validZone != null) { - validZoneIds.add(validZone.getId()); - } else { - throw new InvalidParameterValueException("Failed to create disk offering because invalid zone has been specified."); - } - } - } - } else { - validZoneIds.addAll(_configService.getDiskOfferingZones(id)); - } - return validZoneIds; + return resolveZoneIds(zoneIds, id, _configService::getDiskOfferingZones, "disk offering"); } public String getTags() { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java index 9d973dfc524b..b2db8101a80b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/offering/UpdateServiceOfferingCmd.java @@ -16,7 +16,6 @@ // under the License. package org.apache.cloudstack.api.command.admin.offering; -import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -28,19 +27,18 @@ import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.offering.DomainAndZoneIdResolver; import org.apache.cloudstack.api.response.ServiceOfferingResponse; import org.apache.commons.lang3.EnumUtils; import org.apache.commons.lang3.StringUtils; -import com.cloud.dc.DataCenter; -import com.cloud.domain.Domain; import com.cloud.exception.InvalidParameterValueException; import com.cloud.offering.ServiceOffering; import com.cloud.user.Account; @APICommand(name = "updateServiceOffering", description = "Updates a service offering.", responseObject = ServiceOfferingResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) -public class UpdateServiceOfferingCmd extends BaseCmd { +public class UpdateServiceOfferingCmd extends BaseCmd implements DomainAndZoneIdResolver { ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// @@ -130,63 +128,11 @@ public Integer getSortKey() { } public List getDomainIds() { - List validDomainIds = new ArrayList<>(); - if (StringUtils.isNotEmpty(domainIds)) { - if (domainIds.contains(",")) { - String[] domains = domainIds.split(","); - for (String domain : domains) { - Domain validDomain = _entityMgr.findByUuid(Domain.class, domain.trim()); - if (validDomain != null) { - validDomainIds.add(validDomain.getId()); - } else { - throw new InvalidParameterValueException("Failed to create service offering because invalid domain has been specified."); - } - } - } else { - domainIds = domainIds.trim(); - if (!domainIds.matches("public")) { - Domain validDomain = _entityMgr.findByUuid(Domain.class, domainIds.trim()); - if (validDomain != null) { - validDomainIds.add(validDomain.getId()); - } else { - throw new InvalidParameterValueException("Failed to create service offering because invalid domain has been specified."); - } - } - } - } else { - validDomainIds.addAll(_configService.getServiceOfferingDomains(id)); - } - return validDomainIds; + return resolveDomainIds(domainIds, id, _configService::getServiceOfferingDomains, "service offering"); } public List getZoneIds() { - List validZoneIds = new ArrayList<>(); - if (StringUtils.isNotEmpty(zoneIds)) { - if (zoneIds.contains(",")) { - String[] zones = zoneIds.split(","); - for (String zone : zones) { - DataCenter validZone = _entityMgr.findByUuid(DataCenter.class, zone.trim()); - if (validZone != null) { - validZoneIds.add(validZone.getId()); - } else { - throw new InvalidParameterValueException("Failed to create service offering because invalid zone has been specified."); - } - } - } else { - zoneIds = zoneIds.trim(); - if (!zoneIds.matches("all")) { - DataCenter validZone = _entityMgr.findByUuid(DataCenter.class, zoneIds.trim()); - if (validZone != null) { - validZoneIds.add(validZone.getId()); - } else { - throw new InvalidParameterValueException("Failed to create service offering because invalid zone has been specified."); - } - } - } - } else { - validZoneIds.addAll(_configService.getServiceOfferingZones(id)); - } - return validZoneIds; + return resolveZoneIds(zoneIds, id, _configService::getServiceOfferingZones, "service offering"); } public String getStorageTags() { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/UpdateVPCOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/UpdateVPCOfferingCmd.java index b59837281ef3..4efaf532ee2b 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/UpdateVPCOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/UpdateVPCOfferingCmd.java @@ -16,7 +16,6 @@ // under the License. package org.apache.cloudstack.api.command.admin.vpc; -import java.util.ArrayList; import java.util.List; import org.apache.cloudstack.api.APICommand; @@ -26,19 +25,16 @@ import org.apache.cloudstack.api.BaseAsyncCmd; import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.offering.DomainAndZoneIdResolver; import org.apache.cloudstack.api.response.VpcOfferingResponse; -import org.apache.commons.lang3.StringUtils; -import com.cloud.dc.DataCenter; -import com.cloud.domain.Domain; import com.cloud.event.EventTypes; -import com.cloud.exception.InvalidParameterValueException; import com.cloud.network.vpc.VpcOffering; import com.cloud.user.Account; @APICommand(name = "updateVPCOffering", description = "Updates VPC offering", responseObject = VpcOfferingResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false) -public class UpdateVPCOfferingCmd extends BaseAsyncCmd { +public class UpdateVPCOfferingCmd extends BaseAsyncCmd implements DomainAndZoneIdResolver { ///////////////////////////////////////////////////// //////////////// API parameters ///////////////////// @@ -92,63 +88,11 @@ public String getState() { } public List getDomainIds() { - List validDomainIds = new ArrayList<>(); - if (StringUtils.isNotEmpty(domainIds)) { - if (domainIds.contains(",")) { - String[] domains = domainIds.split(","); - for (String domain : domains) { - Domain validDomain = _entityMgr.findByUuid(Domain.class, domain.trim()); - if (validDomain != null) { - validDomainIds.add(validDomain.getId()); - } else { - throw new InvalidParameterValueException("Failed to create VPC offering because invalid domain has been specified."); - } - } - } else { - domainIds = domainIds.trim(); - if (!domainIds.matches("public")) { - Domain validDomain = _entityMgr.findByUuid(Domain.class, domainIds.trim()); - if (validDomain != null) { - validDomainIds.add(validDomain.getId()); - } else { - throw new InvalidParameterValueException("Failed to create VPC offering because invalid domain has been specified."); - } - } - } - } else { - validDomainIds.addAll(_vpcProvSvc.getVpcOfferingDomains(id)); - } - return validDomainIds; + return resolveDomainIds(domainIds, id, _vpcProvSvc::getVpcOfferingDomains, "VPC offering"); } public List getZoneIds() { - List validZoneIds = new ArrayList<>(); - if (StringUtils.isNotEmpty(zoneIds)) { - if (zoneIds.contains(",")) { - String[] zones = zoneIds.split(","); - for (String zone : zones) { - DataCenter validZone = _entityMgr.findByUuid(DataCenter.class, zone.trim()); - if (validZone != null) { - validZoneIds.add(validZone.getId()); - } else { - throw new InvalidParameterValueException("Failed to create VPC offering because invalid zone has been specified."); - } - } - } else { - zoneIds = zoneIds.trim(); - if (!zoneIds.matches("all")) { - DataCenter validZone = _entityMgr.findByUuid(DataCenter.class, zoneIds.trim()); - if (validZone != null) { - validZoneIds.add(validZone.getId()); - } else { - throw new InvalidParameterValueException("Failed to create VPC offering because invalid zone has been specified."); - } - } - } - } else { - validZoneIds.addAll(_vpcProvSvc.getVpcOfferingZones(id)); - } - return validZoneIds; + return resolveZoneIds(zoneIds, id, _vpcProvSvc::getVpcOfferingZones, "VPC offering"); } public Integer getSortKey() { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/offering/DomainAndZoneIdResolver.java b/api/src/main/java/org/apache/cloudstack/api/command/offering/DomainAndZoneIdResolver.java new file mode 100644 index 000000000000..b302c4a9beec --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/offering/DomainAndZoneIdResolver.java @@ -0,0 +1,114 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 org.apache.cloudstack.api.command.offering; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.LongFunction; + +import com.cloud.dc.DataCenter; +import com.cloud.domain.Domain; +import com.cloud.exception.InvalidParameterValueException; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Helper for commands that accept a domainIds or zoneIds string and need to + * resolve them to lists of IDs, falling back to an offering-specific + * default provider. + */ +public interface DomainAndZoneIdResolver { + /** + * Parse the provided domainIds string and return a list of domain IDs. + * If domainIds is empty, the defaultDomainsProvider will be invoked with the + * provided resource id to obtain the current domains. + */ + default List resolveDomainIds(final String domainIds, final Long id, final LongFunction> defaultDomainsProvider, final String resourceTypeName) { + final List validDomainIds = new ArrayList<>(); + final BaseCmd base = (BaseCmd) this; + final Logger logger = LogManager.getLogger(base.getClass()); + + if (StringUtils.isEmpty(domainIds)) { + if (defaultDomainsProvider != null) { + final List defaults = defaultDomainsProvider.apply(id); + if (defaults != null) { + validDomainIds.addAll(defaults); + } + } + return validDomainIds; + } + + final String[] domains = domainIds.split(","); + final String type = (resourceTypeName == null || resourceTypeName.isEmpty()) ? "offering" : resourceTypeName; + for (String domain : domains) { + final String trimmed = domain == null ? "" : domain.trim(); + if (trimmed.isEmpty() || "public".equalsIgnoreCase(trimmed)) { + continue; + } + + final Domain validDomain = base._entityMgr.findByUuid(Domain.class, trimmed); + if (validDomain == null) { + logger.warn("Invalid domain specified for {}", type); + throw new InvalidParameterValueException("Failed to create " + type + " because invalid domain has been specified."); + } + validDomainIds.add(validDomain.getId()); + } + + return validDomainIds; + } + + /** + * Parse the provided zoneIds string and return a list of zone IDs. + * If zoneIds is empty, the defaultZonesProvider will be invoked with the + * provided resource id to obtain the current zones. + */ + default List resolveZoneIds(final String zoneIds, final Long id, final LongFunction> defaultZonesProvider, final String resourceTypeName) { + final List validZoneIds = new ArrayList<>(); + final BaseCmd base = (BaseCmd) this; + final Logger logger = LogManager.getLogger(base.getClass()); + + if (StringUtils.isEmpty(zoneIds)) { + if (defaultZonesProvider != null) { + final List defaults = defaultZonesProvider.apply(id); + if (defaults != null) { + validZoneIds.addAll(defaults); + } + } + return validZoneIds; + } + + final String[] zones = zoneIds.split(","); + final String type = (resourceTypeName == null || resourceTypeName.isEmpty()) ? "offering" : resourceTypeName; + for (String zone : zones) { + final String trimmed = zone == null ? "" : zone.trim(); + if (trimmed.isEmpty() || "all".equalsIgnoreCase(trimmed)) { + continue; + } + + final DataCenter validZone = base._entityMgr.findByUuid(DataCenter.class, trimmed); + if (validZone == null) { + logger.warn("Invalid zone specified for {}: {}", type, trimmed); + throw new InvalidParameterValueException("Failed to create " + type + " because invalid zone has been specified."); + } + validZoneIds.add(validZone.getId()); + } + + return validZoneIds; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java index 4120f68d9da3..b28690c55090 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java @@ -61,6 +61,16 @@ public class BackupOfferingResponse extends BaseResponse { @Param(description = "zone name") private String zoneName; + @SerializedName(ApiConstants.DOMAIN_ID) + @Param(description = "the domain ID(s) this backup offering belongs to.", + since = "4.23.0") + private String domainId; + + @SerializedName(ApiConstants.DOMAIN) + @Param(description = "the domain name(s) this backup offering belongs to.", + since = "4.23.0") + private String domain; + @SerializedName(ApiConstants.CROSS_ZONE_INSTANCE_CREATION) @Param(description = "the backups with this offering can be used to create Instances on all Zones", since = "4.22.0") private Boolean crossZoneInstanceCreation; @@ -108,4 +118,13 @@ public void setCrossZoneInstanceCreation(Boolean crossZoneInstanceCreation) { public void setCreated(Date created) { this.created = created; } + + public void setDomainId(String domainId) { + this.domainId = domainId; + } + + public void setDomain(String domain) { + this.domain = domain; + } + } diff --git a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java index db051313d962..cbaf61405970 100644 --- a/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java +++ b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java @@ -136,6 +136,8 @@ public interface BackupManager extends BackupService, Configurable, PluggableSer */ BackupOffering importBackupOffering(final ImportBackupOfferingCmd cmd); + List getBackupOfferingDomains(final Long offeringId); + /** * List backup offerings * @param ListBackupOfferingsCmd API cmd diff --git a/api/src/test/java/org/apache/cloudstack/api/command/offering/DomainAndZoneIdResolverTest.java b/api/src/test/java/org/apache/cloudstack/api/command/offering/DomainAndZoneIdResolverTest.java new file mode 100644 index 000000000000..e679bbf2d1f1 --- /dev/null +++ b/api/src/test/java/org/apache/cloudstack/api/command/offering/DomainAndZoneIdResolverTest.java @@ -0,0 +1,149 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 org.apache.cloudstack.api.command.offering; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.LongFunction; + +import com.cloud.dc.DataCenter; +import com.cloud.domain.Domain; +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.utils.db.EntityManager; +import org.apache.cloudstack.api.BaseCmd; +import org.apache.cloudstack.api.ServerApiException; +import org.junit.Assert; +import org.junit.Test; + +public class DomainAndZoneIdResolverTest { + static class TestCmd extends BaseCmd implements DomainAndZoneIdResolver { + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + // No implementation needed for tests + } + + @Override + public String getCommandName() { + return "test"; + } + + @Override + public long getEntityOwnerId() { + return 1L; + } + } + + private void setEntityMgr(final BaseCmd cmd, final EntityManager entityMgr) throws Exception { + Field f = BaseCmd.class.getDeclaredField("_entityMgr"); + f.setAccessible(true); + f.set(cmd, entityMgr); + } + + @Test + public void resolveDomainIds_usesDefaultProviderWhenEmpty() { + TestCmd cmd = new TestCmd(); + + final LongFunction> defaultsProvider = id -> Arrays.asList(100L, 200L); + + List result = cmd.resolveDomainIds("", 42L, defaultsProvider, "offering"); + Assert.assertEquals(Arrays.asList(100L, 200L), result); + } + + @Test + public void resolveDomainIds_resolvesValidUuids() throws Exception { + TestCmd cmd = new TestCmd(); + + EntityManager em = mock(EntityManager.class); + setEntityMgr(cmd, em); + + Domain d1 = mock(Domain.class); + when(d1.getId()).thenReturn(10L); + Domain d2 = mock(Domain.class); + when(d2.getId()).thenReturn(20L); + + when(em.findByUuid(Domain.class, "uuid1")).thenReturn(d1); + when(em.findByUuid(Domain.class, "uuid2")).thenReturn(d2); + + List ids = cmd.resolveDomainIds("uuid1, public, uuid2", null, null, "template"); + Assert.assertEquals(Arrays.asList(10L, 20L), ids); + } + + @Test + public void resolveDomainIds_invalidUuid_throws() throws Exception { + TestCmd cmd = new TestCmd(); + + EntityManager em = mock(EntityManager.class); + setEntityMgr(cmd, em); + + when(em.findByUuid(Domain.class, "bad-uuid")).thenReturn(null); + + Assert.assertThrows(InvalidParameterValueException.class, + () -> cmd.resolveDomainIds("bad-uuid", null, null, "offering")); + } + + @Test + public void resolveZoneIds_usesDefaultProviderWhenEmpty() { + TestCmd cmd = new TestCmd(); + + final LongFunction> defaultsProvider = id -> Collections.singletonList(300L); + + List result = cmd.resolveZoneIds("", 99L, defaultsProvider, "offering"); + Assert.assertEquals(Collections.singletonList(300L), result); + } + + @Test + public void resolveZoneIds_resolvesValidUuids() throws Exception { + TestCmd cmd = new TestCmd(); + + EntityManager em = mock(EntityManager.class); + setEntityMgr(cmd, em); + + DataCenter z1 = mock(DataCenter.class); + when(z1.getId()).thenReturn(30L); + DataCenter z2 = mock(DataCenter.class); + when(z2.getId()).thenReturn(40L); + + when(em.findByUuid(DataCenter.class, "zone-1")).thenReturn(z1); + when(em.findByUuid(DataCenter.class, "zone-2")).thenReturn(z2); + + List ids = cmd.resolveZoneIds("zone-1, all, zone-2", null, null, "service"); + Assert.assertEquals(Arrays.asList(30L, 40L), ids); + } + + @Test + public void resolveZoneIds_invalidUuid_throws() throws Exception { + TestCmd cmd = new TestCmd(); + + EntityManager em = mock(EntityManager.class); + setEntityMgr(cmd, em); + + when(em.findByUuid(DataCenter.class, "bad-zone")).thenReturn(null); + + Assert.assertThrows(InvalidParameterValueException.class, + () -> cmd.resolveZoneIds("bad-zone", null, null, "offering")); + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingDetailsVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingDetailsVO.java new file mode 100644 index 000000000000..6bdf7602a9d4 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingDetailsVO.java @@ -0,0 +1,86 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 org.apache.cloudstack.backup; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; + +import org.apache.cloudstack.api.ResourceDetail; + +@Entity +@Table(name = "backup_offering_details") +public class BackupOfferingDetailsVO implements ResourceDetail { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "backup_offering_id") + private long resourceId; + + @Column(name = "name") + private String name; + + @Column(name = "value") + private String value; + + @Column(name = "display") + private boolean display = true; + + protected BackupOfferingDetailsVO() { + } + + public BackupOfferingDetailsVO(long backupOfferingId, String name, String value, boolean display) { + this.resourceId = backupOfferingId; + this.name = name; + this.value = value; + this.display = display; + } + + @Override + public long getResourceId() { + return resourceId; + } + + public void setResourceId(long backupOfferingId) { + this.resourceId = backupOfferingId; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getValue() { + return value; + } + + @Override + public long getId() { + return id; + } + + @Override + public boolean isDisplay() { + return display; + } +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java index d30385af575d..ebeb7d4a2d59 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingVO.java @@ -17,6 +17,8 @@ package org.apache.cloudstack.backup; +import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; + import java.util.Date; import java.util.UUID; @@ -131,4 +133,9 @@ public void setDescription(String description) { public Date getCreated() { return created; } + + @Override + public String toString() { + return String.format("Backup offering %s.", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(this, "id", "name", "uuid")); + } } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java index a41e4e70d339..708faeef4643 100644 --- a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDaoImpl.java @@ -20,6 +20,8 @@ import javax.annotation.PostConstruct; import javax.inject.Inject; +import com.cloud.domain.DomainVO; +import com.cloud.domain.dao.DomainDao; import org.apache.cloudstack.api.response.BackupOfferingResponse; import org.apache.cloudstack.backup.BackupOffering; import org.apache.cloudstack.backup.BackupOfferingVO; @@ -30,10 +32,16 @@ import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; +import java.util.List; + public class BackupOfferingDaoImpl extends GenericDaoBase implements BackupOfferingDao { @Inject DataCenterDao dataCenterDao; + @Inject + BackupOfferingDetailsDao backupOfferingDetailsDao; + @Inject + DomainDao domainDao; private SearchBuilder backupPoliciesSearch; @@ -51,8 +59,9 @@ protected void init() { @Override public BackupOfferingResponse newBackupOfferingResponse(BackupOffering offering, Boolean crossZoneInstanceCreation) { - DataCenterVO zone = dataCenterDao.findById(offering.getZoneId()); + DataCenterVO zone = dataCenterDao.findById(offering.getZoneId()); + List domainIds = backupOfferingDetailsDao.findDomainIds(offering.getId()); BackupOfferingResponse response = new BackupOfferingResponse(); response.setId(offering.getUuid()); response.setName(offering.getName()); @@ -64,6 +73,18 @@ public BackupOfferingResponse newBackupOfferingResponse(BackupOffering offering, response.setZoneId(zone.getUuid()); response.setZoneName(zone.getName()); } + if (domainIds != null && !domainIds.isEmpty()) { + String domainUUIDs = domainIds.stream().map(Long::valueOf).map(domainId -> { + DomainVO domain = domainDao.findById(domainId); + return domain != null ? domain.getUuid() : ""; + }).filter(name -> !name.isEmpty()).reduce((a, b) -> a + "," + b).orElse(""); + String domainNames = domainIds.stream().map(Long::valueOf).map(domainId -> { + DomainVO domain = domainDao.findById(domainId); + return domain != null ? domain.getName() : ""; + }).filter(name -> !name.isEmpty()).reduce((a, b) -> a + "," + b).orElse(""); + response.setDomain(domainNames); + response.setDomainId(domainUUIDs); + } if (crossZoneInstanceCreation) { response.setCrossZoneInstanceCreation(true); } diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDao.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDao.java new file mode 100644 index 000000000000..390fcba1e0e7 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDao.java @@ -0,0 +1,32 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 org.apache.cloudstack.backup.dao; + +import java.util.List; + +import org.apache.cloudstack.backup.BackupOfferingDetailsVO; +import org.apache.cloudstack.resourcedetail.ResourceDetailsDao; + +import com.cloud.utils.db.GenericDao; + +public interface BackupOfferingDetailsDao extends GenericDao, ResourceDetailsDao { + List findDomainIds(final long resourceId); + List findZoneIds(final long resourceId); + String getDetail(Long backupOfferingId, String key); + List findOfferingIdsByDomainIds(List domainIds); + void updateBackupOfferingDomainIdsDetail(long backupOfferingId, List filteredDomainIds); +} diff --git a/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDaoImpl.java b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDaoImpl.java new file mode 100644 index 000000000000..f052c93f9817 --- /dev/null +++ b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDaoImpl.java @@ -0,0 +1,101 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 org.apache.cloudstack.backup.dao; + + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.cloud.utils.db.DB; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.backup.BackupOfferingDetailsVO; +import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase; +import org.springframework.stereotype.Component; + +@Component +public class BackupOfferingDetailsDaoImpl extends ResourceDetailsDaoBase implements BackupOfferingDetailsDao { + + @Override + public void addDetail(long resourceId, String key, String value, boolean display) { + super.addDetail(new BackupOfferingDetailsVO(resourceId, key, value, display)); + } + + @Override + public List findDomainIds(long resourceId) { + final List domainIds = new ArrayList<>(); + for (final BackupOfferingDetailsVO detail: findDetails(resourceId, ApiConstants.DOMAIN_ID)) { + final Long domainId = Long.valueOf(detail.getValue()); + if (domainId > 0) { + domainIds.add(domainId); + } + } + return domainIds; + } + + @Override + public List findZoneIds(long resourceId) { + final List zoneIds = new ArrayList<>(); + for (final BackupOfferingDetailsVO detail: findDetails(resourceId, ApiConstants.ZONE_ID)) { + final Long zoneId = Long.valueOf(detail.getValue()); + if (zoneId > 0) { + zoneIds.add(zoneId); + } + } + return zoneIds; + } + + @Override + public String getDetail(Long backupOfferingId, String key) { + String detailValue = null; + BackupOfferingDetailsVO backupOfferingDetail = findDetail(backupOfferingId, key); + if (backupOfferingDetail != null) { + detailValue = backupOfferingDetail.getValue(); + } + return detailValue; + } + + @Override + public List findOfferingIdsByDomainIds(List domainIds) { + Object[] dIds = domainIds.stream().map(s -> String.valueOf(s)).collect(Collectors.toList()).toArray(); + return findResourceIdsByNameAndValueIn("domainid", dIds); + } + + @DB + @Override + public void updateBackupOfferingDomainIdsDetail(long backupOfferingId, List filteredDomainIds) { + SearchBuilder sb = createSearchBuilder(); + List detailsVO = new ArrayList<>(); + sb.and("offeringId", sb.entity().getResourceId(), SearchCriteria.Op.EQ); + sb.and("detailName", sb.entity().getName(), SearchCriteria.Op.EQ); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("offeringId", String.valueOf(backupOfferingId)); + sc.setParameters("detailName", ApiConstants.DOMAIN_ID); + remove(sc); + for (Long domainId : filteredDomainIds) { + detailsVO.add(new BackupOfferingDetailsVO(backupOfferingId, ApiConstants.DOMAIN_ID, String.valueOf(domainId), false)); + } + if (!detailsVO.isEmpty()) { + for (BackupOfferingDetailsVO detailVO : detailsVO) { + persist(detailVO); + } + } + } +} diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml index d308a9e5aaf9..1846c3c62a0e 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-common-daos-between-management-and-usage-context.xml @@ -71,6 +71,7 @@ - + + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql index 7024368a1d51..3d3a8800d087 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql @@ -18,3 +18,13 @@ --; -- Schema upgrade from 4.22.1.0 to 4.23.0.0 --; + +CREATE TABLE `cloud`.`backup_offering_details` ( + `id` bigint unsigned NOT NULL auto_increment, + `backup_offering_id` bigint unsigned NOT NULL COMMENT 'Backup offering id', + `name` varchar(255) NOT NULL, + `value` varchar(1024) NOT NULL, + `display` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Should detail be displayed to the end user', + PRIMARY KEY (`id`), + CONSTRAINT `fk_offering_details__backup_offering_id` FOREIGN KEY `fk_offering_details__backup_offering_id`(`backup_offering_id`) REFERENCES `backup_offering`(`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/plugins/hypervisors/ovm3/sonar-project.properties b/plugins/hypervisors/ovm3/sonar-project.properties index d632dfb9f916..5d99997a61d6 100644 --- a/plugins/hypervisors/ovm3/sonar-project.properties +++ b/plugins/hypervisors/ovm3/sonar-project.properties @@ -24,7 +24,7 @@ sonar.projectVersion=1.0 sonar.sources=src sonar.binaries=target/classes -# Exclussions +# Exclusions sonar.exclusions=**/*Test.java # Language diff --git a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java index bc9dbfa7b436..684b379cf50d 100644 --- a/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java +++ b/plugins/network-elements/juniper-contrail/src/test/java/org/apache/cloudstack/network/contrail/management/MockAccountManager.java @@ -30,6 +30,7 @@ import org.apache.cloudstack.api.command.admin.user.MoveUserCmd; import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupResponse; import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; +import org.apache.cloudstack.backup.BackupOffering; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.acl.ControlledEntity; @@ -491,6 +492,11 @@ public void checkAccess(Account account, VpcOffering vof, DataCenter zone) throw // TODO Auto-generated method stub } + @Override + public void checkAccess(Account account, BackupOffering bof) throws PermissionDeniedException { + // TODO Auto-generated method stub + } + @Override public Pair> getKeys(GetUserKeysCmd cmd){ return null; diff --git a/pom.xml b/pom.xml index fcf11e357d23..7db515fcba76 100644 --- a/pom.xml +++ b/pom.xml @@ -53,6 +53,8 @@ 4.22.0.0 apache https://sonarcloud.io + engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingDetailsVO.java + api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java 11 diff --git a/server/src/main/java/com/cloud/acl/DomainChecker.java b/server/src/main/java/com/cloud/acl/DomainChecker.java index 97832311b178..99a1a4d5a20e 100644 --- a/server/src/main/java/com/cloud/acl/DomainChecker.java +++ b/server/src/main/java/com/cloud/acl/DomainChecker.java @@ -32,6 +32,8 @@ import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao; import org.springframework.stereotype.Component; +import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao; +import org.apache.cloudstack.backup.BackupOffering; import com.cloud.dc.DataCenter; import com.cloud.dc.DedicatedResourceVO; import com.cloud.dc.dao.DedicatedResourceDao; @@ -70,6 +72,8 @@ public class DomainChecker extends AdapterBase implements SecurityChecker { @Inject DomainDao _domainDao; @Inject + BackupOfferingDetailsDao backupOfferingDetailsDao; + @Inject AccountDao _accountDao; @Inject LaunchPermissionDao _launchPermissionDao; @@ -474,6 +478,35 @@ else if (_accountService.isNormalUser(account.getId()) return hasAccess; } + @Override + public boolean checkAccess(Account account, BackupOffering backupOffering) throws PermissionDeniedException { + boolean hasAccess = false; + if (account == null || backupOffering == null) { + hasAccess = true; + } else { + if (_accountService.isRootAdmin(account.getId())) { + hasAccess = true; + } + else if (_accountService.isNormalUser(account.getId()) + || account.getType() == Account.Type.RESOURCE_DOMAIN_ADMIN + || _accountService.isDomainAdmin(account.getId()) + || account.getType() == Account.Type.PROJECT) { + final List boDomainIds = backupOfferingDetailsDao.findDomainIds(backupOffering.getId()); + if (boDomainIds.isEmpty()) { + hasAccess = true; + } else { + for (Long domainId : boDomainIds) { + if (_domainDao.isChildDomain(domainId, account.getDomainId())) { + hasAccess = true; + break; + } + } + } + } + } + return hasAccess; + } + @Override public boolean checkAccess(Account account, DataCenter zone) throws PermissionDeniedException { if (account == null || zone.getDomainId() == null) {//public zone diff --git a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java index e2fc57b1b16d..8940aa3ae914 100644 --- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java +++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java @@ -52,6 +52,7 @@ import com.cloud.consoleproxy.ConsoleProxyManager; import com.cloud.network.router.VirtualNetworkApplianceManager; import com.cloud.storage.secondary.SecondaryStorageVmManager; +import com.cloud.utils.DomainHelper; import com.cloud.vm.VirtualMachineManager; import org.apache.cloudstack.acl.RoleType; import org.apache.cloudstack.acl.SecurityChecker; @@ -398,6 +399,8 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati ClusterDao _clusterDao; @Inject AlertManager _alertMgr; + @Inject + DomainHelper domainHelper; List _secChecker; List externalProvisioners; @@ -3509,7 +3512,7 @@ protected ServiceOfferingVO createServiceOffering(final long userId, final boole final boolean isCustomized, final boolean encryptRoot, Long vgpuProfileId, Integer gpuCount, Boolean gpuDisplay, final boolean purgeResources, Integer leaseDuration, VMLeaseManager.ExpiryAction leaseExpiryAction) { // Filter child domains when both parent and child domains are present - List filteredDomainIds = filterChildSubDomains(domainIds); + List filteredDomainIds = domainHelper.filterChildSubDomains(domainIds); // Check if user exists in the system final User user = _userDao.findById(userId); @@ -3898,7 +3901,7 @@ public ServiceOffering updateServiceOffering(final UpdateServiceOfferingCmd cmd) final Account account = _accountDao.findById(user.getAccountId()); // Filter child domains when both parent and child domains are present - List filteredDomainIds = filterChildSubDomains(domainIds); + List filteredDomainIds = domainHelper.filterChildSubDomains(domainIds); Collections.sort(filteredDomainIds); List filteredZoneIds = new ArrayList<>(); @@ -4102,7 +4105,7 @@ protected DiskOfferingVO createDiskOffering(final Long userId, final List } // Filter child domains when both parent and child domains are present - List filteredDomainIds = filterChildSubDomains(domainIds); + List filteredDomainIds = domainHelper.filterChildSubDomains(domainIds); // Check if user exists in the system final User user = _userDao.findById(userId); @@ -4378,7 +4381,7 @@ public DiskOffering updateDiskOffering(final UpdateDiskOfferingCmd cmd) { final Account account = _accountDao.findById(user.getAccountId()); // Filter child domains when both parent and child domains are present - List filteredDomainIds = filterChildSubDomains(domainIds); + List filteredDomainIds = domainHelper.filterChildSubDomains(domainIds); Collections.sort(filteredDomainIds); List filteredZoneIds = new ArrayList<>(); @@ -7385,7 +7388,7 @@ public NetworkOfferingVO doInTransaction(final TransactionStatus status) { } if (offering != null) { // Filter child domains when both parent and child domains are present - List filteredDomainIds = filterChildSubDomains(domainIds); + List filteredDomainIds = domainHelper.filterChildSubDomains(domainIds); List detailsVO = new ArrayList<>(); for (Long domainId : filteredDomainIds) { detailsVO.add(new NetworkOfferingDetailsVO(offering.getId(), Detail.domainid, String.valueOf(domainId), false)); @@ -7851,7 +7854,7 @@ public NetworkOffering updateNetworkOffering(final UpdateNetworkOfferingCmd cmd) } // Filter child domains when both parent and child domains are present - List filteredDomainIds = filterChildSubDomains(domainIds); + List filteredDomainIds = domainHelper.filterChildSubDomains(domainIds); Collections.sort(filteredDomainIds); List filteredZoneIds = new ArrayList<>(); @@ -8418,30 +8421,6 @@ private boolean checkOverlapPortableIpRange(final int regionId, final String new return false; } - private List filterChildSubDomains(final List domainIds) { - List filteredDomainIds = new ArrayList<>(); - if (domainIds != null) { - filteredDomainIds.addAll(domainIds); - } - if (filteredDomainIds.size() > 1) { - for (int i = filteredDomainIds.size() - 1; i >= 1; i--) { - long first = filteredDomainIds.get(i); - for (int j = i - 1; j >= 0; j--) { - long second = filteredDomainIds.get(j); - if (_domainDao.isChildDomain(filteredDomainIds.get(i), filteredDomainIds.get(j))) { - filteredDomainIds.remove(j); - i--; - } - if (_domainDao.isChildDomain(filteredDomainIds.get(j), filteredDomainIds.get(i))) { - filteredDomainIds.remove(i); - break; - } - } - } - } - return filteredDomainIds; - } - protected void validateCacheMode(String cacheMode){ if(cacheMode != null && !Enums.getIfPresent(DiskOffering.DiskCacheMode.class, diff --git a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java index e4219c858da6..60b93d409aab 100644 --- a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java +++ b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java @@ -63,6 +63,7 @@ import com.cloud.network.element.NsxProviderVO; import com.cloud.network.rules.RulesManager; import com.cloud.network.vpn.RemoteAccessVpnService; +import com.cloud.utils.DomainHelper; import com.cloud.vm.dao.VMInstanceDao; import com.google.common.collect.Sets; import org.apache.cloudstack.acl.ControlledEntity.ACLType; @@ -285,6 +286,8 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis @Inject DomainDao domainDao; @Inject + DomainHelper domainHelper; + @Inject private AnnotationDao annotationDao; @Inject NetworkOfferingDao _networkOfferingDao; @@ -636,7 +639,7 @@ public VpcOffering createVpcOffering(final String name, final String displayText } // Filter child domains when both parent and child domains are present - List filteredDomainIds = filterChildSubDomains(domainIds); + List filteredDomainIds = domainHelper.filterChildSubDomains(domainIds); final Map> svcProviderMap = new HashMap>(); final Set defaultProviders = new HashSet(); @@ -1118,7 +1121,7 @@ private VpcOffering updateVpcOfferingInternal(long vpcOffId, String vpcOfferingN // Filter child domains when both parent and child domains are present - List filteredDomainIds = filterChildSubDomains(domainIds); + List filteredDomainIds = domainHelper.filterChildSubDomains(domainIds); Collections.sort(filteredDomainIds); List filteredZoneIds = new ArrayList<>(); @@ -3658,30 +3661,6 @@ private boolean rollingRestartVpc(final Vpc vpc, final ReservationContext contex return _ntwkMgr.areRoutersRunning(routerDao.listByVpcId(vpc.getId())); } - private List filterChildSubDomains(final List domainIds) { - List filteredDomainIds = new ArrayList<>(); - if (domainIds != null) { - filteredDomainIds.addAll(domainIds); - } - if (filteredDomainIds.size() > 1) { - for (int i = filteredDomainIds.size() - 1; i >= 1; i--) { - long first = filteredDomainIds.get(i); - for (int j = i - 1; j >= 0; j--) { - long second = filteredDomainIds.get(j); - if (domainDao.isChildDomain(filteredDomainIds.get(i), filteredDomainIds.get(j))) { - filteredDomainIds.remove(j); - i--; - } - if (domainDao.isChildDomain(filteredDomainIds.get(j), filteredDomainIds.get(i))) { - filteredDomainIds.remove(i); - break; - } - } - } - } - return filteredDomainIds; - } - protected boolean isGlobalAcl(Long aclVpcId) { return aclVpcId != null && aclVpcId == 0; } diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java b/server/src/main/java/com/cloud/user/AccountManagerImpl.java index bbfc8fd36826..71df4fc1b01a 100644 --- a/server/src/main/java/com/cloud/user/AccountManagerImpl.java +++ b/server/src/main/java/com/cloud/user/AccountManagerImpl.java @@ -67,6 +67,7 @@ import org.apache.cloudstack.auth.UserAuthenticator; import org.apache.cloudstack.auth.UserAuthenticator.ActionOnFailedAuthentication; import org.apache.cloudstack.auth.UserTwoFactorAuthenticator; +import org.apache.cloudstack.backup.BackupOffering; import org.apache.cloudstack.config.ApiServiceConfiguration; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; @@ -3568,6 +3569,21 @@ public void checkAccess(Account account, VpcOffering vof, DataCenter zone) throw throw new PermissionDeniedException("There's no way to confirm " + account + " has access to " + vof); } + @Override + public void checkAccess(Account account, BackupOffering bof) throws PermissionDeniedException { + for (SecurityChecker checker : _securityCheckers) { + if (checker.checkAccess(account, bof)) { + if (logger.isDebugEnabled()) { + logger.debug("Access granted to " + account + " to " + bof + " by " + checker.getName()); + } + return; + } + } + + assert false : "How can all of the security checkers pass on checking this caller?"; + throw new PermissionDeniedException("There's no way to confirm " + account + " has access to " + bof); + } + @Override public void checkAccess(User user, ControlledEntity entity) throws PermissionDeniedException { for (SecurityChecker checker : _securityCheckers) { diff --git a/server/src/main/java/com/cloud/utils/DomainHelper.java b/server/src/main/java/com/cloud/utils/DomainHelper.java new file mode 100644 index 000000000000..480726d256b8 --- /dev/null +++ b/server/src/main/java/com/cloud/utils/DomainHelper.java @@ -0,0 +1,63 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 com.cloud.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import javax.inject.Inject; + +import org.springframework.stereotype.Component; + +import com.cloud.domain.dao.DomainDao; + +@Component +public class DomainHelper { + + @Inject + private DomainDao domainDao; + + /** + * + * @param domainIds List of domain IDs to filter + * @return Filtered list containing only domains that are not descendants of other domains in the list + */ + public List filterChildSubDomains(final List domainIds) { + if (domainIds == null || domainIds.size() <= 1) { + return domainIds == null ? new ArrayList<>() : new ArrayList<>(domainIds); + } + + final List result = new ArrayList<>(); + for (final Long candidate : domainIds) { + boolean isDescendant = false; + for (final Long other : domainIds) { + if (Objects.equals(candidate, other)) { + continue; + } + if (domainDao.isChildDomain(other, candidate)) { + isDescendant = true; + break; + } + } + if (!isDescendant) { + result.add(candidate); + } + } + return result; + } +} diff --git a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java index ef3ba917de74..7e98e5feebf7 100644 --- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java @@ -38,6 +38,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.utils.DomainHelper; import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.InternalIdentity; @@ -68,6 +69,7 @@ import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.backup.dao.BackupDetailsDao; import org.apache.cloudstack.backup.dao.BackupOfferingDao; +import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao; import org.apache.cloudstack.backup.dao.BackupScheduleDao; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.ConfigKey; @@ -81,12 +83,12 @@ import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; +import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.math.NumberUtils; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; -import com.amazonaws.util.CollectionUtils; import com.cloud.alert.AlertManager; import com.cloud.api.ApiDispatcher; import com.cloud.api.ApiGsonHelper; @@ -184,6 +186,8 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager { @Inject private BackupOfferingDao backupOfferingDao; @Inject + private BackupOfferingDetailsDao backupOfferingDetailsDao; + @Inject private VMInstanceDao vmInstanceDao; @Inject private AccountService accountService; @@ -237,6 +241,8 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager { private AlertManager alertManager; @Inject private GuestOSDao _guestOSDao; + @Inject + private DomainHelper domainHelper; private AsyncJobDispatcher asyncJobDispatcher; private Timer backupTimer; @@ -280,6 +286,20 @@ public BackupOffering importBackupOffering(final ImportBackupOfferingCmd cmd) { throw new CloudRuntimeException("A backup offering with the same name already exists in this zone"); } + if (CollectionUtils.isNotEmpty(cmd.getDomainIds())) { + for (final Long domainId: cmd.getDomainIds()) { + if (domainDao.findById(domainId) == null) { + throw new InvalidParameterValueException("Please specify a valid domain id"); + } + } + } + + final Account caller = CallContext.current().getCallingAccount(); + List filteredDomainIds = cmd.getDomainIds() == null ? new ArrayList<>() : new ArrayList<>(cmd.getDomainIds()); + if (filteredDomainIds.size() > 1) { + filteredDomainIds = domainHelper.filterChildSubDomains(filteredDomainIds); + } + final BackupProvider provider = getBackupProvider(cmd.getZoneId()); if (!provider.isValidProviderOffering(cmd.getZoneId(), cmd.getExternalId())) { throw new CloudRuntimeException("Backup offering '" + cmd.getExternalId() + "' does not exist on provider " + provider.getName() + " on zone " + cmd.getZoneId()); @@ -292,15 +312,34 @@ public BackupOffering importBackupOffering(final ImportBackupOfferingCmd cmd) { if (savedOffering == null) { throw new CloudRuntimeException("Unable to create backup offering: " + cmd.getExternalId() + ", name: " + cmd.getName()); } + if (CollectionUtils.isNotEmpty(filteredDomainIds)) { + List detailsVOList = new ArrayList<>(); + for (Long domainId : filteredDomainIds) { + detailsVOList.add(new BackupOfferingDetailsVO(savedOffering.getId(), ApiConstants.DOMAIN_ID, String.valueOf(domainId), false)); + } + if (!detailsVOList.isEmpty()) { + backupOfferingDetailsDao.saveDetails(detailsVOList); + } + } logger.debug("Successfully created backup offering " + cmd.getName() + " mapped to backup provider offering " + cmd.getExternalId()); return savedOffering; } + @Override + public List getBackupOfferingDomains(Long offeringId) { + final BackupOffering backupOffering = backupOfferingDao.findById(offeringId); + if (backupOffering == null) { + throw new InvalidParameterValueException("Unable to find backup offering for id: " + offeringId); + } + return backupOfferingDetailsDao.findDomainIds(offeringId); + } + @Override public Pair, Integer> listBackupOfferings(final ListBackupOfferingsCmd cmd) { final Long offeringId = cmd.getOfferingId(); final Long zoneId = cmd.getZoneId(); final String keyword = cmd.getKeyword(); + Long domainId = cmd.getDomainId(); if (offeringId != null) { BackupOfferingVO offering = backupOfferingDao.findById(offeringId); @@ -314,8 +353,13 @@ public Pair, Integer> listBackupOfferings(final ListBackupO SearchBuilder sb = backupOfferingDao.createSearchBuilder(); sb.and("zone_id", sb.entity().getZoneId(), SearchCriteria.Op.EQ); sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ); + CallContext ctx = CallContext.current(); final Account caller = ctx.getCallingAccount(); + if (Account.Type.ADMIN != caller.getType() && domainId == null) { + domainId = caller.getDomainId(); + } + if (Account.Type.NORMAL == caller.getType()) { sb.and("user_backups_allowed", sb.entity().isUserDrivenBackupAllowed(), SearchCriteria.Op.EQ); } @@ -328,13 +372,36 @@ public Pair, Integer> listBackupOfferings(final ListBackupO if (keyword != null) { sc.setParameters("name", "%" + keyword + "%"); } + if (Account.Type.NORMAL == caller.getType()) { sc.setParameters("user_backups_allowed", true); } + Pair, Integer> result = backupOfferingDao.searchAndCount(sc, searchFilter); + + if (domainId != null) { + List filteredOfferings = new ArrayList<>(); + for (BackupOfferingVO offering : result.first()) { + List offeringDomains = backupOfferingDetailsDao.findDomainIds(offering.getId()); + if (offeringDomains.isEmpty() || offeringDomains.contains(domainId) || containsParentDomain(offeringDomains, domainId)) { + filteredOfferings.add(offering); + } + } + return new Pair<>(new ArrayList<>(filteredOfferings), filteredOfferings.size()); + } + return new Pair<>(new ArrayList<>(result.first()), result.second()); } + private boolean containsParentDomain(List offeringDomains, Long domainId) { + for (Long offeringDomainId : offeringDomains) { + if (domainDao.isChildDomain(offeringDomainId, domainId)) { + return true; + } + } + return false; + } + @Override public boolean deleteBackupOffering(final Long offeringId) { final BackupOfferingVO offering = backupOfferingDao.findById(offeringId); @@ -342,6 +409,8 @@ public boolean deleteBackupOffering(final Long offeringId) { throw new CloudRuntimeException("Could not find a backup offering with id: " + offeringId); } + accountManager.checkAccess(CallContext.current().getCallingAccount(), offering); + if (backupDao.listByOfferingId(offering.getId()).size() > 0) { throw new CloudRuntimeException("Backup Offering cannot be removed as it has backups associated with it."); } @@ -452,6 +521,12 @@ public boolean assignVMToBackupOffering(Long vmId, Long offeringId) { throw new CloudRuntimeException("Provided backup offering does not exist"); } + Account owner = accountManager.getAccount(vm.getAccountId()); + if (owner == null) { + throw new CloudRuntimeException("Unable to find the owner of the VM"); + } + accountManager.checkAccess(owner, offering); + final BackupProvider backupProvider = getBackupProvider(offering.getProvider()); if (backupProvider == null) { throw new CloudRuntimeException("Failed to get the backup provider for the zone, please contact the administrator"); @@ -762,10 +837,11 @@ protected boolean deleteAllVmBackupSchedules(long vmId) { @ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_CREATE, eventDescription = "creating VM backup", async = true) public boolean createBackup(CreateBackupCmd cmd, Object job) throws ResourceAllocationException { Long vmId = cmd.getVmId(); + Account caller = CallContext.current().getCallingAccount(); final VMInstanceVO vm = findVmById(vmId); validateBackupForZone(vm.getDataCenterId()); - accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + accountManager.checkAccess(caller, null, true, vm); if (vm.getBackupOfferingId() == null) { throw new CloudRuntimeException("VM has not backup offering configured, cannot create backup before assigning it to a backup offering"); @@ -1065,7 +1141,7 @@ public boolean restoreBackup(final Long backupId) { } // This is done to handle historic backups if any with Veeam / Networker plugins - List backupVolumes = CollectionUtils.isNullOrEmpty(backup.getBackedUpVolumes()) ? + List backupVolumes = CollectionUtils.isEmpty(backup.getBackedUpVolumes()) ? vm.getBackupVolumeList() : backup.getBackedUpVolumes(); List vmVolumes = volumeDao.findByInstance(vm.getId()); if (vmVolumes.size() != backupVolumes.size()) { @@ -2112,11 +2188,15 @@ public BackupOffering updateBackupOffering(UpdateBackupOfferingCmd updateBackupO String name = updateBackupOfferingCmd.getName(); String description = updateBackupOfferingCmd.getDescription(); Boolean allowUserDrivenBackups = updateBackupOfferingCmd.getAllowUserDrivenBackups(); + List domainIds = updateBackupOfferingCmd.getDomainIds(); BackupOfferingVO backupOfferingVO = backupOfferingDao.findById(id); if (backupOfferingVO == null) { throw new InvalidParameterValueException(String.format("Unable to find Backup Offering with id: [%s].", id)); } + + accountManager.checkAccess(CallContext.current().getCallingAccount(), backupOfferingVO); + logger.debug("Trying to update Backup Offering {} to {}.", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backupOfferingVO, "uuid", "name", "description", "userDrivenBackupAllowed"), ReflectionToStringBuilderUtils.reflectOnlySelectedFields(updateBackupOfferingCmd, "name", "description", "allowUserDrivenBackups")); @@ -2139,16 +2219,43 @@ public BackupOffering updateBackupOffering(UpdateBackupOfferingCmd updateBackupO fields.add("allowUserDrivenBackups: " + allowUserDrivenBackups); } - if (!backupOfferingDao.update(id, offering)) { + if (CollectionUtils.isNotEmpty(domainIds)) { + for (final Long domainId: domainIds) { + if (domainDao.findById(domainId) == null) { + throw new InvalidParameterValueException("Please specify a valid domain id"); + } + } + } + List filteredDomainIds = domainHelper.filterChildSubDomains(domainIds); + Collections.sort(filteredDomainIds); + + boolean success = backupOfferingDao.update(id, offering); + if (!success) { logger.warn(String.format("Couldn't update Backup offering (%s) with [%s].", backupOfferingVO, String.join(", ", fields))); } + if (success) { + List existingDomainIds = backupOfferingDetailsDao.findDomainIds(id); + Collections.sort(existingDomainIds); + updateBackupOfferingDomainDetails(id, filteredDomainIds, existingDomainIds); + } + BackupOfferingVO response = backupOfferingDao.findById(id); CallContext.current().setEventDetails(String.format("Backup Offering updated [%s].", ReflectionToStringBuilderUtils.reflectOnlySelectedFields(response, "id", "name", "description", "userDrivenBackupAllowed", "externalId"))); return response; } + private void updateBackupOfferingDomainDetails(Long id, List filteredDomainIds, List existingDomainIds) { + if (existingDomainIds == null) { + existingDomainIds = new ArrayList<>(); + } + + if(!filteredDomainIds.equals(existingDomainIds)) { + backupOfferingDetailsDao.updateBackupOfferingDomainIdsDetail(id, filteredDomainIds); + } + } + Map getDetailsFromBackupDetails(Long backupId) { Map details = backupDetailsDao.listDetailsKeyPairs(backupId, true); if (details == null) { @@ -2270,7 +2377,7 @@ public void checkAndRemoveBackupOfferingBeforeExpunge(VirtualMachine vm) { return; } List backupsForVm = backupDao.listByVmIdAndOffering(vm.getDataCenterId(), vm.getId(), vm.getBackupOfferingId()); - if (org.apache.commons.collections.CollectionUtils.isEmpty(backupsForVm)) { + if (CollectionUtils.isEmpty(backupsForVm)) { removeVMFromBackupOffering(vm.getId(), true); } else { throw new CloudRuntimeException(String.format("This Instance [uuid: %s, name: %s] has a " diff --git a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-misc-context.xml b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-misc-context.xml index c633a3b0abd2..f4fd57d59fc4 100644 --- a/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-misc-context.xml +++ b/server/src/main/resources/META-INF/cloudstack/core/spring-server-core-misc-context.xml @@ -81,4 +81,6 @@ + + diff --git a/server/src/test/java/com/cloud/acl/DomainCheckerTest.java b/server/src/test/java/com/cloud/acl/DomainCheckerTest.java index a5ec41306d85..8c7817c2b842 100644 --- a/server/src/test/java/com/cloud/acl/DomainCheckerTest.java +++ b/server/src/test/java/com/cloud/acl/DomainCheckerTest.java @@ -18,6 +18,9 @@ import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.SecurityChecker; +import org.apache.cloudstack.backup.BackupOfferingVO; +import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao; +import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; @@ -35,6 +38,8 @@ import com.cloud.user.dao.AccountDao; import com.cloud.utils.Ternary; +import java.util.Collections; + @RunWith(MockitoJUnitRunner.class) public class DomainCheckerTest { @@ -46,6 +51,8 @@ public class DomainCheckerTest { DomainDao _domainDao; @Mock ProjectManager _projectMgr; + @Mock + BackupOfferingDetailsDao backupOfferingDetailsDao; @Spy @InjectMocks @@ -163,4 +170,42 @@ public void testProjectOwnerCannotAccess() { domainChecker.validateCallerHasAccessToEntityOwner(caller, entity, SecurityChecker.AccessType.ListEntry); } + @Test + public void testBackupOfferingAccessRootAdmin() { + Account rootAdmin = Mockito.mock(Account.class); + Mockito.when(rootAdmin.getId()).thenReturn(1L); + BackupOfferingVO backupOfferingVO = Mockito.mock(BackupOfferingVO.class); + Mockito.when(_accountService.isRootAdmin(rootAdmin.getId())).thenReturn(true); + + boolean hasAccess = domainChecker.checkAccess(rootAdmin, backupOfferingVO); + Assert.assertTrue(hasAccess); + } + + @Test + public void testBackupOfferingAccessDomainAdmin() { + Account domainAdmin = Mockito.mock(Account.class); + Mockito.when(domainAdmin.getId()).thenReturn(2L); + BackupOfferingVO backupOfferingVO = Mockito.mock(BackupOfferingVO.class); + AccountVO owner = Mockito.mock(AccountVO.class); + Mockito.when(_accountService.isDomainAdmin(domainAdmin.getId())).thenReturn(true); + Mockito.when(domainAdmin.getDomainId()).thenReturn(10L); + Mockito.when(_domainDao.isChildDomain(100L, 10L)).thenReturn(true); + Mockito.when(backupOfferingDetailsDao.findDomainIds(backupOfferingVO.getId())).thenReturn(Collections.singletonList(100L)); + + boolean hasAccess = domainChecker.checkAccess(domainAdmin, backupOfferingVO); + Assert.assertTrue(hasAccess); + } + + @Test + public void testBackupOfferingAccessNoAccess() { + Account normalUser = Mockito.mock(Account.class); + Mockito.when(normalUser.getId()).thenReturn(3L); + BackupOfferingVO backupOfferingVO = Mockito.mock(BackupOfferingVO.class); + Mockito.when(_accountService.isRootAdmin(normalUser.getId())).thenReturn(false); + Mockito.when(_accountService.isDomainAdmin(normalUser.getId())).thenReturn(false); + + boolean hasAccess = domainChecker.checkAccess(normalUser, backupOfferingVO); + Assert.assertFalse(hasAccess); + } + } diff --git a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java index a62f4d113afc..c2e97a7e74bd 100644 --- a/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java +++ b/server/src/test/java/com/cloud/configuration/ConfigurationManagerImplTest.java @@ -49,6 +49,7 @@ import com.cloud.user.Account; import com.cloud.user.AccountManagerImpl; import com.cloud.user.User; +import com.cloud.utils.DomainHelper; import com.cloud.utils.Pair; import com.cloud.utils.db.EntityManager; import com.cloud.utils.db.SearchCriteria; @@ -178,6 +179,8 @@ public class ConfigurationManagerImplTest { PrimaryDataStoreDao storagePoolDao; @Mock StoragePoolDetailsDao storagePoolDetailsDao; + @Mock + DomainHelper domainHelper; DeleteZoneCmd deleteZoneCmd; CreateNetworkOfferingCmd createNetworkOfferingCmd; diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java index fe4ea0838f16..1ec141a8be13 100644 --- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java +++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java @@ -59,6 +59,7 @@ import java.util.TimeZone; import java.util.UUID; +import com.cloud.domain.Domain; import com.cloud.storage.dao.SnapshotPolicyDao; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.SecurityChecker; @@ -3107,7 +3108,7 @@ public void moveVmToUserTestAccountManagerCheckAccessThrowsPermissionDeniedExcep configureDoNothingForMethodsThatWeDoNotWantToTest(); - doThrow(PermissionDeniedException.class).when(accountManager).checkAccess(Mockito.any(Account.class), Mockito.any()); + doThrow(PermissionDeniedException.class).when(accountManager).checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class)); Assert.assertThrows(PermissionDeniedException.class, () -> userVmManagerImpl.moveVmToUser(assignVmCmdMock)); } diff --git a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java index 8b13fd474947..a9c083228e2b 100644 --- a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java +++ b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java @@ -60,6 +60,7 @@ import com.cloud.user.User; import com.cloud.user.dao.AccountDao; import com.cloud.utils.DateUtil; +import com.cloud.utils.DomainHelper; import com.cloud.utils.Pair; import com.cloud.utils.db.SearchBuilder; import com.cloud.utils.db.SearchCriteria; @@ -80,11 +81,13 @@ import org.apache.cloudstack.api.command.user.backup.CreateBackupCmd; import org.apache.cloudstack.api.command.user.backup.CreateBackupScheduleCmd; import org.apache.cloudstack.api.command.user.backup.DeleteBackupScheduleCmd; +import org.apache.cloudstack.api.command.user.backup.ListBackupOfferingsCmd; import org.apache.cloudstack.api.command.user.backup.ListBackupScheduleCmd; import org.apache.cloudstack.api.response.BackupResponse; import org.apache.cloudstack.backup.dao.BackupDao; import org.apache.cloudstack.backup.dao.BackupDetailsDao; import org.apache.cloudstack.backup.dao.BackupOfferingDao; +import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao; import org.apache.cloudstack.backup.dao.BackupScheduleDao; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.ConfigKey; @@ -241,6 +244,12 @@ public class BackupManagerTest { @Mock private GuestOSDao _guestOSDao; + @Mock + private BackupOfferingDetailsDao backupOfferingDetailsDao; + + @Mock + DomainHelper domainHelper; + private Gson gson; private String[] hostPossibleValues = {"127.0.0.1", "hostname"}; @@ -352,6 +361,7 @@ public void testUpdateBackupOfferingSuccess() { when(cmd.getName()).thenReturn("New name"); when(cmd.getDescription()).thenReturn("New description"); when(cmd.getAllowUserDrivenBackups()).thenReturn(true); + when(backupOfferingDetailsDao.findDomainIds(id)).thenReturn(Collections.emptyList()); BackupOffering updated = backupManager.updateBackupOffering(cmd); assertEquals("New name", updated.getName()); @@ -1081,7 +1091,7 @@ public void testGetRootDiskInfoFromBackup() { assertEquals("root-disk-offering-uuid", VmDiskInfo.getDiskOffering().getUuid()); assertEquals(Long.valueOf(5), VmDiskInfo.getSize()); - assertEquals(null, VmDiskInfo.getDeviceId()); + assertNull(VmDiskInfo.getDeviceId()); } @Test @@ -1106,7 +1116,7 @@ public void testImportBackupOffering() { assertEquals("Test Offering", result.getName()); assertEquals("Test Description", result.getDescription()); - assertEquals(true, result.isUserDrivenBackupAllowed()); + assertTrue(result.isUserDrivenBackupAllowed()); assertEquals("external-id", result.getExternalId()); assertEquals("testbackupprovider", result.getProvider()); } @@ -1149,6 +1159,8 @@ public void testAssignVMToBackupOffering() { VMInstanceVO vm = mock(VMInstanceVO.class); when(vm.getId()).thenReturn(vmId); BackupOfferingVO offering = mock(BackupOfferingVO.class); + Account owner = mock(Account.class); + overrideBackupFrameworkConfigValue(); @@ -1159,6 +1171,8 @@ public void testAssignVMToBackupOffering() { when(vm.getBackupOfferingId()).thenReturn(null); when(offering.getProvider()).thenReturn("testbackupprovider"); when(backupProvider.assignVMToBackupOffering(vm, offering)).thenReturn(true); + when(vm.getAccountId()).thenReturn(3L); + when(accountManager.getAccount(vm.getAccountId())).thenReturn(owner); when(vmInstanceDao.update(1L, vm)).thenReturn(true); try (MockedStatic ignored2 = Mockito.mockStatic(UsageEventUtils.class)) { @@ -2156,4 +2170,352 @@ public void testRestoreBackupVolumeMismatch() { verify(vmInstanceDao, times(1)).findByIdIncludingRemoved(vmId); verify(volumeDao, times(1)).findByInstance(vmId); } + + @Test + public void getBackupOfferingDomainsTestOfferingNotFound() { + Long offeringId = 1L; + when(backupOfferingDao.findById(offeringId)).thenReturn(null); + + InvalidParameterValueException exception = Assert.assertThrows(InvalidParameterValueException.class, + () -> backupManager.getBackupOfferingDomains(offeringId)); + assertEquals("Unable to find backup offering for id: " + offeringId, exception.getMessage()); + } + + @Test + public void getBackupOfferingDomainsTestReturnsDomains() { + Long offeringId = 1L; + BackupOfferingVO offering = Mockito.mock(BackupOfferingVO.class); + when(backupOfferingDao.findById(offeringId)).thenReturn(offering); + when(backupOfferingDetailsDao.findDomainIds(offeringId)).thenReturn(List.of(10L, 20L)); + + List result = backupManager.getBackupOfferingDomains(offeringId); + + assertEquals(2, result.size()); + assertTrue(result.contains(10L)); + assertTrue(result.contains(20L)); + } + + @Test + public void testUpdateBackupOfferingThrowsWhenDomainIdInvalid() { + Long id = 1234L; + UpdateBackupOfferingCmd cmd = Mockito.spy(UpdateBackupOfferingCmd.class); + when(cmd.getId()).thenReturn(id); + when(cmd.getDomainIds()).thenReturn(List.of(99L)); + + when(domainDao.findById(99L)).thenReturn(null); + + InvalidParameterValueException exception = Assert.assertThrows(InvalidParameterValueException.class, + () -> backupManager.updateBackupOffering(cmd)); + assertEquals("Please specify a valid domain id", exception.getMessage()); + } + + @Test + public void testUpdateBackupOfferingPersistsDomainDetailsWhenProvided() { + Long id = 1234L; + Long domainId = 11L; + UpdateBackupOfferingCmd cmd = Mockito.spy(UpdateBackupOfferingCmd.class); + when(cmd.getId()).thenReturn(id); + when(cmd.getDomainIds()).thenReturn(List.of(domainId)); + + DomainVO domain = Mockito.mock(DomainVO.class); + when(domainDao.findById(domainId)).thenReturn(domain); + + when(domainHelper.filterChildSubDomains(List.of(domainId))).thenReturn(new ArrayList<>(List.of(domainId))); + when(backupOfferingDetailsDao.findDomainIds(id)).thenReturn(new ArrayList<>()); + + BackupOfferingVO offering = Mockito.mock(BackupOfferingVO.class); + BackupOfferingVO offeringUpdate = Mockito.mock(BackupOfferingVO.class); + when(backupOfferingDao.findById(id)).thenReturn(offering); + when(backupOfferingDao.createForUpdate(id)).thenReturn(offeringUpdate); + when(backupOfferingDao.update(id, offeringUpdate)).thenReturn(true); + + BackupOffering updated = backupManager.updateBackupOffering(cmd); + + verify(backupOfferingDetailsDao, times(1)).updateBackupOfferingDomainIdsDetail(id, List.of(domainId)); + } + + @Test + public void testListBackupOfferingsWithDomainFilteringIncludesGlobalOfferings() { + Long requestedDomainId = 3L; + + ListBackupOfferingsCmd cmd = + Mockito.mock(ListBackupOfferingsCmd.class); + when(cmd.getOfferingId()).thenReturn(null); + when(cmd.getDomainId()).thenReturn(requestedDomainId); + when(cmd.getStartIndex()).thenReturn(0L); + when(cmd.getPageSizeVal()).thenReturn(20L); + + BackupOfferingVO globalOffering = createMockOffering(1L, "Global Offering"); + BackupOfferingVO domainOffering = createMockOffering(2L, "Domain Offering"); + + List allOfferings = List.of(globalOffering, domainOffering); + + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + BackupOfferingVO entityMock = Mockito.mock(BackupOfferingVO.class); + when(backupOfferingDao.createSearchBuilder()).thenReturn(sb); + when(sb.entity()).thenReturn(entityMock); + when(sb.and(Mockito.anyString(), Mockito.any(), Mockito.any(SearchCriteria.Op.class))).thenReturn(sb); + when(sb.create()).thenReturn(sc); + when(backupOfferingDao.searchAndCount(Mockito.any(), Mockito.any())) + .thenReturn(new Pair<>(allOfferings, allOfferings.size())); + + when(backupOfferingDetailsDao.findDomainIds(1L)).thenReturn(Collections.emptyList()); + when(backupOfferingDetailsDao.findDomainIds(2L)).thenReturn(List.of(2L)); + + Account account = Mockito.mock(Account.class); + when(account.getType()).thenReturn(Account.Type.NORMAL); + + try (MockedStatic mockedCallContext = Mockito.mockStatic(CallContext.class)) { + CallContext contextMock = Mockito.mock(CallContext.class); + mockedCallContext.when(CallContext::current).thenReturn(contextMock); + when(contextMock.getCallingAccount()).thenReturn(account); + + Pair, Integer> result = backupManager.listBackupOfferings(cmd); + + assertEquals(1, result.first().size()); + assertEquals("Global Offering", result.first().get(0).getName()); + } + } + + @Test + public void testListBackupOfferingsWithDomainFilteringIncludesDirectDomainMapping() { + Long requestedDomainId = 3L; + + ListBackupOfferingsCmd cmd = + Mockito.mock(ListBackupOfferingsCmd.class); + when(cmd.getOfferingId()).thenReturn(null); + when(cmd.getDomainId()).thenReturn(requestedDomainId); + when(cmd.getStartIndex()).thenReturn(0L); + when(cmd.getPageSizeVal()).thenReturn(20L); + + BackupOfferingVO directDomainOffering = createMockOffering(1L, "Direct Domain Offering"); + BackupOfferingVO otherDomainOffering = createMockOffering(2L, "Other Domain Offering"); + + List allOfferings = List.of(directDomainOffering, otherDomainOffering); + + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + BackupOfferingVO entityMock = Mockito.mock(BackupOfferingVO.class); + when(backupOfferingDao.createSearchBuilder()).thenReturn(sb); + when(sb.entity()).thenReturn(entityMock); + when(sb.and(Mockito.anyString(), Mockito.any(), Mockito.any(SearchCriteria.Op.class))).thenReturn(sb); + when(sb.create()).thenReturn(sc); + when(backupOfferingDao.searchAndCount(Mockito.any(), Mockito.any())) + .thenReturn(new Pair<>(allOfferings, allOfferings.size())); + + when(backupOfferingDetailsDao.findDomainIds(1L)).thenReturn(List.of(requestedDomainId)); + when(backupOfferingDetailsDao.findDomainIds(2L)).thenReturn(List.of(5L)); + + Account account = Mockito.mock(Account.class); + when(account.getType()).thenReturn(Account.Type.NORMAL); + + try (MockedStatic mockedCallContext = Mockito.mockStatic(CallContext.class)) { + CallContext contextMock = Mockito.mock(CallContext.class); + mockedCallContext.when(CallContext::current).thenReturn(contextMock); + when(contextMock.getCallingAccount()).thenReturn(account); + + Pair, Integer> result = backupManager.listBackupOfferings(cmd); + + assertEquals(1, result.first().size()); + assertEquals("Direct Domain Offering", result.first().get(0).getName()); + } + } + + @Test + public void testListBackupOfferingsWithDomainFilteringIncludesParentDomainOfferings() { + Long parentDomainId = 1L; + Long childDomainId = 3L; + + ListBackupOfferingsCmd cmd = + Mockito.mock(ListBackupOfferingsCmd.class); + when(cmd.getOfferingId()).thenReturn(null); + when(cmd.getDomainId()).thenReturn(childDomainId); + when(cmd.getStartIndex()).thenReturn(0L); + when(cmd.getPageSizeVal()).thenReturn(20L); + + BackupOfferingVO parentDomainOffering = createMockOffering(1L, "Parent Domain Offering"); + BackupOfferingVO siblingDomainOffering = createMockOffering(2L, "Sibling Domain Offering"); + + List allOfferings = List.of(parentDomainOffering, siblingDomainOffering); + + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + BackupOfferingVO entityMock = Mockito.mock(BackupOfferingVO.class); + when(backupOfferingDao.createSearchBuilder()).thenReturn(sb); + when(sb.entity()).thenReturn(entityMock); + when(sb.and(Mockito.anyString(), Mockito.any(), Mockito.any(SearchCriteria.Op.class))).thenReturn(sb); + when(sb.create()).thenReturn(sc); + when(backupOfferingDao.searchAndCount(Mockito.any(), Mockito.any())) + .thenReturn(new Pair<>(allOfferings, allOfferings.size())); + + when(backupOfferingDetailsDao.findDomainIds(1L)).thenReturn(List.of(parentDomainId)); + when(backupOfferingDetailsDao.findDomainIds(2L)).thenReturn(List.of(4L)); + + when(domainDao.isChildDomain(parentDomainId, childDomainId)).thenReturn(true); + when(domainDao.isChildDomain(4L, childDomainId)).thenReturn(false); + + Account account = Mockito.mock(Account.class); + when(account.getType()).thenReturn(Account.Type.NORMAL); + + try (MockedStatic mockedCallContext = Mockito.mockStatic(CallContext.class)) { + CallContext contextMock = Mockito.mock(CallContext.class); + mockedCallContext.when(CallContext::current).thenReturn(contextMock); + when(contextMock.getCallingAccount()).thenReturn(account); + + Pair, Integer> result = backupManager.listBackupOfferings(cmd); + + assertEquals(1, result.first().size()); + assertEquals("Parent Domain Offering", result.first().get(0).getName()); + } + } + + @Test + public void testListBackupOfferingsWithDomainFilteringExcludesSiblingDomainOfferings() { + Long requestedDomainId = 3L; + Long siblingDomainId = 4L; + + ListBackupOfferingsCmd cmd = + Mockito.mock(ListBackupOfferingsCmd.class); + when(cmd.getOfferingId()).thenReturn(null); + when(cmd.getDomainId()).thenReturn(requestedDomainId); + when(cmd.getStartIndex()).thenReturn(0L); + when(cmd.getPageSizeVal()).thenReturn(20L); + + BackupOfferingVO siblingOffering = createMockOffering(1L, "Sibling Domain Offering"); + List allOfferings = List.of(siblingOffering); + + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + BackupOfferingVO entityMock = Mockito.mock(BackupOfferingVO.class); + when(backupOfferingDao.createSearchBuilder()).thenReturn(sb); + when(sb.entity()).thenReturn(entityMock); + when(sb.and(Mockito.anyString(), Mockito.any(), Mockito.any(SearchCriteria.Op.class))).thenReturn(sb); + when(sb.create()).thenReturn(sc); + when(backupOfferingDao.searchAndCount(Mockito.any(), Mockito.any())) + .thenReturn(new Pair<>(allOfferings, allOfferings.size())); + + when(backupOfferingDetailsDao.findDomainIds(1L)).thenReturn(List.of(siblingDomainId)); + when(domainDao.isChildDomain(siblingDomainId, requestedDomainId)).thenReturn(false); + + Account account = Mockito.mock(Account.class); + when(account.getType()).thenReturn(Account.Type.NORMAL); + + try (MockedStatic mockedCallContext = Mockito.mockStatic(CallContext.class)) { + CallContext contextMock = Mockito.mock(CallContext.class); + mockedCallContext.when(CallContext::current).thenReturn(contextMock); + when(contextMock.getCallingAccount()).thenReturn(account); + + Pair, Integer> result = backupManager.listBackupOfferings(cmd); + + assertEquals(0, result.first().size()); + } + } + + @Test + public void testListBackupOfferingsWithDomainFilteringMultipleDomainMappings() { + Long requestedDomainId = 5L; + Long parentDomainId1 = 1L; + Long parentDomainId2 = 2L; + Long unrelatedDomainId = 8L; + + ListBackupOfferingsCmd cmd = + Mockito.mock(ListBackupOfferingsCmd.class); + when(cmd.getOfferingId()).thenReturn(null); + when(cmd.getDomainId()).thenReturn(requestedDomainId); + when(cmd.getStartIndex()).thenReturn(0L); + when(cmd.getPageSizeVal()).thenReturn(20L); + + BackupOfferingVO multiDomainOffering = createMockOffering(1L, "Multi-Domain Offering"); + List allOfferings = List.of(multiDomainOffering); + + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + BackupOfferingVO entityMock = Mockito.mock(BackupOfferingVO.class); + when(backupOfferingDao.createSearchBuilder()).thenReturn(sb); + when(sb.entity()).thenReturn(entityMock); + when(sb.and(Mockito.anyString(), Mockito.any(), Mockito.any(SearchCriteria.Op.class))).thenReturn(sb); + when(sb.create()).thenReturn(sc); + when(backupOfferingDao.searchAndCount(Mockito.any(), Mockito.any())) + .thenReturn(new Pair<>(allOfferings, allOfferings.size())); + + when(backupOfferingDetailsDao.findDomainIds(1L)) + .thenReturn(List.of(parentDomainId1, unrelatedDomainId, parentDomainId2)); + + when(domainDao.isChildDomain(parentDomainId1, requestedDomainId)).thenReturn(false); + when(domainDao.isChildDomain(unrelatedDomainId, requestedDomainId)).thenReturn(false); + when(domainDao.isChildDomain(parentDomainId2, requestedDomainId)).thenReturn(true); + + Account account = Mockito.mock(Account.class); + when(account.getType()).thenReturn(Account.Type.NORMAL); + + try (MockedStatic mockedCallContext = Mockito.mockStatic(CallContext.class)) { + CallContext contextMock = Mockito.mock(CallContext.class); + mockedCallContext.when(CallContext::current).thenReturn(contextMock); + when(contextMock.getCallingAccount()).thenReturn(account); + + Pair, Integer> result = backupManager.listBackupOfferings(cmd); + + assertEquals(1, result.first().size()); + assertEquals("Multi-Domain Offering", result.first().get(0).getName()); + } + } + + @Test + public void testListBackupOfferingsNormalUserDefaultsToDomainFiltering() { + Long userDomainId = 7L; + + ListBackupOfferingsCmd cmd = + Mockito.mock(ListBackupOfferingsCmd.class); + when(cmd.getOfferingId()).thenReturn(null); + when(cmd.getDomainId()).thenReturn(null); // User didn't pass domain filter + when(cmd.getStartIndex()).thenReturn(0L); + when(cmd.getPageSizeVal()).thenReturn(20L); + + BackupOfferingVO globalOffering = createMockOffering(1L, "Global Offering"); + BackupOfferingVO userDomainOffering = createMockOffering(2L, "User Domain Offering"); + BackupOfferingVO otherDomainOffering = createMockOffering(3L, "Other Domain Offering"); + + List allOfferings = List.of(globalOffering, userDomainOffering, otherDomainOffering); + + SearchBuilder sb = Mockito.mock(SearchBuilder.class); + SearchCriteria sc = Mockito.mock(SearchCriteria.class); + BackupOfferingVO entityMock = Mockito.mock(BackupOfferingVO.class); + when(backupOfferingDao.createSearchBuilder()).thenReturn(sb); + when(sb.entity()).thenReturn(entityMock); + when(sb.and(Mockito.anyString(), Mockito.any(), Mockito.any(SearchCriteria.Op.class))).thenReturn(sb); + when(sb.create()).thenReturn(sc); + when(backupOfferingDao.searchAndCount(Mockito.any(), Mockito.any())) + .thenReturn(new Pair<>(allOfferings, allOfferings.size())); + + when(backupOfferingDetailsDao.findDomainIds(1L)).thenReturn(Collections.emptyList()); // Global + when(backupOfferingDetailsDao.findDomainIds(2L)).thenReturn(List.of(userDomainId)); // User's domain + when(backupOfferingDetailsDao.findDomainIds(3L)).thenReturn(List.of(99L)); // Other domain + + when(domainDao.isChildDomain(99L, userDomainId)).thenReturn(false); + + Account account = Mockito.mock(Account.class); + when(account.getType()).thenReturn(Account.Type.NORMAL); + when(account.getDomainId()).thenReturn(userDomainId); + + try (MockedStatic mockedCallContext = Mockito.mockStatic(CallContext.class)) { + CallContext contextMock = Mockito.mock(CallContext.class); + mockedCallContext.when(CallContext::current).thenReturn(contextMock); + when(contextMock.getCallingAccount()).thenReturn(account); + + Pair, Integer> result = backupManager.listBackupOfferings(cmd); + + assertEquals(2, result.first().size()); + assertTrue(result.first().stream().anyMatch(o -> o.getName().equals("Global Offering"))); + assertTrue(result.first().stream().anyMatch(o -> o.getName().equals("User Domain Offering"))); + } + } + + private BackupOfferingVO createMockOffering(Long id, String name) { + BackupOfferingVO offering = Mockito.mock(BackupOfferingVO.class); + when(offering.getId()).thenReturn(id); + when(offering.getName()).thenReturn(name); + return offering; + } + } diff --git a/server/src/test/java/org/apache/cloudstack/networkoffering/CreateNetworkOfferingTest.java b/server/src/test/java/org/apache/cloudstack/networkoffering/CreateNetworkOfferingTest.java index eab5d3eeffd8..69147d9197d5 100644 --- a/server/src/test/java/org/apache/cloudstack/networkoffering/CreateNetworkOfferingTest.java +++ b/server/src/test/java/org/apache/cloudstack/networkoffering/CreateNetworkOfferingTest.java @@ -27,6 +27,7 @@ import javax.inject.Inject; +import com.cloud.utils.DomainHelper; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.framework.config.dao.ConfigurationDao; @@ -98,6 +99,9 @@ public class CreateNetworkOfferingTest extends TestCase { @Mock LoadBalancerVMMapDao _loadBalancerVMMapDao; + @Mock + DomainHelper domainHelper; + @Mock AnnotationDao annotationDao; @Inject diff --git a/tools/marvin/setup.py b/tools/marvin/setup.py index 05ce9d41023c..6c9aa087bc7d 100644 --- a/tools/marvin/setup.py +++ b/tools/marvin/setup.py @@ -27,7 +27,7 @@ raise RuntimeError("python setuptools is required to build Marvin") -VERSION = "4.23.0.0-SNAPSHOT" +VERSION = "4.23.0.0" setup(name="Marvin", version=VERSION, diff --git a/ui/src/config/section/offering.js b/ui/src/config/section/offering.js index 4a32619b8c2f..bc95772d6f7a 100644 --- a/ui/src/config/section/offering.js +++ b/ui/src/config/section/offering.js @@ -340,9 +340,9 @@ export default { icon: 'cloud-upload-outlined', docHelp: 'adminguide/virtual_machines.html#backup-offerings', permission: ['listBackupOfferings'], - searchFilters: ['zoneid'], - columns: ['name', 'description', 'zonename'], - details: ['name', 'id', 'description', 'externalid', 'zone', 'allowuserdrivenbackups', 'created'], + searchFilters: ['zoneid', 'domainid'], + columns: ['name', 'description', 'domain', 'zonename'], + details: ['name', 'id', 'description', 'externalid', 'domain', 'zone', 'allowuserdrivenbackups', 'created'], related: [{ name: 'vm', title: 'label.instances', diff --git a/ui/src/views/offering/ImportBackupOffering.vue b/ui/src/views/offering/ImportBackupOffering.vue index b8ac7d8e8e65..f680eacd4a7d 100644 --- a/ui/src/views/offering/ImportBackupOffering.vue +++ b/ui/src/views/offering/ImportBackupOffering.vue @@ -85,6 +85,33 @@ + + + + + + + + + + + {{ opt.path || opt.name || opt.description }} + + + +
{{ this.$t('label.cancel') }} {{ this.$t('label.ok') }} @@ -96,6 +123,7 @@