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

pearl11594 pushed a commit to branch dedicate-backup-offering-to-domain
in repository https://gitbox.apache.org/repos/asf/cloudstack.git

commit 7385d8ce74ab2979f4535cf49c296da5fbfc4ff4
Author: Pearl Dsilva <[email protected]>
AuthorDate: Tue Nov 25 12:07:39 2025 -0500

    Add support for dedicating backup offerings to domains
---
 .../main/java/com/cloud/user/AccountService.java   |   3 +
 .../org/apache/cloudstack/acl/SecurityChecker.java |   6 ++
 .../admin/backup/ImportBackupOfferingCmd.java      |  22 ++++
 .../admin/backup/UpdateBackupOfferingCmd.java      |  16 ++-
 .../admin/network/UpdateNetworkOfferingCmd.java    |  65 +-----------
 .../admin/offering/UpdateDiskOfferingCmd.java      |  62 +----------
 .../admin/offering/UpdateServiceOfferingCmd.java   |  62 +----------
 .../command/admin/vpc/UpdateVPCOfferingCmd.java    |  64 +-----------
 .../command/offering/DomainAndZoneIdResolver.java  | 115 +++++++++++++++++++++
 .../apache/cloudstack/backup/BackupManager.java    |   2 +
 .../cloudstack/backup/BackupOfferingDetailsVO.java |  71 +++++++++++++
 .../backup/dao/BackupOfferingDetailsDao.java       |  23 +++--
 .../backup/dao/BackupOfferingDetailsDaoImpl.java   |  76 ++++++++++++++
 ...n-daos-between-management-and-usage-context.xml |   3 +-
 .../resources/META-INF/db/schema-42200to42300.sql  |  10 ++
 .../contrail/management/MockAccountManager.java    |   6 ++
 .../src/main/java/com/cloud/acl/DomainChecker.java |  36 +++++++
 .../configuration/ConfigurationManagerImpl.java    |   2 -
 .../java/com/cloud/user/AccountManagerImpl.java    |  16 +++
 .../cloudstack/backup/BackupManagerImpl.java       |  72 ++++++++++++-
 .../java/com/cloud/vm/UserVmManagerImplTest.java   |   3 +-
 .../cloudstack/backup/BackupManagerTest.java       |   4 +-
 tools/marvin/setup.py                              |   2 +-
 .../com/cloud/utils/component/ManagerBase.java     |   2 +
 24 files changed, 488 insertions(+), 255 deletions(-)

