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

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


The following commit(s) were added to refs/heads/main by this push:
     new 8b2f1f19c27 Support dedicating backup offerings to domains (#12194)
8b2f1f19c27 is described below

commit 8b2f1f19c27bebf908f9cda53ec1fadd005e2520
Author: Pearl Dsilva <[email protected]>
AuthorDate: Mon Jan 19 03:51:47 2026 -0500

    Support dedicating backup offerings to domains (#12194)
    
    * Add support for dedicating backup offerings to domains
    
    * Add tests and UI support and update response params
    
    * add license header
    
    * exclude backupofferingdetailsvo from sonar
    
    * fix pre-commit checks - missing / extra EOF line
    
    * add test
    
    * EOF
    
    * filter backup offerings by domain id
    
    * add unit tests
    
    * add more unit tests and remove response file from code coverage check
    
    * update checks
    
    * address review comments: extract common code, fix tests
    
    * added bean definition
    
    * address comments
    
    * add unit tests to increase coverage
    
    * pre-commit check failure fix
    
    * address merge issue
    
    * allow updating backup offering when only domain id is modified
---
 .../main/java/com/cloud/user/AccountService.java   |   3 +
 .../org/apache/cloudstack/acl/SecurityChecker.java |   4 +
 .../apache/cloudstack/api/BaseBackupListCmd.java   |   2 +-
 .../admin/backup/ImportBackupOfferingCmd.java      |  22 ++
 .../admin/backup/UpdateBackupOfferingCmd.java      |  28 +-
 .../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  | 114 +++++++
 .../api/response/BackupOfferingResponse.java       |  19 ++
 .../apache/cloudstack/backup/BackupManager.java    |   2 +
 .../offering/DomainAndZoneIdResolverTest.java      | 149 +++++++++
 .../cloudstack/backup/BackupOfferingDetailsVO.java |  86 +++++
 .../apache/cloudstack/backup/BackupOfferingVO.java |   7 +
 .../backup/dao/BackupOfferingDaoImpl.java          |  23 +-
 .../backup/dao/BackupOfferingDetailsDao.java       |  32 ++
 .../backup/dao/BackupOfferingDetailsDaoImpl.java   | 101 ++++++
 ...n-daos-between-management-and-usage-context.xml |   3 +-
 .../resources/META-INF/db/schema-42210to42300.sql  |  10 +
 .../dao/BackupOfferingDetailsDaoImplTest.java      | 251 ++++++++++++++
 plugins/hypervisors/ovm3/sonar-project.properties  |   2 +-
 .../contrail/management/MockAccountManager.java    |   6 +
 pom.xml                                            |   2 +
 .../src/main/java/com/cloud/acl/DomainChecker.java |  33 ++
 .../configuration/ConfigurationManagerImpl.java    |  47 +--
 .../java/com/cloud/network/vpc/VpcManagerImpl.java |  31 +-
 .../java/com/cloud/user/AccountManagerImpl.java    |  16 +
 .../main/java/com/cloud/utils/DomainHelper.java    |  63 ++++
 .../cloudstack/backup/BackupManagerImpl.java       | 117 ++++++-
 .../core/spring-server-core-misc-context.xml       |   2 +
 .../test/java/com/cloud/acl/DomainCheckerTest.java |  45 +++
 .../ConfigurationManagerImplTest.java              |   3 +
 .../java/com/cloud/vm/UserVmManagerImplTest.java   |   3 +-
 .../cloudstack/backup/BackupManagerTest.java       | 366 ++++++++++++++++++++-
 .../networkoffering/CreateNetworkOfferingTest.java |   4 +
 tools/marvin/setup.py                              |   2 +-
 ui/src/config/section/offering.js                  |   6 +-
 ui/src/views/offering/ImportBackupOffering.vue     |  69 +++-
 39 files changed, 1610 insertions(+), 316 deletions(-)

diff --git a/api/src/main/java/com/cloud/user/AccountService.java 
b/api/src/main/java/com/cloud/user/AccountService.java
index 09fe5ffc059..8f29a2fbc42 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..fa17df7c6ed 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,6 @@ 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;
+
+    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 0aa8366bcd5..2a64a1fb6fd 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.api.response.ListResponse;
 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<BackupOffering> 
offerings, final Integer count) {
         final ListResponse<BackupOfferingResponse> 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 2e73698e7aa..5e702585a2c 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 a645b1e0c8d..2f0dd6acd0e 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.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;
 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 class UpdateBackupOfferingCmd extends BaseCmd {
     @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 class UpdateBackupOfferingCmd extends BaseCmd {
         }
     }
 
+    public List<Long> 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<List<Long>> 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 9af10262b2d..e3fac81a793 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 2f07f85f983..917d7ff42d8 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 4027662574a..3a6d6639a5b 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 b8a8077b30b..300584428ea 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..b302c4a9bee
--- /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<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);
+                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/api/response/BackupOfferingResponse.java
 
b/api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java
index b3a7d036219..c4f3ee31dad 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 class BackupOfferingResponse extends BaseResponse {
     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 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/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 00000000000..e679bbf2d1f
--- /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<List<Long>> defaultsProvider = id -> 
Arrays.asList(100L, 200L);
+
+        List<Long> 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<Long> 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<List<Long>> defaultsProvider = id -> 
Collections.singletonList(300L);
+
+        List<Long> 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<Long> 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 00000000000..6bdf7602a9d
--- /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 d30385af575..ebeb7d4a2d5 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 class BackupOfferingVO implements BackupOffering {
     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 a41e4e70d33..708faeef464 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 @@ package org.apache.cloudstack.backup.dao;
 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.GenericDaoBase;
 import com.cloud.utils.db.SearchBuilder;
 import com.cloud.utils.db.SearchCriteria;
 
+import java.util.List;
+
 public class BackupOfferingDaoImpl extends GenericDaoBase<BackupOfferingVO, 
Long> implements BackupOfferingDao {
 
     @Inject
     DataCenterDao dataCenterDao;
+    @Inject
+    BackupOfferingDetailsDao backupOfferingDetailsDao;
+    @Inject
+    DomainDao domainDao;
 
     private SearchBuilder<BackupOfferingVO> backupPoliciesSearch;
 
@@ -51,8 +59,9 @@ public class BackupOfferingDaoImpl extends 
GenericDaoBase<BackupOfferingVO, Long
 
     @Override
     public BackupOfferingResponse newBackupOfferingResponse(BackupOffering 
offering, Boolean crossZoneInstanceCreation) {
-        DataCenterVO zone = dataCenterDao.findById(offering.getZoneId());
 
+        DataCenterVO zone = dataCenterDao.findById(offering.getZoneId());
+        List<Long> domainIds = 
backupOfferingDetailsDao.findDomainIds(offering.getId());
         BackupOfferingResponse response = new BackupOfferingResponse();
         response.setId(offering.getUuid());
         response.setName(offering.getName());
@@ -64,6 +73,18 @@ public class BackupOfferingDaoImpl extends 
GenericDaoBase<BackupOfferingVO, Long
             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 00000000000..390fcba1e0e
--- /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<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);
+    void updateBackupOfferingDomainIdsDetail(long backupOfferingId, List<Long> 
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 00000000000..f052c93f981
--- /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<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);
+    }
+
+    @DB
+    @Override
+    public void updateBackupOfferingDomainIdsDetail(long backupOfferingId, 
List<Long> filteredDomainIds) {
+    SearchBuilder<BackupOfferingDetailsVO> sb = createSearchBuilder();
+        List<BackupOfferingDetailsVO> detailsVO = new ArrayList<>();
+        sb.and("offeringId", sb.entity().getResourceId(), 
SearchCriteria.Op.EQ);
+        sb.and("detailName", sb.entity().getName(), SearchCriteria.Op.EQ);
+        sb.done();
+        SearchCriteria<BackupOfferingDetailsVO> 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 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-42210to42300.sql 
b/engine/schema/src/main/resources/META-INF/db/schema-42210to42300.sql
index 07f394b19c9..d330ecd0c0d 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
@@ -19,6 +19,16 @@
 -- 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;
+
 -- Update value to random for the config 'vm.allocation.algorithm' or 
'volume.allocation.algorithm' if configured as userconcentratedpod_random
 -- Update value to firstfit for the config 'vm.allocation.algorithm' or 
'volume.allocation.algorithm' if configured as userconcentratedpod_firstfit
 UPDATE `cloud`.`configuration` SET value='random' WHERE name IN 
('vm.allocation.algorithm', 'volume.allocation.algorithm') AND 
value='userconcentratedpod_random';
diff --git 
a/engine/schema/src/test/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDaoImplTest.java
 
b/engine/schema/src/test/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDaoImplTest.java
new file mode 100644
index 00000000000..fc8f2d0fcf7
--- /dev/null
+++ 
b/engine/schema/src/test/java/org/apache/cloudstack/backup/dao/BackupOfferingDetailsDaoImplTest.java
@@ -0,0 +1,251 @@
+// 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.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.cloudstack.api.ApiConstants;
+import org.apache.cloudstack.backup.BackupOfferingDetailsVO;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.mockito.InjectMocks;
+import org.mockito.Mockito;
+import org.mockito.Spy;
+import org.mockito.junit.MockitoJUnitRunner;
+
+import com.cloud.utils.db.SearchCriteria;
+
+@RunWith(MockitoJUnitRunner.class)
+public class BackupOfferingDetailsDaoImplTest {
+
+    @Spy
+    @InjectMocks
+    private BackupOfferingDetailsDaoImpl backupOfferingDetailsDao;
+
+    private static final long RESOURCE_ID = 1L;
+    private static final long OFFERING_ID = 100L;
+    private static final String TEST_KEY = "testKey";
+    private static final String TEST_VALUE = "testValue";
+
+    @Test
+    public void testAddDetail() {
+        BackupOfferingDetailsVO detailVO = new 
BackupOfferingDetailsVO(RESOURCE_ID, TEST_KEY, TEST_VALUE, true);
+
+        Assert.assertEquals("Resource ID should match", RESOURCE_ID, 
detailVO.getResourceId());
+        Assert.assertEquals("Detail name/key should match", TEST_KEY, 
detailVO.getName());
+        Assert.assertEquals("Detail value should match", TEST_VALUE, 
detailVO.getValue());
+        Assert.assertTrue("Display flag should be true", detailVO.isDisplay());
+
+        BackupOfferingDetailsVO detailVOHidden = new 
BackupOfferingDetailsVO(RESOURCE_ID, "hiddenKey", "hiddenValue", false);
+        Assert.assertFalse("Display flag should be false", 
detailVOHidden.isDisplay());
+    }
+
+    @Test
+    public void testFindDomainIdsWithMultipleDomains() {
+        List<BackupOfferingDetailsVO> mockDetails = Arrays.asList(
+                createDetailVO(RESOURCE_ID, ApiConstants.DOMAIN_ID, "1", 
false),
+                createDetailVO(RESOURCE_ID, ApiConstants.DOMAIN_ID, "2", 
false),
+                createDetailVO(RESOURCE_ID, ApiConstants.DOMAIN_ID, "3", false)
+        );
+
+        Mockito.doReturn(mockDetails).when(backupOfferingDetailsDao)
+                .findDetails(RESOURCE_ID, ApiConstants.DOMAIN_ID);
+
+        List<Long> domainIds = 
backupOfferingDetailsDao.findDomainIds(RESOURCE_ID);
+
+        Assert.assertNotNull(domainIds);
+        Assert.assertEquals(3, domainIds.size());
+        Assert.assertEquals(Arrays.asList(1L, 2L, 3L), domainIds);
+    }
+
+    @Test
+    public void testFindDomainIdsWithEmptyList() {
+        
Mockito.doReturn(Collections.emptyList()).when(backupOfferingDetailsDao)
+                .findDetails(RESOURCE_ID, ApiConstants.DOMAIN_ID);
+
+        List<Long> domainIds = 
backupOfferingDetailsDao.findDomainIds(RESOURCE_ID);
+
+        Assert.assertNotNull(domainIds);
+        Assert.assertTrue(domainIds.isEmpty());
+    }
+
+    @Test
+    public void testFindDomainIdsExcludesZeroOrNegativeValues() {
+        List<BackupOfferingDetailsVO> mockDetails = Arrays.asList(
+                createDetailVO(RESOURCE_ID, ApiConstants.DOMAIN_ID, "1", 
false),
+                createDetailVO(RESOURCE_ID, ApiConstants.DOMAIN_ID, "0", 
false),
+                createDetailVO(RESOURCE_ID, ApiConstants.DOMAIN_ID, "-1", 
false),
+                createDetailVO(RESOURCE_ID, ApiConstants.DOMAIN_ID, "2", false)
+        );
+
+        Mockito.doReturn(mockDetails).when(backupOfferingDetailsDao)
+                .findDetails(RESOURCE_ID, ApiConstants.DOMAIN_ID);
+
+        List<Long> domainIds = 
backupOfferingDetailsDao.findDomainIds(RESOURCE_ID);
+
+        Assert.assertNotNull(domainIds);
+        Assert.assertEquals(2, domainIds.size());
+        Assert.assertEquals(Arrays.asList(1L, 2L), domainIds);
+    }
+
+    @Test
+    public void testFindZoneIdsWithMultipleZones() {
+        List<BackupOfferingDetailsVO> mockDetails = Arrays.asList(
+                createDetailVO(RESOURCE_ID, ApiConstants.ZONE_ID, "10", false),
+                createDetailVO(RESOURCE_ID, ApiConstants.ZONE_ID, "20", false),
+                createDetailVO(RESOURCE_ID, ApiConstants.ZONE_ID, "30", false)
+        );
+
+        Mockito.doReturn(mockDetails).when(backupOfferingDetailsDao)
+                .findDetails(RESOURCE_ID, ApiConstants.ZONE_ID);
+
+        List<Long> zoneIds = backupOfferingDetailsDao.findZoneIds(RESOURCE_ID);
+
+        Assert.assertNotNull(zoneIds);
+        Assert.assertEquals(3, zoneIds.size());
+        Assert.assertEquals(Arrays.asList(10L, 20L, 30L), zoneIds);
+    }
+
+    @Test
+    public void testFindZoneIdsWithEmptyList() {
+        
Mockito.doReturn(Collections.emptyList()).when(backupOfferingDetailsDao)
+                .findDetails(RESOURCE_ID, ApiConstants.ZONE_ID);
+
+        List<Long> zoneIds = backupOfferingDetailsDao.findZoneIds(RESOURCE_ID);
+
+        Assert.assertNotNull(zoneIds);
+        Assert.assertTrue(zoneIds.isEmpty());
+    }
+
+    @Test
+    public void testFindZoneIdsExcludesZeroOrNegativeValues() {
+        List<BackupOfferingDetailsVO> mockDetails = Arrays.asList(
+                createDetailVO(RESOURCE_ID, ApiConstants.ZONE_ID, "10", false),
+                createDetailVO(RESOURCE_ID, ApiConstants.ZONE_ID, "0", false),
+                createDetailVO(RESOURCE_ID, ApiConstants.ZONE_ID, "-5", false),
+                createDetailVO(RESOURCE_ID, ApiConstants.ZONE_ID, "20", false)
+        );
+
+        Mockito.doReturn(mockDetails).when(backupOfferingDetailsDao)
+                .findDetails(RESOURCE_ID, ApiConstants.ZONE_ID);
+
+        List<Long> zoneIds = backupOfferingDetailsDao.findZoneIds(RESOURCE_ID);
+
+        Assert.assertNotNull(zoneIds);
+        Assert.assertEquals(2, zoneIds.size());
+        Assert.assertEquals(Arrays.asList(10L, 20L), zoneIds);
+    }
+
+    @Test
+    public void testGetDetailWhenDetailExists() {
+        BackupOfferingDetailsVO mockDetail = createDetailVO(OFFERING_ID, 
TEST_KEY, TEST_VALUE, true);
+
+        Mockito.doReturn(mockDetail).when(backupOfferingDetailsDao)
+                .findDetail(OFFERING_ID, TEST_KEY);
+
+        String detailValue = backupOfferingDetailsDao.getDetail(OFFERING_ID, 
TEST_KEY);
+
+        Assert.assertNotNull(detailValue);
+        Assert.assertEquals(TEST_VALUE, detailValue);
+    }
+
+    @Test
+    public void testGetDetailWhenDetailDoesNotExist() {
+        Mockito.doReturn(null).when(backupOfferingDetailsDao)
+                .findDetail(OFFERING_ID, TEST_KEY);
+
+        String detailValue = backupOfferingDetailsDao.getDetail(OFFERING_ID, 
TEST_KEY);
+
+        Assert.assertNull(detailValue);
+    }
+
+    @Test
+    public void testFindOfferingIdsByDomainIds() {
+        List<Long> domainIds = Arrays.asList(1L, 2L, 3L);
+        List<Long> expectedOfferingIds = Arrays.asList(100L, 101L, 102L);
+
+        Mockito.doReturn(expectedOfferingIds).when(backupOfferingDetailsDao)
+                .findResourceIdsByNameAndValueIn(Mockito.eq("domainid"), 
Mockito.any(Object[].class));
+
+        List<Long> offeringIds = 
backupOfferingDetailsDao.findOfferingIdsByDomainIds(domainIds);
+
+        Assert.assertNotNull(offeringIds);
+        Assert.assertEquals(expectedOfferingIds, offeringIds);
+        
Mockito.verify(backupOfferingDetailsDao).findResourceIdsByNameAndValueIn(
+                Mockito.eq("domainid"), Mockito.any(Object[].class));
+    }
+
+    @Test
+    public void testFindOfferingIdsByDomainIdsWithEmptyList() {
+        List<Long> domainIds = Collections.emptyList();
+        List<Long> expectedOfferingIds = Collections.emptyList();
+
+        Mockito.doReturn(expectedOfferingIds).when(backupOfferingDetailsDao)
+                .findResourceIdsByNameAndValueIn(Mockito.eq("domainid"), 
Mockito.any(Object[].class));
+
+        List<Long> offeringIds = 
backupOfferingDetailsDao.findOfferingIdsByDomainIds(domainIds);
+
+        Assert.assertNotNull(offeringIds);
+        Assert.assertTrue(offeringIds.isEmpty());
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testUpdateBackupOfferingDomainIdsDetail() {
+        List<Long> newDomainIds = Arrays.asList(1L, 2L, 3L);
+
+        
Mockito.doReturn(0).when(backupOfferingDetailsDao).remove(Mockito.any(SearchCriteria.class));
+        
Mockito.doReturn(null).when(backupOfferingDetailsDao).persist(Mockito.any(BackupOfferingDetailsVO.class));
+
+        
backupOfferingDetailsDao.updateBackupOfferingDomainIdsDetail(OFFERING_ID, 
newDomainIds);
+
+        Mockito.verify(backupOfferingDetailsDao, 
Mockito.times(3)).persist(Mockito.any(BackupOfferingDetailsVO.class));
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testUpdateBackupOfferingDomainIdsDetailWithEmptyList() {
+        List<Long> emptyDomainIds = Collections.emptyList();
+
+        
Mockito.doReturn(0).when(backupOfferingDetailsDao).remove(Mockito.any(SearchCriteria.class));
+
+        
backupOfferingDetailsDao.updateBackupOfferingDomainIdsDetail(OFFERING_ID, 
emptyDomainIds);
+
+        Mockito.verify(backupOfferingDetailsDao, 
Mockito.never()).persist(Mockito.any(BackupOfferingDetailsVO.class));
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testUpdateBackupOfferingDomainIdsDetailWithSingleDomain() {
+        List<Long> singleDomainId = Collections.singletonList(5L);
+
+        
Mockito.doReturn(0).when(backupOfferingDetailsDao).remove(Mockito.any(SearchCriteria.class));
+        
Mockito.doReturn(null).when(backupOfferingDetailsDao).persist(Mockito.any(BackupOfferingDetailsVO.class));
+
+        
backupOfferingDetailsDao.updateBackupOfferingDomainIdsDetail(OFFERING_ID, 
singleDomainId);
+
+        Mockito.verify(backupOfferingDetailsDao, 
Mockito.times(1)).persist(Mockito.any(BackupOfferingDetailsVO.class));
+    }
+
+    private BackupOfferingDetailsVO createDetailVO(long resourceId, String 
name, String value, boolean display) {
+        return new BackupOfferingDetailsVO(resourceId, name, value, display);
+    }
+}
diff --git a/plugins/hypervisors/ovm3/sonar-project.properties 
b/plugins/hypervisors/ovm3/sonar-project.properties
index 7355f1df4f7..7cc3a41f2a5 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 2ff68b4836f..54e76463bca 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/pom.xml b/pom.xml
index 33b232045ca..fa57b98ce7b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -53,6 +53,8 @@
         
<project.systemvm.template.version>4.22.0.0</project.systemvm.template.version>
         <sonar.organization>apache</sonar.organization>
         <sonar.host.url>https://sonarcloud.io</sonar.host.url>
+        
<sonar.exclusions>engine/schema/src/main/java/org/apache/cloudstack/backup/BackupOfferingDetailsVO.java</sonar.exclusions>
+        
<sonar.exclusions>api/src/main/java/org/apache/cloudstack/api/response/BackupOfferingResponse.java</sonar.exclusions>
 
         <!-- Build properties -->
         <cs.jdk.version>11</cs.jdk.version>
diff --git a/server/src/main/java/com/cloud/acl/DomainChecker.java 
b/server/src/main/java/com/cloud/acl/DomainChecker.java
index 24b6346d0af..0500960abb1 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,35 @@ public class DomainChecker extends AdapterBase implements 
SecurityChecker {
         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<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 e8dd45ce719..5331cb10ed6 100644
--- a/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java
+++ b/server/src/main/java/com/cloud/configuration/ConfigurationManagerImpl.java
@@ -49,6 +49,11 @@ import java.util.stream.Collectors;
 import javax.inject.Inject;
 import javax.naming.ConfigurationException;
 
+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;
 import org.apache.cloudstack.affinity.AffinityGroup;
@@ -150,7 +155,6 @@ import com.cloud.api.query.vo.NetworkOfferingJoinVO;
 import com.cloud.capacity.CapacityManager;
 import com.cloud.capacity.dao.CapacityDao;
 import com.cloud.configuration.Resource.ResourceType;
-import com.cloud.consoleproxy.ConsoleProxyManager;
 import com.cloud.dc.AccountVlanMapVO;
 import com.cloud.dc.ClusterDetailsDao;
 import com.cloud.dc.ClusterDetailsVO;
@@ -245,7 +249,6 @@ import com.cloud.network.dao.UserIpv6AddressDao;
 import com.cloud.network.element.NetrisProviderVO;
 import com.cloud.network.element.NsxProviderVO;
 import com.cloud.network.netris.NetrisService;
-import com.cloud.network.router.VirtualNetworkApplianceManager;
 import com.cloud.network.rules.LoadBalancerContainer.Scheme;
 import com.cloud.network.vpc.VpcManager;
 import com.cloud.offering.DiskOffering;
@@ -280,7 +283,6 @@ import com.cloud.storage.dao.DiskOfferingDao;
 import com.cloud.storage.dao.StoragePoolTagsDao;
 import com.cloud.storage.dao.VMTemplateZoneDao;
 import com.cloud.storage.dao.VolumeDao;
-import com.cloud.storage.secondary.SecondaryStorageVmManager;
 import com.cloud.test.IPRangeConfig;
 import com.cloud.user.Account;
 import com.cloud.user.AccountDetailVO;
@@ -314,7 +316,6 @@ import com.cloud.utils.exception.CloudRuntimeException;
 import com.cloud.utils.net.NetUtils;
 import com.cloud.vm.NicIpAlias;
 import com.cloud.vm.VirtualMachine;
-import com.cloud.vm.VirtualMachineManager;
 import com.cloud.vm.VmDetailConstants;
 import com.cloud.vm.dao.NicIpAliasDao;
 import com.cloud.vm.dao.NicIpAliasVO;
@@ -399,6 +400,8 @@ public class ConfigurationManagerImpl extends ManagerBase 
implements Configurati
     ClusterDao _clusterDao;
     @Inject
     AlertManager _alertMgr;
+    @Inject
+    DomainHelper domainHelper;
     List<SecurityChecker> _secChecker;
     List<ExternalProvisioner> externalProvisioners;
 
@@ -3519,7 +3522,7 @@ public class ConfigurationManagerImpl extends ManagerBase 
implements Configurati
                                                       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<Long> filteredDomainIds = filterChildSubDomains(domainIds);
+        List<Long> filteredDomainIds = 
domainHelper.filterChildSubDomains(domainIds);
 
         // Check if user exists in the system
         final User user = _userDao.findById(userId);
@@ -3908,7 +3911,7 @@ public class ConfigurationManagerImpl extends ManagerBase 
implements Configurati
         final Account account = _accountDao.findById(user.getAccountId());
 
         // Filter child domains when both parent and child domains are present
-        List<Long> filteredDomainIds = filterChildSubDomains(domainIds);
+        List<Long> filteredDomainIds = 
domainHelper.filterChildSubDomains(domainIds);
         Collections.sort(filteredDomainIds);
 
         // avoid domain update of service offering if any instance is 
associated to it
@@ -4118,7 +4121,7 @@ public class ConfigurationManagerImpl extends ManagerBase 
implements Configurati
         }
 
         // Filter child domains when both parent and child domains are present
-        List<Long> filteredDomainIds = filterChildSubDomains(domainIds);
+        List<Long> filteredDomainIds = 
domainHelper.filterChildSubDomains(domainIds);
 
         // Check if user exists in the system
         final User user = _userDao.findById(userId);
@@ -4394,7 +4397,7 @@ public class ConfigurationManagerImpl extends ManagerBase 
implements Configurati
         final Account account = _accountDao.findById(user.getAccountId());
 
         // Filter child domains when both parent and child domains are present
-        List<Long> filteredDomainIds = filterChildSubDomains(domainIds);
+        List<Long> filteredDomainIds = 
domainHelper.filterChildSubDomains(domainIds);
         Collections.sort(filteredDomainIds);
 
         List<Long> filteredZoneIds = new ArrayList<>();
@@ -7401,7 +7404,7 @@ public class ConfigurationManagerImpl extends ManagerBase 
implements Configurati
                     }
                     if (offering != null) {
                         // Filter child domains when both parent and child 
domains are present
-                        List<Long> filteredDomainIds = 
filterChildSubDomains(domainIds);
+                        List<Long> filteredDomainIds = 
domainHelper.filterChildSubDomains(domainIds);
                         List<NetworkOfferingDetailsVO> detailsVO = new 
ArrayList<>();
                         for (Long domainId : filteredDomainIds) {
                             detailsVO.add(new 
NetworkOfferingDetailsVO(offering.getId(), Detail.domainid, 
String.valueOf(domainId), false));
@@ -7867,7 +7870,7 @@ public class ConfigurationManagerImpl extends ManagerBase 
implements Configurati
         }
 
         // Filter child domains when both parent and child domains are present
-        List<Long> filteredDomainIds = filterChildSubDomains(domainIds);
+        List<Long> filteredDomainIds = 
domainHelper.filterChildSubDomains(domainIds);
         Collections.sort(filteredDomainIds);
 
         List<Long> filteredZoneIds = new ArrayList<>();
@@ -8434,30 +8437,6 @@ public class ConfigurationManagerImpl extends 
ManagerBase implements Configurati
         return false;
     }
 
-    private List<Long> filterChildSubDomains(final List<Long> domainIds) {
-        List<Long> 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 e4219c858da..60b93d409aa 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.NetworkACLServiceProvider;
 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 class VpcManagerImpl extends ManagerBase implements 
VpcManager, VpcProvis
         }
 
         // Filter child domains when both parent and child domains are present
-        List<Long> filteredDomainIds = filterChildSubDomains(domainIds);
+        List<Long> filteredDomainIds = 
domainHelper.filterChildSubDomains(domainIds);
 
         final Map<Network.Service, Set<Network.Provider>> svcProviderMap = new 
HashMap<Network.Service, Set<Network.Provider>>();
         final Set<Network.Provider> defaultProviders = new 
HashSet<Network.Provider>();
@@ -1118,7 +1121,7 @@ public class VpcManagerImpl extends ManagerBase 
implements VpcManager, VpcProvis
 
 
         // Filter child domains when both parent and child domains are present
-        List<Long> filteredDomainIds = filterChildSubDomains(domainIds);
+        List<Long> filteredDomainIds = 
domainHelper.filterChildSubDomains(domainIds);
         Collections.sort(filteredDomainIds);
 
         List<Long> filteredZoneIds = new ArrayList<>();
@@ -3658,30 +3661,6 @@ public class VpcManagerImpl extends ManagerBase 
implements VpcManager, VpcProvis
         return _ntwkMgr.areRoutersRunning(routerDao.listByVpcId(vpc.getId()));
     }
 
-    private List<Long> filterChildSubDomains(final List<Long> domainIds) {
-        List<Long> 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 dd60fbfb9cc..6f68a104867 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;
@@ -3574,6 +3575,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/com/cloud/utils/DomainHelper.java 
b/server/src/main/java/com/cloud/utils/DomainHelper.java
new file mode 100644
index 00000000000..480726d256b
--- /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<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;
+    }
+}
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..5743c729923 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 java.util.stream.Stream;
 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.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;
@@ -81,12 +83,12 @@ import org.apache.cloudstack.poll.BackgroundPollTask;
 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 class BackupManagerImpl extends ManagerBase 
implements BackupManager {
             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<Long> 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 class BackupManagerImpl extends ManagerBase 
implements BackupManager {
         if (savedOffering == null) {
             throw new CloudRuntimeException("Unable to create backup offering: 
" + cmd.getExternalId() + ", name: " + cmd.getName());
         }
+        if (CollectionUtils.isNotEmpty(filteredDomainIds)) {
+            List<BackupOfferingDetailsVO> 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<Long> 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<List<BackupOffering>, 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 class BackupManagerImpl extends ManagerBase 
implements BackupManager {
         SearchBuilder<BackupOfferingVO> 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 class BackupManagerImpl extends ManagerBase 
implements BackupManager {
         if (keyword != null) {
             sc.setParameters("name", "%" + keyword + "%");
         }
+
         if (Account.Type.NORMAL == caller.getType()) {
             sc.setParameters("user_backups_allowed", true);
         }
+
         Pair<List<BackupOfferingVO>, Integer> result = 
backupOfferingDao.searchAndCount(sc, searchFilter);
+
+        if (domainId != null) {
+            List<BackupOfferingVO> filteredOfferings = new ArrayList<>();
+            for (BackupOfferingVO offering : result.first()) {
+                List<Long> 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<Long> 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 class BackupManagerImpl extends ManagerBase 
implements BackupManager {
             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 class BackupManagerImpl extends ManagerBase 
implements BackupManager {
             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 @@ 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");
@@ -1065,7 +1141,7 @@ public class BackupManagerImpl extends ManagerBase 
implements BackupManager {
         }
 
         // This is done to handle historic backups if any with Veeam / 
Networker plugins
-        List<Backup.VolumeInfo> backupVolumes = 
CollectionUtils.isNullOrEmpty(backup.getBackedUpVolumes()) ?
+        List<Backup.VolumeInfo> backupVolumes = 
CollectionUtils.isEmpty(backup.getBackedUpVolumes()) ?
                 vm.getBackupVolumeList() : backup.getBackedUpVolumes();
         List<VolumeVO> vmVolumes = volumeDao.findByInstance(vm.getId());
         if (vmVolumes.size() != backupVolumes.size()) {
@@ -2112,11 +2188,15 @@ public class BackupManagerImpl extends ManagerBase 
implements BackupManager {
         String name = updateBackupOfferingCmd.getName();
         String description = updateBackupOfferingCmd.getDescription();
         Boolean allowUserDrivenBackups = 
updateBackupOfferingCmd.getAllowUserDrivenBackups();
+        List<Long> 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 class BackupManagerImpl extends ManagerBase 
implements BackupManager {
             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<Long> 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 || fields.isEmpty()) {
+            List<Long> 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<Long> 
filteredDomainIds, List<Long> existingDomainIds) {
+        if (existingDomainIds == null) {
+            existingDomainIds = new ArrayList<>();
+        }
+
+        if(!filteredDomainIds.equals(existingDomainIds)) {
+            backupOfferingDetailsDao.updateBackupOfferingDomainIdsDetail(id, 
filteredDomainIds);
+        }
+    }
+
     Map<String, String> getDetailsFromBackupDetails(Long backupId) {
         Map<String, String> details = 
backupDetailsDao.listDetailsKeyPairs(backupId, true);
         if (details == null) {
@@ -2270,7 +2377,7 @@ public class BackupManagerImpl extends ManagerBase 
implements BackupManager {
             return;
         }
         List<Backup> 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 c633a3b0abd..f4fd57d59fc 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 @@
 
     <bean id="DPDKHelper" class="com.cloud.hypervisor.kvm.dpdk.DpdkHelperImpl" 
/>
 
+    <bean id="domainHelper" class="com.cloud.utils.DomainHelper" />
+
 </beans>
diff --git a/server/src/test/java/com/cloud/acl/DomainCheckerTest.java 
b/server/src/test/java/com/cloud/acl/DomainCheckerTest.java
index a5ec41306d8..8c7817c2b84 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 @@ package com.cloud.acl;
 
 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.AccountVO;
 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 class DomainCheckerTest {
         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 60263cea9b8..8295202fcc5 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.storage.dao.VolumeDao;
 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 fe4ea0838f1..1ec141a8be1 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;
@@ -3107,7 +3108,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..a9c083228e2 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.ResourceLimitService;
 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.admin.backup.UpdateBackupOfferingCmd;
 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 class BackupManagerTest {
         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 class BackupManagerTest {
 
         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 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());
     }
@@ -1149,6 +1159,8 @@ public class BackupManagerTest {
         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 class BackupManagerTest {
         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<UsageEventUtils> ignored2 = 
Mockito.mockStatic(UsageEventUtils.class)) {
@@ -2156,4 +2170,352 @@ public class BackupManagerTest {
         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<Long> 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<BackupOfferingVO> allOfferings = List.of(globalOffering, 
domainOffering);
+
+        SearchBuilder<BackupOfferingVO> sb = Mockito.mock(SearchBuilder.class);
+        SearchCriteria<BackupOfferingVO> 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<CallContext> mockedCallContext = 
Mockito.mockStatic(CallContext.class)) {
+            CallContext contextMock = Mockito.mock(CallContext.class);
+            
mockedCallContext.when(CallContext::current).thenReturn(contextMock);
+            when(contextMock.getCallingAccount()).thenReturn(account);
+
+            Pair<List<BackupOffering>, 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<BackupOfferingVO> allOfferings = List.of(directDomainOffering, 
otherDomainOffering);
+
+        SearchBuilder<BackupOfferingVO> sb = Mockito.mock(SearchBuilder.class);
+        SearchCriteria<BackupOfferingVO> 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<CallContext> mockedCallContext = 
Mockito.mockStatic(CallContext.class)) {
+            CallContext contextMock = Mockito.mock(CallContext.class);
+            
mockedCallContext.when(CallContext::current).thenReturn(contextMock);
+            when(contextMock.getCallingAccount()).thenReturn(account);
+
+            Pair<List<BackupOffering>, 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<BackupOfferingVO> allOfferings = List.of(parentDomainOffering, 
siblingDomainOffering);
+
+        SearchBuilder<BackupOfferingVO> sb = Mockito.mock(SearchBuilder.class);
+        SearchCriteria<BackupOfferingVO> 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<CallContext> mockedCallContext = 
Mockito.mockStatic(CallContext.class)) {
+            CallContext contextMock = Mockito.mock(CallContext.class);
+            
mockedCallContext.when(CallContext::current).thenReturn(contextMock);
+            when(contextMock.getCallingAccount()).thenReturn(account);
+
+            Pair<List<BackupOffering>, 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<BackupOfferingVO> allOfferings = List.of(siblingOffering);
+
+        SearchBuilder<BackupOfferingVO> sb = Mockito.mock(SearchBuilder.class);
+        SearchCriteria<BackupOfferingVO> 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<CallContext> mockedCallContext = 
Mockito.mockStatic(CallContext.class)) {
+            CallContext contextMock = Mockito.mock(CallContext.class);
+            
mockedCallContext.when(CallContext::current).thenReturn(contextMock);
+            when(contextMock.getCallingAccount()).thenReturn(account);
+
+            Pair<List<BackupOffering>, 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<BackupOfferingVO> allOfferings = List.of(multiDomainOffering);
+
+        SearchBuilder<BackupOfferingVO> sb = Mockito.mock(SearchBuilder.class);
+        SearchCriteria<BackupOfferingVO> 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<CallContext> mockedCallContext = 
Mockito.mockStatic(CallContext.class)) {
+            CallContext contextMock = Mockito.mock(CallContext.class);
+            
mockedCallContext.when(CallContext::current).thenReturn(contextMock);
+            when(contextMock.getCallingAccount()).thenReturn(account);
+
+            Pair<List<BackupOffering>, 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<BackupOfferingVO> allOfferings = List.of(globalOffering, 
userDomainOffering, otherDomainOffering);
+
+        SearchBuilder<BackupOfferingVO> sb = Mockito.mock(SearchBuilder.class);
+        SearchCriteria<BackupOfferingVO> 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<CallContext> mockedCallContext = 
Mockito.mockStatic(CallContext.class)) {
+            CallContext contextMock = Mockito.mock(CallContext.class);
+            
mockedCallContext.when(CallContext::current).thenReturn(contextMock);
+            when(contextMock.getCallingAccount()).thenReturn(account);
+
+            Pair<List<BackupOffering>, 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 2fa9a2d7a44..f175c567422 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 java.util.Set;
 
 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 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/ui/src/config/section/offering.js 
b/ui/src/config/section/offering.js
index 4a32619b8c2..bc95772d6f7 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 b8ac7d8e8e6..f680eacd4a7 100644
--- a/ui/src/views/offering/ImportBackupOffering.vue
+++ b/ui/src/views/offering/ImportBackupOffering.vue
@@ -85,6 +85,33 @@
         </template>
         <a-switch v-model:checked="form.allowuserdrivenbackups"/>
       </a-form-item>
+      <a-form-item name="ispublic" ref="ispublic" 
:label="$t('label.ispublic')" v-if="isAdmin()">
+          <a-switch v-model:checked="form.ispublic" />
+        </a-form-item>
+        <a-form-item name="domainid" ref="domainid" v-if="!form.ispublic">
+          <template #label>
+            <tooltip-label :title="$t('label.domainid')" 
:tooltip="apiParams.domainid.description"/>
+          </template>
+          <a-select
+            mode="multiple"
+            :getPopupContainer="(trigger) => trigger.parentNode"
+            v-model:value="form.domainid"
+            showSearch
+            optionFilterProp="label"
+            :filterOption="(input, option) => {
+              return option.label.toLowerCase().indexOf(input.toLowerCase()) 
>= 0
+            }"
+            :loading="domains.loading"
+            :placeholder="apiParams.domainid.description">
+            <a-select-option v-for="(opt, optIndex) in domains.opts" 
:key="optIndex" :label="opt.path || opt.name || opt.description">
+              <span>
+                <resource-icon v-if="opt && opt.icon" 
:image="opt.icon.base64image" size="1x" style="margin-right: 5px"/>
+                <block-outlined v-else style="margin-right: 5px" />
+                {{ opt.path || opt.name || opt.description }}
+              </span>
+            </a-select-option>
+          </a-select>
+        </a-form-item>
       <div :span="24" class="action-button">
         <a-button :loading="loading" @click="closeAction">{{ 
this.$t('label.cancel') }}</a-button>
         <a-button :loading="loading" ref="submit" type="primary" 
@click="handleSubmit">{{ this.$t('label.ok') }}</a-button>
@@ -96,6 +123,7 @@
 <script>
 import { ref, reactive, toRaw } from 'vue'
 import { getAPI, postAPI } from '@/api'
+import { isAdmin } from '@/role'
 import ResourceIcon from '@/components/view/ResourceIcon'
 import TooltipLabel from '@/components/widgets/TooltipLabel'
 
@@ -108,6 +136,10 @@ export default {
   data () {
     return {
       loading: false,
+      domains: {
+        loading: false,
+        opts: []
+      },
       zones: {
         loading: false,
         opts: []
@@ -129,17 +161,23 @@ export default {
     initForm () {
       this.formRef = ref()
       this.form = reactive({
-        allowuserdrivenbackups: true
+        allowuserdrivenbackups: true,
+        ispublic: true
       })
       this.rules = reactive({
         name: [{ required: true, message: 
this.$t('message.error.required.input') }],
         description: [{ required: true, message: 
this.$t('message.error.required.input') }],
         zoneid: [{ required: true, message: this.$t('message.error.select') }],
-        externalid: [{ required: true, message: 
this.$t('message.error.select') }]
+        externalid: [{ required: true, message: 
this.$t('message.error.select') }],
+        domainid: [{ type: 'array', message: this.$t('message.error.select') }]
       })
     },
+    isAdmin () {
+      return isAdmin()
+    },
     fetchData () {
       this.fetchZone()
+      this.fetchDomainData()
     },
     fetchZone () {
       this.zones.loading = true
@@ -151,6 +189,19 @@ export default {
         this.zones.loading = false
       })
     },
+    fetchDomainData () {
+      const params = {}
+      params.listAll = true
+      params.details = 'min'
+      this.domains.loading = true
+      getAPI('listDomains', params).then(json => {
+        this.domains.opts = json.listdomainsresponse.domain || []
+      }).catch(error => {
+        this.$notifyError(error)
+      }).finally(() => {
+        this.domains.loading = false
+      })
+    },
     fetchExternal (zoneId) {
       if (!zoneId) {
         this.externals.opts = []
@@ -179,6 +230,20 @@ export default {
             params[key] = input
           }
         }
+        if (values.ispublic !== true) {
+          var domainIndexes = values.domainid
+          var domainId = null
+          if (domainIndexes && domainIndexes.length > 0) {
+            var domainIds = []
+            for (var i = 0; i < domainIndexes.length; i++) {
+              domainIds = 
domainIds.concat(this.domains.opts[domainIndexes[i]].id)
+            }
+            domainId = domainIds.join(',')
+          }
+          if (domainId) {
+            params.domainid = domainId
+          }
+        }
         params.allowuserdrivenbackups = values.allowuserdrivenbackups
         this.loading = true
         const title = this.$t('label.import.offering')


Reply via email to