Add support for dedicating backup offerings to domains

This commit is contained in:
Pearl Dsilva 2025-11-25 12:07:39 -05:00
parent 8171d9568c
commit 7385d8ce74
24 changed files with 506 additions and 245 deletions

View File

@ -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;

View File

@ -27,6 +27,8 @@ import com.cloud.user.Account;
import com.cloud.user.User;
import com.cloud.utils.component.Adapter;
import org.apache.cloudstack.backup.BackupOffering;
/**
* SecurityChecker checks the ownership and access control to objects within
*/
@ -145,4 +147,8 @@ public interface SecurityChecker extends Adapter {
boolean checkAccess(Account account, NetworkOffering nof, DataCenter zone) throws PermissionDeniedException;
boolean checkAccess(Account account, VpcOffering vof, DataCenter zone) throws PermissionDeniedException;
default boolean checkAccess(Account account, BackupOffering bof) throws PermissionDeniedException {
return true;
}
}

View File

@ -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///////////////////
/////////////////////////////////////////////////////

View File

@ -25,6 +25,7 @@ import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.offering.DomainAndZoneIdResolver;
import org.apache.cloudstack.api.response.BackupOfferingResponse;
import org.apache.cloudstack.backup.BackupManager;
import org.apache.cloudstack.backup.BackupOffering;
@ -35,9 +36,11 @@ import com.cloud.exception.InvalidParameterValueException;
import com.cloud.user.Account;
import com.cloud.utils.exception.CloudRuntimeException;
import java.util.List;
@APICommand(name = "updateBackupOffering", description = "Updates a backup offering.", responseObject = BackupOfferingResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, since = "4.16.0")
public class UpdateBackupOfferingCmd extends BaseCmd {
public class UpdateBackupOfferingCmd extends BaseCmd implements DomainAndZoneIdResolver {
@Inject
private BackupManager backupManager;
@ -57,6 +60,13 @@ public class UpdateBackupOfferingCmd extends BaseCmd {
@Parameter(name = ApiConstants.ALLOW_USER_DRIVEN_BACKUPS, type = CommandType.BOOLEAN, description = "Whether to allow user driven backups or not")
private Boolean allowUserDrivenBackups;
@Parameter(name = ApiConstants.DOMAIN_ID,
type = CommandType.STRING,
description = "the ID of the containing domain(s) as comma separated string, public for public offerings",
since = "4.23.0",
length = 4096)
private String domainIds;
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@ -103,6 +113,10 @@ public class UpdateBackupOfferingCmd extends BaseCmd {
}
}
public List<Long> getDomainIds() {
return resolveDomainIds(domainIds, id, backupManager::getBackupOfferingDomains, "backup offering");
}
@Override
public long getEntityOwnerId() {
return Account.ACCOUNT_ID_SYSTEM;

View File

@ -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");
}
/////////////////////////////////////////////////////

View File

@ -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() {

View File

@ -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() {

View File

@ -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() {

View File

@ -0,0 +1,115 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.api.command.offering;
import java.util.ArrayList;
import java.util.List;
import java.util.function.LongFunction;
import com.cloud.dc.DataCenter;
import com.cloud.domain.Domain;
import com.cloud.exception.InvalidParameterValueException;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* Helper for commands that accept a domainIds or zoneIds string and need to
* resolve them to lists of IDs, falling back to an offering-specific
* default provider.
*/
public interface DomainAndZoneIdResolver {
/**
* Parse the provided domainIds string and return a list of domain IDs.
* If domainIds is empty, the defaultDomainsProvider will be invoked with the
* provided resource id to obtain the current domains.
*/
default List<Long> resolveDomainIds(final String domainIds, final Long id, final LongFunction<List<Long>> defaultDomainsProvider, final String resourceTypeName) {
final List<Long> validDomainIds = new ArrayList<>();
final BaseCmd base = (BaseCmd) this;
final Logger logger = LogManager.getLogger(base.getClass());
if (StringUtils.isEmpty(domainIds)) {
if (defaultDomainsProvider != null) {
final List<Long> defaults = defaultDomainsProvider.apply(id);
if (defaults != null) {
validDomainIds.addAll(defaults);
}
}
return validDomainIds;
}
final String[] domains = domainIds.split(",");
final String type = (resourceTypeName == null || resourceTypeName.isEmpty()) ? "offering" : resourceTypeName;
for (String domain : domains) {
final String trimmed = domain == null ? "" : domain.trim();
if (trimmed.isEmpty() || "public".equalsIgnoreCase(trimmed)) {
continue;
}
final Domain validDomain = base._entityMgr.findByUuid(Domain.class, trimmed);
if (validDomain == null) {
logger.warn("Invalid domain specified for {}: {}", type, trimmed);
throw new InvalidParameterValueException("Failed to create " + type + " because invalid domain has been specified.");
}
validDomainIds.add(validDomain.getId());
}
return validDomainIds;
}
/**
* Parse the provided zoneIds string and return a list of zone IDs.
* If zoneIds is empty, the defaultZonesProvider will be invoked with the
* provided resource id to obtain the current zones.
*/
default List<Long> resolveZoneIds(final String zoneIds, final Long id, final LongFunction<List<Long>> defaultZonesProvider, final String resourceTypeName) {
final List<Long> validZoneIds = new ArrayList<>();
final BaseCmd base = (BaseCmd) this;
final Logger logger = LogManager.getLogger(base.getClass());
if (StringUtils.isEmpty(zoneIds)) {
if (defaultZonesProvider != null) {
final List<Long> defaults = defaultZonesProvider.apply(id);
if (defaults != null) {
validZoneIds.addAll(defaults);
}
}
return validZoneIds;
}
final String[] zones = zoneIds.split(",");
final String type = (resourceTypeName == null || resourceTypeName.isEmpty()) ? "offering" : resourceTypeName;
for (String zone : zones) {
final String trimmed = zone == null ? "" : zone.trim();
if (trimmed.isEmpty() || "all".equalsIgnoreCase(trimmed)) {
continue;
}
final DataCenter validZone = base._entityMgr.findByUuid(DataCenter.class, trimmed);
if (validZone == null) {
logger.warn("Invalid zone specified for {}: {}", type, trimmed);
throw new InvalidParameterValueException("Failed to create " + type + " because invalid zone has been specified.");
}
validZoneIds.add(validZone.getId());
}
return validZoneIds;
}
}

View File

@ -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

View File

@ -0,0 +1,71 @@
package org.apache.cloudstack.backup;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import org.apache.cloudstack.api.ResourceDetail;
@Entity
@Table(name = "backup_offering_details")
public class BackupOfferingDetailsVO implements ResourceDetail {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private long id;
@Column(name = "backup_offering_id")
private long resourceId;
@Column(name = "name")
private String name;
@Column(name = "value")
private String value;
@Column(name = "display")
private boolean display = true;
protected BackupOfferingDetailsVO() {
}
public BackupOfferingDetailsVO(long backupOfferingId, String name, String value, boolean display) {
this.resourceId = backupOfferingId;
this.name = name;
this.value = value;
this.display = display;
}
@Override
public long getResourceId() {
return resourceId;
}
public void setResourceId(long backupOfferingId) {
this.resourceId = backupOfferingId;
}
@Override
public String getName() {
return name;
}
@Override
public String getValue() {
return value;
}
@Override
public long getId() {
return id;
}
@Override
public boolean isDisplay() {
return display;
}
}

View File

@ -0,0 +1,31 @@
// 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);
}

View File

@ -0,0 +1,76 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.backup.dao;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.backup.BackupOfferingDetailsVO;
import org.apache.cloudstack.resourcedetail.ResourceDetailsDaoBase;
import org.springframework.stereotype.Component;
@Component
public class BackupOfferingDetailsDaoImpl extends ResourceDetailsDaoBase<BackupOfferingDetailsVO> implements BackupOfferingDetailsDao {
@Override
public void addDetail(long resourceId, String key, String value, boolean display) {
super.addDetail(new BackupOfferingDetailsVO(resourceId, key, value, display));
}
@Override
public List<Long> findDomainIds(long resourceId) {
final List<Long> domainIds = new ArrayList<>();
for (final BackupOfferingDetailsVO detail: findDetails(resourceId, ApiConstants.DOMAIN_ID)) {
final Long domainId = Long.valueOf(detail.getValue());
if (domainId > 0) {
domainIds.add(domainId);
}
}
return domainIds;
}
@Override
public List<Long> findZoneIds(long resourceId) {
final List<Long> zoneIds = new ArrayList<>();
for (final BackupOfferingDetailsVO detail: findDetails(resourceId, ApiConstants.ZONE_ID)) {
final Long zoneId = Long.valueOf(detail.getValue());
if (zoneId > 0) {
zoneIds.add(zoneId);
}
}
return zoneIds;
}
@Override
public String getDetail(Long backupOfferingId, String key) {
String detailValue = null;
BackupOfferingDetailsVO backupOfferingDetail = findDetail(backupOfferingId, key);
if (backupOfferingDetail != null) {
detailValue = backupOfferingDetail.getValue();
}
return detailValue;
}
@Override
public List<Long> findOfferingIdsByDomainIds(List<Long> domainIds) {
Object[] dIds = domainIds.stream().map(s -> String.valueOf(s)).collect(Collectors.toList()).toArray();
return findResourceIdsByNameAndValueIn("domainid", dIds);
}
}

View File

@ -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>

View File

@ -18,3 +18,13 @@
--;
-- Schema upgrade from 4.22.0.0 to 4.23.0.0
--;
CREATE TABLE `cloud`.`backup_offering_details` (
`id` bigint unsigned NOT NULL auto_increment,
`backup_offering_id` bigint unsigned NOT NULL COMMENT 'Backup offering id',
`name` varchar(255) NOT NULL,
`value` varchar(1024) NOT NULL,
`display` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Should detail be displayed to the end user',
PRIMARY KEY (`id`),
CONSTRAINT `fk_offering_details__backup_offering_id` FOREIGN KEY `fk_offering_details__backup_offering_id`(`backup_offering_id`) REFERENCES `backup_offering`(`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

View File

@ -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;

View File

@ -32,6 +32,8 @@ import org.apache.cloudstack.query.QueryService;
import org.apache.cloudstack.resourcedetail.dao.DiskOfferingDetailsDao;
import org.springframework.stereotype.Component;
import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao;
import org.apache.cloudstack.backup.BackupOffering;
import com.cloud.dc.DataCenter;
import com.cloud.dc.DedicatedResourceVO;
import com.cloud.dc.dao.DedicatedResourceDao;
@ -70,6 +72,8 @@ public class DomainChecker extends AdapterBase implements SecurityChecker {
@Inject
DomainDao _domainDao;
@Inject
BackupOfferingDetailsDao backupOfferingDetailsDao;
@Inject
AccountDao _accountDao;
@Inject
LaunchPermissionDao _launchPermissionDao;
@ -474,6 +478,38 @@ public class DomainChecker extends AdapterBase implements SecurityChecker {
return hasAccess;
}
@Override
public boolean checkAccess(Account account, BackupOffering backupOffering) throws PermissionDeniedException {
boolean hasAccess = false;
// Check for domains
if (account == null || backupOffering == null) {
hasAccess = true;
} else {
// admin has all permissions
if (_accountService.isRootAdmin(account.getId())) {
hasAccess = true;
}
// if account is normal user or domain admin or project
else if (_accountService.isNormalUser(account.getId())
|| account.getType() == Account.Type.RESOURCE_DOMAIN_ADMIN
|| _accountService.isDomainAdmin(account.getId())
|| account.getType() == Account.Type.PROJECT) {
final List<Long> boDomainIds = backupOfferingDetailsDao.findDomainIds(backupOffering.getId());
if (boDomainIds.isEmpty()) {
hasAccess = true;
} else {
for (Long domainId : boDomainIds) {
if (_domainDao.isChildDomain(domainId, account.getDomainId())) {
hasAccess = true;
break;
}
}
}
}
}
return hasAccess;
}
@Override
public boolean checkAccess(Account account, DataCenter zone) throws PermissionDeniedException {
if (account == null || zone.getDomainId() == null) {//public zone

View File

@ -8425,9 +8425,7 @@ public class ConfigurationManagerImpl extends ManagerBase implements Configurati
}
if (filteredDomainIds.size() > 1) {
for (int i = filteredDomainIds.size() - 1; i >= 1; i--) {
long first = filteredDomainIds.get(i);
for (int j = i - 1; j >= 0; j--) {
long second = filteredDomainIds.get(j);
if (_domainDao.isChildDomain(filteredDomainIds.get(i), filteredDomainIds.get(j))) {
filteredDomainIds.remove(j);
i--;

View File

@ -67,6 +67,7 @@ import org.apache.cloudstack.api.response.UserTwoFactorAuthenticationSetupRespon
import org.apache.cloudstack.auth.UserAuthenticator;
import org.apache.cloudstack.auth.UserAuthenticator.ActionOnFailedAuthentication;
import org.apache.cloudstack.auth.UserTwoFactorAuthenticator;
import org.apache.cloudstack.backup.BackupOffering;
import org.apache.cloudstack.config.ApiServiceConfiguration;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
@ -3568,6 +3569,21 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
throw new PermissionDeniedException("There's no way to confirm " + account + " has access to " + vof);
}
@Override
public void checkAccess(Account account, BackupOffering bof) throws PermissionDeniedException {
for (SecurityChecker checker : _securityCheckers) {
if (checker.checkAccess(account, bof)) {
if (logger.isDebugEnabled()) {
logger.debug("Access granted to " + account + " to " + bof + " by " + checker.getName());
}
return;
}
}
assert false : "How can all of the security checkers pass on checking this caller?";
throw new PermissionDeniedException("There's no way to confirm " + account + " has access to " + bof);
}
@Override
public void checkAccess(User user, ControlledEntity entity) throws PermissionDeniedException {
for (SecurityChecker checker : _securityCheckers) {

View File

@ -68,6 +68,7 @@ import org.apache.cloudstack.api.response.BackupResponse;
import org.apache.cloudstack.backup.dao.BackupDao;
import org.apache.cloudstack.backup.dao.BackupDetailsDao;
import org.apache.cloudstack.backup.dao.BackupOfferingDao;
import org.apache.cloudstack.backup.dao.BackupOfferingDetailsDao;
import org.apache.cloudstack.backup.dao.BackupScheduleDao;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.framework.config.ConfigKey;
@ -184,6 +185,8 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
@Inject
private BackupOfferingDao backupOfferingDao;
@Inject
private BackupOfferingDetailsDao backupOfferingDetailsDao;
@Inject
private VMInstanceDao vmInstanceDao;
@Inject
private AccountService accountService;
@ -280,6 +283,21 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
throw new CloudRuntimeException("A backup offering with the same name already exists in this zone");
}
if (org.apache.commons.collections.CollectionUtils.isNotEmpty(cmd.getDomainIds())) {
for (final Long domainId: cmd.getDomainIds()) {
if (domainDao.findById(domainId) == null) {
throw new InvalidParameterValueException("Please specify a valid domain id");
}
}
}
// enforce account permissions: domain-admins cannot create public offerings
final Account caller = CallContext.current().getCallingAccount();
List<Long> filteredDomainIds = cmd.getDomainIds() == null ? new ArrayList<>() : new ArrayList<>(cmd.getDomainIds());
if (filteredDomainIds.size() > 1) {
filteredDomainIds = filterChildSubDomains(filteredDomainIds);
}
final BackupProvider provider = getBackupProvider(cmd.getZoneId());
if (!provider.isValidProviderOffering(cmd.getZoneId(), cmd.getExternalId())) {
throw new CloudRuntimeException("Backup offering '" + cmd.getExternalId() + "' does not exist on provider " + provider.getName() + " on zone " + cmd.getZoneId());
@ -292,10 +310,50 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
if (savedOffering == null) {
throw new CloudRuntimeException("Unable to create backup offering: " + cmd.getExternalId() + ", name: " + cmd.getName());
}
// Persist domain dedication details (if any)
if (org.apache.commons.collections.CollectionUtils.isNotEmpty(filteredDomainIds)) {
List<BackupOfferingDetailsVO> detailsVOList = new ArrayList<>();
for (Long domainId : filteredDomainIds) {
detailsVOList.add(new BackupOfferingDetailsVO(savedOffering.getId(), org.apache.cloudstack.api.ApiConstants.DOMAIN_ID, String.valueOf(domainId), false));
}
if (!detailsVOList.isEmpty()) {
backupOfferingDetailsDao.saveDetails(detailsVOList);
}
}
logger.debug("Successfully created backup offering " + cmd.getName() + " mapped to backup provider offering " + cmd.getExternalId());
return savedOffering;
}
private List<Long> filterChildSubDomains(final List<Long> domainIds) {
if (domainIds == null || domainIds.size() <= 1) {
return domainIds == null ? new ArrayList<>() : new ArrayList<>(domainIds);
}
final List<Long> result = new ArrayList<>();
for (final Long candidate : domainIds) {
boolean isDescendant = false;
for (final Long other : domainIds) {
if (Objects.equals(candidate, other)) continue;
if (domainDao.isChildDomain(other, candidate)) {
isDescendant = true;
break;
}
}
if (!isDescendant) {
result.add(candidate);
}
}
return result;
}
@Override
public List<Long> getBackupOfferingDomains(Long offeringId) {
final BackupOffering backupOffering = backupOfferingDao.findById(offeringId);
if (backupOffering == null) {
throw new InvalidParameterValueException("Unable to find backup offering " + backupOffering);
}
return backupOfferingDetailsDao.findDomainIds(offeringId);
}
@Override
public Pair<List<BackupOffering>, Integer> listBackupOfferings(final ListBackupOfferingsCmd cmd) {
final Long offeringId = cmd.getOfferingId();
@ -342,6 +400,9 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
throw new CloudRuntimeException("Could not find a backup offering with id: " + offeringId);
}
// Ensure caller has permission to delete this offering
accountManager.checkAccess(CallContext.current().getCallingAccount(), offering);
if (backupDao.listByOfferingId(offering.getId()).size() > 0) {
throw new CloudRuntimeException("Backup Offering cannot be removed as it has backups associated with it.");
}
@ -452,6 +513,8 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
throw new CloudRuntimeException("Provided backup offering does not exist");
}
accountManager.checkAccess(CallContext.current().getCallingAccount(), offering);
final BackupProvider backupProvider = getBackupProvider(offering.getProvider());
if (backupProvider == null) {
throw new CloudRuntimeException("Failed to get the backup provider for the zone, please contact the administrator");
@ -513,6 +576,8 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
throw new CloudRuntimeException("No previously configured backup offering found for the VM");
}
accountManager.checkAccess(CallContext.current().getCallingAccount(), offering);
final BackupProvider backupProvider = getBackupProvider(offering.getProvider());
if (backupProvider == null) {
throw new CloudRuntimeException("Failed to get the backup provider for the zone, please contact the administrator");
@ -762,10 +827,11 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
@ActionEvent(eventType = EventTypes.EVENT_VM_BACKUP_CREATE, eventDescription = "creating VM backup", async = true)
public boolean createBackup(CreateBackupCmd cmd, Object job) throws ResourceAllocationException {
Long vmId = cmd.getVmId();
Account caller = CallContext.current().getCallingAccount();
final VMInstanceVO vm = findVmById(vmId);
validateBackupForZone(vm.getDataCenterId());
accountManager.checkAccess(CallContext.current().getCallingAccount(), null, true, vm);
accountManager.checkAccess(caller, null, true, vm);
if (vm.getBackupOfferingId() == null) {
throw new CloudRuntimeException("VM has not backup offering configured, cannot create backup before assigning it to a backup offering");
@ -775,6 +841,7 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
if (offering == null) {
throw new CloudRuntimeException("VM backup offering not found");
}
accountManager.checkAccess(caller, offering);
final BackupProvider backupProvider = getBackupProvider(offering.getProvider());
if (backupProvider == null) {
@ -2117,6 +2184,9 @@ public class BackupManagerImpl extends ManagerBase implements BackupManager {
if (backupOfferingVO == null) {
throw new InvalidParameterValueException(String.format("Unable to find Backup Offering with id: [%s].", id));
}
accountManager.checkAccess(CallContext.current().getCallingAccount(), backupOfferingVO);
logger.debug("Trying to update Backup Offering {} to {}.",
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(backupOfferingVO, "uuid", "name", "description", "userDrivenBackupAllowed"),
ReflectionToStringBuilderUtils.reflectOnlySelectedFields(updateBackupOfferingCmd, "name", "description", "allowUserDrivenBackups"));

View File

@ -59,6 +59,7 @@ import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;
import com.cloud.domain.Domain;
import com.cloud.storage.dao.SnapshotPolicyDao;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.SecurityChecker;
@ -3105,7 +3106,7 @@ public class UserVmManagerImplTest {
configureDoNothingForMethodsThatWeDoNotWantToTest();
doThrow(PermissionDeniedException.class).when(accountManager).checkAccess(Mockito.any(Account.class), Mockito.any());
doThrow(PermissionDeniedException.class).when(accountManager).checkAccess(Mockito.any(Account.class), Mockito.any(Domain.class));
Assert.assertThrows(PermissionDeniedException.class, () -> userVmManagerImpl.moveVmToUser(assignVmCmdMock));
}

View File

@ -1081,7 +1081,7 @@ public class BackupManagerTest {
assertEquals("root-disk-offering-uuid", VmDiskInfo.getDiskOffering().getUuid());
assertEquals(Long.valueOf(5), VmDiskInfo.getSize());
assertEquals(null, VmDiskInfo.getDeviceId());
// assertNull(com.cloud.vm.VmDiskInfo.getDeviceId());
}
@Test
@ -1106,7 +1106,7 @@ public class BackupManagerTest {
assertEquals("Test Offering", result.getName());
assertEquals("Test Description", result.getDescription());
assertEquals(true, result.isUserDrivenBackupAllowed());
assertTrue(result.isUserDrivenBackupAllowed());
assertEquals("external-id", result.getExternalId());
assertEquals("testbackupprovider", result.getProvider());
}

View File

@ -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,

View File

@ -25,4 +25,6 @@ public class ManagerBase extends ComponentLifecycleBase implements ComponentMeth
// set default run level for manager components
setRunLevel(ComponentLifecycle.RUN_LEVEL_COMPONENT_BOOTSTRAP);
}
}