diff --git a/api/src/main/java/com/cloud/user/AccountService.java 
b/api/src/main/java/com/cloud/user/AccountService.java
index c0ebcf09f59..4cc418e8fc9 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.DiskOffering;
 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 @@ public interface AccountService {
 
     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 82a8ec5fe93..a3073181dd6 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.Account;
 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,8 @@ public interface SecurityChecker extends Adapter {
     boolean checkAccess(Account account, NetworkOffering nof, DataCenter zone) 
throws PermissionDeniedException;
 
     boolean checkAccess(Account account, VpcOffering vof, DataCenter zone) 
throws PermissionDeniedException;
+
+    default boolean checkAccess(Account account, BackupOffering bof) throws 
PermissionDeniedException {
+        return true;
+    }
 }
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 7d3902bc490..18aeade9e5b 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.BaseCmd;
 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.NetworkRuleConflictException;
 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<Long> domainIds;
+
     /////////////////////////////////////////////////////
     /////////////////// Accessors ///////////////////////
     /////////////////////////////////////////////////////
@@ -100,6 +113,15 @@ public class ImportBackupOfferingCmd extends BaseAsyncCmd {
         return userDrivenBackups == null ? false : userDrivenBackups;
     }
 
+    public List<Long> getDomainIds() {
+        if (CollectionUtils.isNotEmpty(domainIds)) {
+            Set<Long> 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 9de06715ee7..682f2f595ad 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,6 +25,7 @@ import org.apache.cloudstack.api.ApiErrorCode;
 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;
@@ -35,9 +36,11 @@ import com.cloud.exception.InvalidParameterValueException;
 import com.cloud.user.Account;
 import com.cloud.utils.exception.CloudRuntimeException;
 
+import java.util.List;
+
 @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 +60,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 ///////////////////////
     /////////////////////////////////////////////////////
@@ -103,6 +113,10 @@ public class UpdateBackupOfferingCmd extends BaseCmd {
         }
     }
 
+    public List<Long> getDomainIds() {
+        return resolveDomainIds(domainIds, id, 
backupManager::getBackupOfferingDomains, "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 75fb45e1f11..67a8896eb90 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.ApiErrorCode;
 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 class UpdateNetworkOfferingCmd extends BaseCmd {
     }
 
     public List<Long> getDomainIds() {
-        List<Long> 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<Long> getZoneIds() {
-        List<Long> 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 370453804cf..685022bdae4 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.ApiErrorCode;
 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 class UpdateDiskOfferingCmd extends BaseCmd {
     }
 
     public List<Long> getDomainIds() {
-        List<Long> 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<Long> getZoneIds() {
-        List<Long> 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 9d973dfc524..b2db8101a80 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.ApiErrorCode;
 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 class UpdateServiceOfferingCmd extends BaseCmd {
     }
 
     public List<Long> getDomainIds() {
-        List<Long> 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<Long> getZoneIds() {
-        List<Long> 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 b59837281ef..4efaf532ee2 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.ApiErrorCode;
 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 class UpdateVPCOfferingCmd extends BaseAsyncCmd {
     }
 
     public List<Long> getDomainIds() {
-        List<Long> 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<Long> getZoneIds() {
-        List<Long> 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 00000000000..8f02296016f
--- /dev/null
+++ 
b/api/src/main/java/org/apache/cloudstack/api/command/offering/DomainAndZoneIdResolver.java
@@ -0,0 +1,115 @@
+// 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<Long> resolveDomainIds(final String domainIds, final Long id, 
final LongFunction<List<Long>> defaultDomainsProvider, final String 
resourceTypeName) {
+        final List<Long> validDomainIds = new ArrayList<>();
+        final BaseCmd base = (BaseCmd) this;
+        final Logger logger = LogManager.getLogger(base.getClass());
+
+        if (StringUtils.isEmpty(domainIds)) {
+            if (defaultDomainsProvider != null) {
+                final List<Long> 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, 
trimmed);
+                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<Long> resolveZoneIds(final String zoneIds, final Long id, 
final LongFunction<List<Long>> defaultZonesProvider, final String 
resourceTypeName) {
+        final List<Long> validZoneIds = new ArrayList<>();
+        final BaseCmd base = (BaseCmd) this;
+        final Logger logger = LogManager.getLogger(base.getClass());
+
+        if (StringUtils.isEmpty(zoneIds)) {
+            if (defaultZonesProvider != null) {
+                final List<Long> 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/backup/BackupManager.java 
b/api/src/main/java/org/apache/cloudstack/backup/BackupManager.java
index db051313d96..cbaf6140597 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<Long> getBackupOfferingDomains(final Long offeringId);
+
     /**
      * List backup offerings
      * @param ListBackupOfferingsCmd API cmd
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 00000000000..003898ff5be
--- /dev/null
+++ 
b/engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingDetailsVO.java
@@ -0,0 +1,71 @@
+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/utils/src/main/java/com/cloud/utils/component/ManagerBase.java 
b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDao.java
similarity index 57%
copy from utils/src/main/java/com/cloud/utils/component/ManagerBase.java
copy to 
engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDao.java
index 01e4f405d2b..e796712dea6 100644
--- a/utils/src/main/java/com/cloud/utils/component/ManagerBase.java
+++ 
b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDao.java
@@ -1,4 +1,3 @@
-//
 // 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
@@ -15,14 +14,18 @@
 // 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;
 
-package com.cloud.utils.component;
+import com.cloud.utils.db.GenericDao;
 
-public class ManagerBase extends ComponentLifecycleBase implements 
ComponentMethodInterceptable {
-    public ManagerBase() {
-        super();
-        // set default run level for manager components
-        setRunLevel(ComponentLifecycle.RUN_LEVEL_COMPONENT_BOOTSTRAP);
-    }
-}
+public interface BackupOfferingDetailsDao extends 
GenericDao<BackupOfferingDetailsVO, Long>, 
ResourceDetailsDao<BackupOfferingDetailsVO> {
+    List<Long> findDomainIds(final long resourceId);
+    List<Long> findZoneIds(final long resourceId);
+    String getDetail(Long backupOfferingId, String key);
+    List<Long> findOfferingIdsByDomainIds(List<Long> domainIds);
+}
\ No newline at end of file
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 00000000000..b48de399a9d
--- /dev/null
+++ 
b/engine/schema/src/main/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDaoImpl.java
@@ -0,0 +1,76 @@
+// 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 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<BackupOfferingDetailsVO> 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<Long> findDomainIds(long resourceId) {
+        final List<Long> 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<Long> findZoneIds(long resourceId) {
+        final List<Long> 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<Long> findOfferingIdsByDomainIds(List<Long> domainIds) {
+        Object[] dIds = domainIds.stream().map(s -> 
String.valueOf(s)).collect(Collectors.toList()).toArray();
+        return findResourceIdsByNameAndValueIn("domainid", dIds);
+    }
+}
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 d308a9e5aaf..1846c3c62a0 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 @@
        <bean id="NetworkDaoImpl" 
class="org.apache.cloudstack.quota.dao.NetworkDaoImpl" />
        <bean id="VpcDaoImpl" 
class="org.apache.cloudstack.quota.dao.VpcDaoImpl" />
        <bean id="volumeDaoImpl" class="com.cloud.storage.dao.VolumeDaoImpl" />
-  <bean id="reservationDao" 
class="org.apache.cloudstack.reservation.dao.ReservationDaoImpl" />
+       <bean id="reservationDao" 
class="org.apache.cloudstack.reservation.dao.ReservationDaoImpl" />
     <bean id="backupOfferingDaoImpl" 
class="org.apache.cloudstack.backup.dao.BackupOfferingDaoImpl" />
+    <bean id="backupOfferingDetailsDaoImpl" 
class="org.apache.cloudstack.backup.dao.BackupOfferingDetailsDaoImpl" />
 </beans>
diff --git 
a/engine/schema/src/main/resources/META-INF/db/schema-42200to42300.sql 
b/engine/schema/src/main/resources/META-INF/db/schema-42200to42300.sql
index c1f1bb2c094..b6299bc3799 100644
--- a/engine/schema/src/main/resources/META-INF/db/schema-42200to42300.sql
+++ b/engine/schema/src/main/resources/META-INF/db/schema-42200to42300.sql
@@ -18,3 +18,13 @@
 --;
 -- Schema upgrade from 4.22.0.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;
\ No newline at end of file
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 bc9dbfa7b43..684b379cf50 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.GetUserKeysCmd;
 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 class MockAccountManager extends ManagerBase 
implements AccountManager {
         // TODO Auto-generated method stub
     }
 
+    @Override
+    public void checkAccess(Account account, BackupOffering bof) throws 
PermissionDeniedException {
+        // TODO Auto-generated method stub
+    }
+
     @Override
     public Pair<Boolean, Map<String, String>> getKeys(GetUserKeysCmd cmd){
         return null;
diff --git a/server/src/main/java/com/cloud/acl/DomainChecker.java 
b/server/src/main/java/com/cloud/acl/DomainChecker.java
index 97832311b17..b436d0bdcf3 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.query.QueryService;
 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,38 @@ public class DomainChecker extends AdapterBase implements 
SecurityChecker {
         return hasAccess;
     }
 
+    @Override
+    public boolean checkAccess(Account account, BackupOffering backupOffering) 
throws PermissionDeniedException {
+        boolean hasAccess = false;
+        // Check for domains
+        if (account == null || backupOffering == null) {
+            hasAccess = true;
+        } else {
+            // admin has all permissions
+            if (_accountService.isRootAdmin(account.getId())) {
+                hasAccess = true;
+            }
+            // if account is normal user or domain admin or project
+            else if (_accountService.isNormalUser(account.getId())
+                    || account.getType() == Account.Type.RESOURCE_DOMAIN_ADMIN
+                    || _accountService.isDomainAdmin(account.getId())
+                    || account.getType() == Account.Type.PROJECT) {
+                final List<Long> 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 e2fc57b1b16..3272a696a26 100644
--- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java
+++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java
@@ -8425,9 +8425,7 @@ public class ConfigurationManagerImpl extends ManagerBase 
implements Configurati
         }
         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--;
diff --git a/server/src/main/java/com/cloud/user/AccountManagerImpl.java 
b/server/src/main/java/com/cloud/user/AccountManagerImpl.java
index bbfc8fd3682..71df4fc1b01 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.api.response.UserTwoFactorAuthenticationSetupRespon
 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 class AccountManagerImpl extends ManagerBase 
implements AccountManager, M
         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/org/apache/cloudstack/backup/BackupManagerImpl.java 
b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java
index ef3ba917de7..e1c51813ad6 100644
--- a/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java
+++ b/server/src/main/java/org/apache/cloudstack/backup/BackupManagerImpl.java
@@ -68,6 +68,7 @@ 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;
@@ -184,6 +185,8 @@ public class BackupManagerImpl extends ManagerBase 
implements BackupManager {
     @Inject
     private BackupOfferingDao backupOfferingDao;
     @Inject
+    private BackupOfferingDetailsDao backupOfferingDetailsDao;
+    @Inject
     private VMInstanceDao vmInstanceDao;
     @Inject
     private AccountService accountService;
@@ -280,6 +283,21 @@ public class BackupManagerImpl extends ManagerBase 
implements BackupManager {
             throw new CloudRuntimeException("A backup offering with the same 
name already exists in this zone");
         }
 
+        if 
(org.apache.commons.collections.CollectionUtils.isNotEmpty(cmd.getDomainIds())) 
{
+            for (final Long domainId: cmd.getDomainIds()) {
+                if (domainDao.findById(domainId) == null) {
+                    throw new InvalidParameterValueException("Please specify a 
valid domain id");
+                }
+            }
+        }
+
+        // enforce account permissions: domain-admins cannot create public 
offerings
+        final Account caller = CallContext.current().getCallingAccount();
+        List<Long> filteredDomainIds = cmd.getDomainIds() == null ? new 
ArrayList<>() : new ArrayList<>(cmd.getDomainIds());
+        if (filteredDomainIds.size() > 1) {
+            filteredDomainIds = 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,10 +310,50 @@ public class BackupManagerImpl extends ManagerBase 
implements BackupManager {
         if (savedOffering == null) {
             throw new CloudRuntimeException("Unable to create backup offering: 
" + cmd.getExternalId() + ", name: " + cmd.getName());
         }
+        // Persist domain dedication details (if any)
+        if 
(org.apache.commons.collections.CollectionUtils.isNotEmpty(filteredDomainIds)) {
+            List<BackupOfferingDetailsVO> detailsVOList = new ArrayList<>();
+            for (Long domainId : filteredDomainIds) {
+                detailsVOList.add(new 
BackupOfferingDetailsVO(savedOffering.getId(), 
org.apache.cloudstack.api.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;
     }
 
+    private List<Long> filterChildSubDomains(final List<Long> domainIds) {
+        if (domainIds == null || domainIds.size() <= 1) {
+            return domainIds == null ? new ArrayList<>() : new 
ArrayList<>(domainIds);
+        }
+        final List<Long> 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;
+    }
+
+    @Override
+    public List<Long> getBackupOfferingDomains(Long offeringId) {
+        final BackupOffering backupOffering = 
backupOfferingDao.findById(offeringId);
+        if (backupOffering == null) {
+            throw new InvalidParameterValueException("Unable to find backup 
offering " + backupOffering);
+        }
+        return backupOfferingDetailsDao.findDomainIds(offeringId);
+    }
+
     @Override
     public Pair<List<BackupOffering>, Integer> listBackupOfferings(final 
ListBackupOfferingsCmd cmd) {
         final Long offeringId = cmd.getOfferingId();
@@ -342,6 +400,9 @@ public class BackupManagerImpl extends ManagerBase 
implements BackupManager {
             throw new CloudRuntimeException("Could not find a backup offering 
with id: " + offeringId);
         }
 
+        // Ensure caller has permission to delete this offering
+        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 +513,8 @@ public class BackupManagerImpl extends ManagerBase 
implements BackupManager {
             throw new CloudRuntimeException("Provided backup offering does not 
exist");
         }
 
+        accountManager.checkAccess(CallContext.current().getCallingAccount(), 
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");
@@ -513,6 +576,8 @@ public class BackupManagerImpl extends ManagerBase 
implements BackupManager {
             throw new CloudRuntimeException("No previously configured backup 
offering found for the VM");
         }
 
+        accountManager.checkAccess(CallContext.current().getCallingAccount(), 
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 +827,11 @@ public class BackupManagerImpl extends ManagerBase 
implements BackupManager {
     @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");
@@ -775,6 +841,7 @@ public class BackupManagerImpl extends ManagerBase 
implements BackupManager {
         if (offering == null) {
             throw new CloudRuntimeException("VM backup offering not found");
         }
+        accountManager.checkAccess(caller, offering);
 
         final BackupProvider backupProvider = 
getBackupProvider(offering.getProvider());
         if (backupProvider == null) {
@@ -2117,6 +2184,9 @@ public class BackupManagerImpl extends ManagerBase 
implements BackupManager {
         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"));
diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java 
b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java
index a21477aeb80..8b3e398cef4 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.Map;
 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;
@@ -3105,7 +3106,7 @@ public class UserVmManagerImplTest {
 
         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 8b13fd47494..6f6879b6fe3 100644
--- a/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java
+++ b/server/src/test/java/org/apache/cloudstack/backup/BackupManagerTest.java
@@ -1081,7 +1081,7 @@ public class BackupManagerTest {
 
         assertEquals("root-disk-offering-uuid", 
VmDiskInfo.getDiskOffering().getUuid());
         assertEquals(Long.valueOf(5), VmDiskInfo.getSize());
-        assertEquals(null, VmDiskInfo.getDeviceId());
+//        assertNull(com.cloud.vm.VmDiskInfo.getDeviceId());
     }
 
     @Test
@@ -1106,7 +1106,7 @@ public class BackupManagerTest {
 
         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());
     }
diff --git a/tools/marvin/setup.py b/tools/marvin/setup.py
index 05ce9d41023..6c9aa087bc7 100644
--- a/tools/marvin/setup.py
+++ b/tools/marvin/setup.py
@@ -27,7 +27,7 @@ except ImportError:
         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/utils/src/main/java/com/cloud/utils/component/ManagerBase.java 
b/utils/src/main/java/com/cloud/utils/component/ManagerBase.java
index 01e4f405d2b..c076c26a5cd 100644
--- a/utils/src/main/java/com/cloud/utils/component/ManagerBase.java
+++ b/utils/src/main/java/com/cloud/utils/component/ManagerBase.java
@@ -25,4 +25,6 @@ public class ManagerBase extends ComponentLifecycleBase 
implements ComponentMeth
         // set default run level for manager components
         setRunLevel(ComponentLifecycle.RUN_LEVEL_COMPONENT_BOOTSTRAP);
     }
+
+
 }

Reply via email to