diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java
index 10a706e3a815..0cc3cb6edf4e 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/dns/CreateDnsZoneCmd.java
@@ -29,6 +29,7 @@
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.DnsServerResponse;
import org.apache.cloudstack.api.response.DnsZoneResponse;
+import org.apache.cloudstack.api.response.DomainResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.dns.DnsZone;
import org.apache.commons.lang3.StringUtils;
@@ -71,6 +72,16 @@ public class CreateDnsZoneCmd extends BaseAsyncCreateCmd {
"If false, creates the zone in the DNS provider and registers it in CloudStack. Default is false")
private Boolean existing = false;
+ @Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING,
+ description = "(Root admin only) The optional account that will own the DNS zone. Must be used with domainid",
+ authorized = {RoleType.Admin})
+ private String accountName;
+
+ @Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, entityType = DomainResponse.class,
+ description = "(Root admin only) The optional domain of the account that will own the DNS zone",
+ authorized = {RoleType.Admin})
+ private Long domainId;
+
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@@ -98,6 +109,14 @@ public String getDescription() {
return description;
}
+ public String getAccountName() {
+ return accountName;
+ }
+
+ public Long getDomainId() {
+ return domainId;
+ }
+
/////////////////////////////////////////////////////
/////////////// Implementation //////////////////////
/////////////////////////////////////////////////////
diff --git a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java
index ef55eaf66c19..a01bd190c444 100644
--- a/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java
+++ b/server/src/main/java/org/apache/cloudstack/dns/DnsProviderManagerImpl.java
@@ -619,8 +619,21 @@ public DnsZone allocateDnsZone(CreateDnsZoneCmd cmd) {
throw new InvalidParameterValueException(String.format("DNS server not found for the given ID: %s", cmd.getDnsServerId()));
}
Account caller = CallContext.current().getCallingAccount();
- boolean isOwner = (server.getAccountId() == caller.getId());
- if (!isOwner) {
+ boolean isServerOwner = (server.getAccountId() == caller.getId());
+ Account dnsZoneOwner = caller;
+ boolean onBehalf = StringUtils.isNotBlank(cmd.getAccountName());
+ if (onBehalf) {
+ if (!accountMgr.isRootAdmin(caller.getId())) {
+ throw new PermissionDeniedException("Only root admin can create a DNS zone on behalf of another account.");
+ }
+ if (!isServerOwner) {
+ throw new PermissionDeniedException("A DNS zone can be created on behalf of another account only on a DNS server owned by the caller.");
+ }
+ dnsZoneOwner = accountMgr.getActiveAccountByName(cmd.getAccountName(), cmd.getDomainId());
+ if (dnsZoneOwner == null || dnsZoneOwner.getState() != Account.State.ENABLED) {
+ throw new InvalidParameterValueException(String.format("Unable to find an enabled account %s in the specified domain.", cmd.getAccountName()));
+ }
+ } else if (!isServerOwner) {
if (!server.getPublicServer()) {
throw new PermissionDeniedException("You do not have permission to use this DNS server.");
}
@@ -632,7 +645,8 @@ public DnsZone allocateDnsZone(CreateDnsZoneCmd cmd) {
if (existing != null) {
throw new InvalidParameterValueException("DNS zone already exists on this server.");
}
- DnsZoneVO dnsZoneVO = new DnsZoneVO(dnsZoneName, type, server.getId(), caller.getId(), caller.getDomainId(), cmd.getDescription());
+ DnsZoneVO dnsZoneVO = new DnsZoneVO(dnsZoneName, type, server.getId(), dnsZoneOwner.getId(),
+ dnsZoneOwner.getDomainId(), cmd.getDescription());
return dnsZoneDao.persist(dnsZoneVO);
}
diff --git a/server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java
index f70d5915a653..2a4068cd09c4 100644
--- a/server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java
+++ b/server/src/test/java/org/apache/cloudstack/dns/DnsProviderManagerImplTest.java
@@ -268,6 +268,73 @@ public void testAllocateDnsZoneOwnerSuccess() {
assertNotNull(result);
}
+ private CreateDnsZoneCmd onBehalfCmd(String name) {
+ CreateDnsZoneCmd cmd = mock(CreateDnsZoneCmd.class);
+ when(cmd.getName()).thenReturn(name);
+ when(cmd.getDnsServerId()).thenReturn(SERVER_ID);
+ when(cmd.getAccountName()).thenReturn("tenant1");
+ return cmd;
+ }
+
+ @Test(expected = PermissionDeniedException.class)
+ public void testAllocateDnsZoneOnBehalfNonRootDenied() {
+ CreateDnsZoneCmd cmd = onBehalfCmd("customer.com");
+ when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO);
+ when(accountMgr.isRootAdmin(callerMock.getId())).thenReturn(false);
+ manager.allocateDnsZone(cmd);
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testAllocateDnsZoneOnBehalfAccountNotFound() {
+ CreateDnsZoneCmd cmd = onBehalfCmd("customer.com");
+ when(cmd.getDomainId()).thenReturn(DOMAIN_ID);
+ when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO);
+ when(accountMgr.getActiveAccountByName("tenant1", DOMAIN_ID)).thenReturn(null);
+ manager.allocateDnsZone(cmd);
+ }
+
+ @Test(expected = InvalidParameterValueException.class)
+ public void testAllocateDnsZoneOnBehalfAccountDisabled() {
+ CreateDnsZoneCmd cmd = onBehalfCmd("customer.com");
+ when(cmd.getDomainId()).thenReturn(DOMAIN_ID);
+ Account tenant = mock(Account.class);
+ when(tenant.getState()).thenReturn(Account.State.DISABLED);
+ when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO);
+ when(accountMgr.getActiveAccountByName("tenant1", DOMAIN_ID)).thenReturn(tenant);
+ manager.allocateDnsZone(cmd);
+ }
+
+ @Test(expected = PermissionDeniedException.class)
+ public void testAllocateDnsZoneOnBehalfServerNotOwnedByCallerDenied() {
+ CreateDnsZoneCmd cmd = onBehalfCmd("customer.com");
+ when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO);
+ Mockito.doReturn(ACCOUNT_ID + 1).when(serverVO).getAccountId(); // server owned by the target account, not the caller
+ manager.allocateDnsZone(cmd);
+ }
+
+ @Test
+ public void testAllocateDnsZoneOnBehalfSuccessNoSuffix() {
+ long tenantId = ACCOUNT_ID + 1;
+ CreateDnsZoneCmd cmd = onBehalfCmd("customer.com");
+ when(cmd.getDomainId()).thenReturn(DOMAIN_ID);
+ when(cmd.getType()).thenReturn(DnsZone.ZoneType.Public);
+ Account tenant = mock(Account.class);
+ when(tenant.getId()).thenReturn(tenantId);
+ when(tenant.getDomainId()).thenReturn(DOMAIN_ID);
+ when(tenant.getState()).thenReturn(Account.State.ENABLED);
+ when(accountMgr.getActiveAccountByName("tenant1", DOMAIN_ID)).thenReturn(tenant);
+ when(dnsServerDao.findById(SERVER_ID)).thenReturn(serverVO);
+ Mockito.doReturn(SERVER_ID).when(serverVO).getId();
+ Mockito.doReturn(ACCOUNT_ID).when(serverVO).getAccountId();
+ when(dnsZoneDao.findByNameServerAndType(anyString(), anyLong(), any())).thenReturn(null);
+ when(dnsZoneDao.persist(any(DnsZoneVO.class))).thenAnswer(i -> i.getArgument(0));
+
+ DnsZone result = manager.allocateDnsZone(cmd);
+
+ assertEquals("customer.com", result.getName());
+ assertEquals(tenantId, result.getAccountId());
+ }
+
@Test(expected = PermissionDeniedException.class)
public void testAllocateDnsZoneNonOwnerPrivateServer() {
CreateDnsZoneCmd cmd = mock(CreateDnsZoneCmd.class);
diff --git a/ui/src/views/network/dns/CreateDnsZone.vue b/ui/src/views/network/dns/CreateDnsZone.vue
index b9c07444c5ca..78325bed426f 100644
--- a/ui/src/views/network/dns/CreateDnsZone.vue
+++ b/ui/src/views/network/dns/CreateDnsZone.vue
@@ -68,6 +68,50 @@
:placeholder="apiParams.description.description"/>
+
+
+
+
+
+
+ {{ domain.path || domain.name }}
+
+
+
+
+
+
+
+
+
+
+ {{ account.name }}
+
+
+
+
@@ -94,6 +138,7 @@