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 }} + + + +