Merge branch '4.19'

This commit is contained in:
Daan Hoogland 2024-10-11 17:59:46 +02:00
commit dbfc7f23a7
32 changed files with 949 additions and 183 deletions

View File

@ -116,6 +116,8 @@ public interface AccountService {
void checkAccess(Account account, AccessType accessType, boolean sameOwner, String apiName, ControlledEntity... entities) throws PermissionDeniedException;
void validateAccountHasAccessToResource(Account account, AccessType accessType, Object resource);
Long finalyzeAccountId(String accountName, Long domainId, Long projectId, boolean enabledOnly);
/**

View File

@ -1958,25 +1958,26 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
* - Full clones (no backing file): Take snapshot of the VM prior disk creation
* Return this information
*/
protected void setVolumeMigrationOptions(VolumeInfo srcVolumeInfo, VolumeInfo destVolumeInfo,
VirtualMachineTO vmTO, Host srcHost, StoragePoolVO destStoragePool) {
if (!destStoragePool.isManaged()) {
String srcVolumeBackingFile = getVolumeBackingFile(srcVolumeInfo);
String srcPoolUuid = srcVolumeInfo.getDataStore().getUuid();
StoragePoolVO srcPool = _storagePoolDao.findById(srcVolumeInfo.getPoolId());
Storage.StoragePoolType srcPoolType = srcPool.getPoolType();
MigrationOptions migrationOptions;
if (StringUtils.isNotBlank(srcVolumeBackingFile)) {
migrationOptions = createLinkedCloneMigrationOptions(srcVolumeInfo, destVolumeInfo,
srcVolumeBackingFile, srcPoolUuid, srcPoolType);
} else {
migrationOptions = createFullCloneMigrationOptions(srcVolumeInfo, vmTO, srcHost, srcPoolUuid, srcPoolType);
}
migrationOptions.setTimeout(StorageManager.KvmStorageOnlineMigrationWait.value());
destVolumeInfo.setMigrationOptions(migrationOptions);
protected void setVolumeMigrationOptions(VolumeInfo srcVolumeInfo, VolumeInfo destVolumeInfo, VirtualMachineTO vmTO, Host srcHost, StoragePoolVO destStoragePool,
MigrationOptions.Type migrationType) {
if (destStoragePool.isManaged()) {
return;
}
String srcVolumeBackingFile = getVolumeBackingFile(srcVolumeInfo);
String srcPoolUuid = srcVolumeInfo.getDataStore().getUuid();
StoragePoolVO srcPool = _storagePoolDao.findById(srcVolumeInfo.getPoolId());
Storage.StoragePoolType srcPoolType = srcPool.getPoolType();
MigrationOptions migrationOptions;
if (MigrationOptions.Type.LinkedClone.equals(migrationType)) {
migrationOptions = createLinkedCloneMigrationOptions(srcVolumeInfo, destVolumeInfo, srcVolumeBackingFile, srcPoolUuid, srcPoolType);
} else {
migrationOptions = createFullCloneMigrationOptions(srcVolumeInfo, vmTO, srcHost, srcPoolUuid, srcPoolType);
}
migrationOptions.setTimeout(StorageManager.KvmStorageOnlineMigrationWait.value());
destVolumeInfo.setMigrationOptions(migrationOptions);
}
/**
@ -2007,6 +2008,7 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
Map<VolumeInfo, VolumeInfo> srcVolumeInfoToDestVolumeInfo = new HashMap<>();
boolean managedStorageDestination = false;
boolean migrateNonSharedInc = false;
for (Map.Entry<VolumeInfo, DataStore> entry : volumeDataStoreMap.entrySet()) {
VolumeInfo srcVolumeInfo = entry.getKey();
DataStore destDataStore = entry.getValue();
@ -2034,6 +2036,9 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
logger.debug(String.format("Skipping copy template from source storage pool [%s] to target storage pool [%s] before migration due to volume [%s] does not have a template.", sourceStoragePool.getId(), destStoragePool.getId(), srcVolumeInfo.getId()));
}
MigrationOptions.Type migrationType = decideMigrationTypeAndCopyTemplateIfNeeded(destHost, vmInstance, srcVolumeInfo, sourceStoragePool, destStoragePool, destDataStore);
migrateNonSharedInc = migrateNonSharedInc || MigrationOptions.Type.LinkedClone.equals(migrationType);
VolumeVO destVolume = duplicateVolumeOnAnotherStorage(srcVolume, destStoragePool);
VolumeInfo destVolumeInfo = _volumeDataFactory.getVolume(destVolume.getId(), destDataStore);
@ -2044,7 +2049,7 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
// move the volume from Ready to Migrating
destVolumeInfo.processEvent(Event.MigrationRequested);
setVolumeMigrationOptions(srcVolumeInfo, destVolumeInfo, vmTO, srcHost, destStoragePool);
setVolumeMigrationOptions(srcVolumeInfo, destVolumeInfo, vmTO, srcHost, destStoragePool, migrationType);
// create a volume on the destination storage
destDataStore.getDriver().createAsync(destDataStore, destVolumeInfo, null);
@ -2059,7 +2064,7 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
_volumeDao.update(destVolume.getId(), destVolume);
postVolumeCreationActions(srcVolumeInfo, destVolumeInfo, vmTO, srcHost);
postVolumeCreationActions(srcVolumeInfo, destVolumeInfo);
destVolumeInfo = _volumeDataFactory.getVolume(destVolume.getId(), destDataStore);
@ -2110,8 +2115,6 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
VMInstanceVO vm = _vmDao.findById(vmTO.getId());
boolean isWindows = _guestOsCategoryDao.findById(_guestOsDao.findById(vm.getGuestOSId()).getCategoryId()).getName().equalsIgnoreCase("Windows");
boolean migrateNonSharedInc = isSourceAndDestinationPoolTypeOfNfs(volumeDataStoreMap);
MigrateCommand migrateCommand = new MigrateCommand(vmTO.getName(), destHost.getPrivateIpAddress(), isWindows, vmTO, true);
migrateCommand.setWait(StorageManager.KvmStorageOnlineMigrationWait.value());
migrateCommand.setMigrateStorage(migrateStorage);
@ -2161,6 +2164,22 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
}
}
private MigrationOptions.Type decideMigrationTypeAndCopyTemplateIfNeeded(Host destHost, VMInstanceVO vmInstance, VolumeInfo srcVolumeInfo, StoragePoolVO sourceStoragePool, StoragePoolVO destStoragePool, DataStore destDataStore) {
VMTemplateVO vmTemplate = _vmTemplateDao.findById(vmInstance.getTemplateId());
String srcVolumeBackingFile = getVolumeBackingFile(srcVolumeInfo);
if (StringUtils.isNotBlank(srcVolumeBackingFile) && supportStoragePoolType(destStoragePool.getPoolType(), StoragePoolType.Filesystem) &&
srcVolumeInfo.getTemplateId() != null &&
Objects.nonNull(vmTemplate) &&
!Arrays.asList(KVM_VM_IMPORT_DEFAULT_TEMPLATE_NAME, VM_IMPORT_DEFAULT_TEMPLATE_NAME).contains(vmTemplate.getName())) {
LOGGER.debug(String.format("Copying template [%s] of volume [%s] from source storage pool [%s] to target storage pool [%s].", srcVolumeInfo.getTemplateId(), srcVolumeInfo.getId(), sourceStoragePool.getId(), destStoragePool.getId()));
copyTemplateToTargetFilesystemStorageIfNeeded(srcVolumeInfo, sourceStoragePool, destDataStore, destStoragePool, destHost);
return MigrationOptions.Type.LinkedClone;
}
LOGGER.debug(String.format("Skipping copy template from source storage pool [%s] to target storage pool [%s] before migration due to volume [%s] does not have a " +
"template or we are doing full clone migration.", sourceStoragePool.getId(), destStoragePool.getId(), srcVolumeInfo.getId()));
return MigrationOptions.Type.FullClone;
}
protected String formatMigrationElementsAsJsonToDisplayOnLog(String objectName, Object object, Object from, Object to){
return String.format("{%s: \"%s\", from: \"%s\", to:\"%s\"}", objectName, object, from, to);
}
@ -2422,7 +2441,7 @@ public class StorageSystemDataMotionStrategy implements DataMotionStrategy {
/**
* Handle post destination volume creation actions depending on the migrating volume type: full clone or linked clone
*/
protected void postVolumeCreationActions(VolumeInfo srcVolumeInfo, VolumeInfo destVolumeInfo, VirtualMachineTO vmTO, Host srcHost) {
protected void postVolumeCreationActions(VolumeInfo srcVolumeInfo, VolumeInfo destVolumeInfo) {
MigrationOptions migrationOptions = destVolumeInfo.getMigrationOptions();
if (migrationOptions != null) {
if (migrationOptions.getType() == MigrationOptions.Type.LinkedClone && migrationOptions.isCopySrcTemplate()) {

View File

@ -21,6 +21,9 @@ import java.util.List;
import javax.inject.Inject;
import com.cloud.user.Account;
import org.apache.cloudstack.api.ACL;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
@ -40,6 +43,7 @@ public class QuotaBalanceCmd extends BaseCmd {
@Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, required = true, description = "Account Id for which statement needs to be generated")
private String accountName;
@ACL
@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = true, entityType = DomainResponse.class, description = "If domain Id is given and the caller is domain admin then the statement is generated for domain.")
private Long domainId;
@ -51,6 +55,7 @@ public class QuotaBalanceCmd extends BaseCmd {
ApiConstants.PARAMETER_DESCRIPTION_START_DATE_POSSIBLE_FORMATS)
private Date startDate;
@ACL
@Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "List usage records for the specified account")
private Long accountId;
@ -104,7 +109,14 @@ public class QuotaBalanceCmd extends BaseCmd {
@Override
public long getEntityOwnerId() {
return _accountService.getActiveAccountByName(accountName, domainId).getAccountId();
if (accountId != null) {
return accountId;
}
Account account = _accountService.getActiveAccountByName(accountName, domainId);
if (account != null) {
return account.getAccountId();
}
return Account.ACCOUNT_ID_SYSTEM;
}
@Override

View File

@ -18,6 +18,7 @@ package org.apache.cloudstack.api.command;
import com.cloud.user.Account;
import org.apache.cloudstack.api.ACL;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
@ -46,6 +47,7 @@ public class QuotaCreditsCmd extends BaseCmd {
@Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, required = true, description = "Account Id for which quota credits need to be added")
private String accountName;
@ACL
@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = true, entityType = DomainResponse.class, description = "Domain for which quota credits need to be added")
private Long domainId;
@ -130,6 +132,10 @@ public class QuotaCreditsCmd extends BaseCmd {
@Override
public long getEntityOwnerId() {
Account account = _accountService.getActiveAccountByName(accountName, domainId);
if (account != null) {
return account.getAccountId();
}
return Account.ACCOUNT_ID_SYSTEM;
}

View File

@ -21,6 +21,7 @@ import java.util.List;
import javax.inject.Inject;
import org.apache.cloudstack.api.ACL;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
@ -42,6 +43,7 @@ public class QuotaStatementCmd extends BaseCmd {
@Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, required = true, description = "Optional, Account Id for which statement needs to be generated")
private String accountName;
@ACL
@Parameter(name = ApiConstants.DOMAIN_ID, type = CommandType.UUID, required = true, entityType = DomainResponse.class, description = "Optional, If domain Id is given and the caller is domain admin then the statement is generated for domain.")
private Long domainId;
@ -56,6 +58,7 @@ public class QuotaStatementCmd extends BaseCmd {
@Parameter(name = ApiConstants.TYPE, type = CommandType.INTEGER, description = "List quota usage records for the specified usage type")
private Integer usageType;
@ACL
@Parameter(name = ApiConstants.ACCOUNT_ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "List usage records for the specified account")
private Long accountId;
@ -112,6 +115,9 @@ public class QuotaStatementCmd extends BaseCmd {
@Override
public long getEntityOwnerId() {
if (accountId != null) {
return accountId;
}
Account activeAccountByName = _accountService.getActiveAccountByName(accountName, domainId);
if (activeAccountByName != null) {
return activeAccountByName.getAccountId();

View File

@ -123,8 +123,10 @@ public class MigrateKVMAsync implements Callable<Domain> {
if (migrateNonSharedInc) {
flags |= VIR_MIGRATE_PERSIST_DEST;
flags |= VIR_MIGRATE_NON_SHARED_INC;
logger.debug("Setting VIR_MIGRATE_NON_SHARED_INC for linked clone migration.");
} else {
flags |= VIR_MIGRATE_NON_SHARED_DISK;
logger.debug("Setting VIR_MIGRATE_NON_SHARED_DISK for full clone migration.");
}
}

View File

@ -454,6 +454,11 @@ public class MockAccountManager extends ManagerBase implements AccountManager {
// TODO Auto-generated method stub
}
@Override
public void validateAccountHasAccessToResource(Account account, AccessType accessType, Object resource) {
// TODO Auto-generated method stub
}
@Override
public Long finalyzeAccountId(String accountName, Long domainId, Long projectId, boolean enabledOnly) {
// TODO Auto-generated method stub

View File

@ -47,6 +47,7 @@ import org.apache.cloudstack.api.response.UserResponse;
import org.apache.cloudstack.saml.SAML2AuthManager;
import org.apache.cloudstack.saml.SAMLUtils;
import com.cloud.api.ApiServer;
import com.cloud.api.response.ApiResponseSerializer;
import com.cloud.domain.Domain;
import com.cloud.domain.dao.DomainDao;
@ -59,6 +60,8 @@ import com.cloud.user.dao.UserAccountDao;
import com.cloud.user.dao.UserDao;
import com.cloud.utils.HttpUtils;
import org.apache.commons.lang3.EnumUtils;
@APICommand(name = "listAndSwitchSamlAccount", description = "Lists and switches to other SAML accounts owned by the SAML user", responseObject = SuccessResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
public class ListAndSwitchSAMLAccountCmd extends BaseCmd implements APIAuthenticator {
@ -102,7 +105,9 @@ public class ListAndSwitchSAMLAccountCmd extends BaseCmd implements APIAuthentic
params, responseType));
}
if (!HttpUtils.validateSessionKey(session, params, req.getCookies(), ApiConstants.SESSIONKEY)) {
HttpUtils.ApiSessionKeyCheckOption sessionKeyCheckOption = EnumUtils.getEnumIgnoreCase(HttpUtils.ApiSessionKeyCheckOption.class,
ApiServer.ApiSessionKeyCheckLocations.value(), HttpUtils.ApiSessionKeyCheckOption.CookieAndParameter);
if (!HttpUtils.validateSessionKey(session, params, req.getCookies(), ApiConstants.SESSIONKEY, sessionKeyCheckOption)) {
throw new ServerApiException(ApiErrorCode.UNAUTHORIZED, _apiServer.getSerializedApiError(ApiErrorCode.UNAUTHORIZED.getHttpCode(),
"Unauthorized session, please re-login",
params, responseType));

View File

@ -73,6 +73,9 @@ public interface SAML2AuthManager extends PluggableAPIAuthenticator, PluggableSe
ConfigKey<Boolean> SAMLCheckSignature = new ConfigKey<Boolean>("Advanced", Boolean.class, "saml2.check.signature", "true",
"When enabled (default and recommended), SAML2 signature checks are enforced and lack of signature in the SAML SSO response will cause login exception. Disabling this is not advisable but provided for backward compatibility for users who are able to accept the risks.", false);
ConfigKey<String> SAMLUserSessionKeyPathAttribute = new ConfigKey<String>("Advanced", String.class, "saml2.user.sessionkey.path", "",
"The Path attribute of sessionkey cookie when SAML users have logged in. If not set, it will be set to the path of SAML redirection URL (saml2.redirect.url).", true);
SAMLProviderMetadata getSPMetadata();
SAMLProviderMetadata getIdPMetadata(String entityId);
Collection<SAMLProviderMetadata> getAllIdPMetadata();

View File

@ -540,6 +540,7 @@ public class SAML2AuthManagerImpl extends AdapterBase implements SAML2AuthManage
SAMLServiceProviderSingleSignOnURL, SAMLServiceProviderSingleLogOutURL,
SAMLCloudStackRedirectionUrl, SAMLUserAttributeName,
SAMLIdentityProviderMetadataURL, SAMLDefaultIdentityProviderId,
SAMLSignatureAlgorithm, SAMLAppendDomainSuffix, SAMLTimeout, SAMLCheckSignature};
SAMLSignatureAlgorithm, SAMLAppendDomainSuffix, SAMLTimeout, SAMLCheckSignature,
SAMLUserSessionKeyPathAttribute};
}
}

View File

@ -25,6 +25,8 @@ import java.io.IOException;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.security.InvalidKeyException;
@ -102,7 +104,9 @@ import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
import com.cloud.api.ApiServlet;
import com.cloud.utils.HttpUtils;
import com.cloud.utils.exception.CloudRuntimeException;
public class SAMLUtils {
protected static Logger LOGGER = LogManager.getLogger(SAMLUtils.class);
@ -297,7 +301,26 @@ public class SAMLUtils {
resp.addCookie(new Cookie("timezone", URLEncoder.encode(timezone, HttpUtils.UTF_8)));
}
resp.addCookie(new Cookie("userfullname", URLEncoder.encode(loginResponse.getFirstName() + " " + loginResponse.getLastName(), HttpUtils.UTF_8).replace("+", "%20")));
resp.addHeader("SET-COOKIE", String.format("%s=%s;HttpOnly;Path=/client/api", ApiConstants.SESSIONKEY, loginResponse.getSessionKey()));
String redirectUrl = SAML2AuthManager.SAMLCloudStackRedirectionUrl.value();
String path = SAML2AuthManager.SAMLUserSessionKeyPathAttribute.value();
String domain = null;
try {
URI redirectUri = new URI(redirectUrl);
domain = redirectUri.getHost();
if (StringUtils.isBlank(path)) {
path = redirectUri.getPath();
}
if (StringUtils.isBlank(path)) {
path = "/";
}
} catch (URISyntaxException ex) {
throw new CloudRuntimeException("Invalid URI: " + redirectUrl);
}
String sameSite = ApiServlet.getApiSessionKeySameSite();
String sessionKeyCookie = String.format("%s=%s;Domain=%s;Path=%s;%s", ApiConstants.SESSIONKEY, loginResponse.getSessionKey(), domain, path, sameSite);
s_logger.debug("Adding sessionkey cookie to response: " + sessionKeyCookie);
resp.addHeader("SET-COOKIE", sessionKeyCookie);
}
/**

View File

@ -27,6 +27,7 @@ import java.net.InetAddress;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@ -88,6 +89,9 @@ public class ListAndSwitchSAMLAccountCmdTest extends TestCase {
@Mock
HttpServletRequest req;
final String sessionId = "node0xxxxxxxxxxxxx";
Cookie[] cookies;
@Test
public void testListAndSwitchSAMLAccountCmd() throws Exception {
// Setup
@ -95,6 +99,7 @@ public class ListAndSwitchSAMLAccountCmdTest extends TestCase {
final String sessionKeyValue = "someSessionIDValue";
Mockito.when(session.getAttribute(ApiConstants.SESSIONKEY)).thenReturn(sessionKeyValue);
Mockito.when(session.getAttribute("userid")).thenReturn(2L);
Mockito.when(session.getId()).thenReturn(sessionId);
params.put(ApiConstants.USER_ID, new String[]{"2"});
params.put(ApiConstants.DOMAIN_ID, new String[]{"1"});
Mockito.when(userDao.findByUuid(anyString())).thenReturn(new UserVO(2L));
@ -146,7 +151,25 @@ public class ListAndSwitchSAMLAccountCmdTest extends TestCase {
Mockito.verify(accountService, Mockito.times(0)).getUserAccountById(Mockito.anyLong());
}
// valid sessionkey value test
// valid sessionkey value and invalid JSESSIONID test
cookies = new Cookie[2];
cookies[0] = new Cookie(ApiConstants.SESSIONKEY, sessionKeyValue);
cookies[1] = new Cookie("JSESSIONID", "invalid-JSESSIONID");
Mockito.when(req.getCookies()).thenReturn(cookies);
params.put(ApiConstants.SESSIONKEY, new String[]{sessionKeyValue});
try {
cmd.authenticate("command", params, session, null, HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);
} catch (ServerApiException exception) {
assertEquals(exception.getErrorCode(), ApiErrorCode.UNAUTHORIZED);
} finally {
Mockito.verify(accountService, Mockito.times(0)).getUserAccountById(Mockito.anyLong());
}
// valid sessionkey value and valid JSESSIONID test
cookies = new Cookie[2];
cookies[0] = new Cookie(ApiConstants.SESSIONKEY, sessionKeyValue);
cookies[1] = new Cookie("JSESSIONID", sessionId + ".node0");
Mockito.when(req.getCookies()).thenReturn(cookies);
params.put(ApiConstants.SESSIONKEY, new String[]{sessionKeyValue});
try {
cmd.authenticate("command", params, session, null, HttpUtils.RESPONSE_TYPE_JSON, new StringBuilder(), req, resp);

View File

@ -33,6 +33,7 @@ import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
@ -47,6 +48,7 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
@ -169,6 +171,8 @@ import com.cloud.storage.VolumeApiService;
import com.cloud.utils.ConstantTimeComparator;
import com.cloud.utils.DateUtil;
import com.cloud.utils.HttpUtils;
import com.cloud.utils.HttpUtils.ApiSessionKeySameSite;
import com.cloud.utils.HttpUtils.ApiSessionKeyCheckOption;
import com.cloud.utils.Pair;
import com.cloud.utils.ReflectUtil;
import com.cloud.utils.StringUtils;
@ -310,6 +314,24 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
, true
, ConfigKey.Scope.Global);
static final ConfigKey<String> ApiSessionKeyCookieSameSiteSetting = new ConfigKey<>(String.class
, "api.sessionkey.cookie.samesite"
, ConfigKey.CATEGORY_ADVANCED
, ApiSessionKeySameSite.Lax.name()
, "The SameSite attribute of cookie 'sessionkey'. Valid options are: Lax (default), Strict, NoneAndSecure and Null."
, true
, ConfigKey.Scope.Global, null, null, null, null, null, ConfigKey.Kind.Select,
EnumSet.allOf(ApiSessionKeySameSite.class).stream().map(Enum::toString).collect(Collectors.joining(", ")));
public static final ConfigKey<String> ApiSessionKeyCheckLocations = new ConfigKey<>(String.class
, "api.sessionkey.check.locations"
, ConfigKey.CATEGORY_ADVANCED
, ApiSessionKeyCheckOption.CookieAndParameter.name()
, "The locations of 'sessionkey' during the validation of the API requests. Valid options are: CookieOrParameter, ParameterOnly, CookieAndParameter (default)."
, true
, ConfigKey.Scope.Global, null, null, null, null, null, ConfigKey.Kind.Select,
EnumSet.allOf(ApiSessionKeyCheckOption.class).stream().map(Enum::toString).collect(Collectors.joining(", ")));
@Override
public boolean configure(final String name, final Map<String, Object> params) throws ConfigurationException {
messageBus.subscribe(AsyncJob.Topics.JOB_EVENT_PUBLISH, MessageDispatcher.getDispatcher(this));
@ -1582,7 +1604,9 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
JSONDefaultContentType,
proxyForwardList,
useForwardHeader,
listOfForwardHeaders
listOfForwardHeaders,
ApiSessionKeyCookieSameSiteSetting,
ApiSessionKeyCheckLocations
};
}
}

View File

@ -47,8 +47,10 @@ import org.apache.cloudstack.api.command.user.consoleproxy.CreateConsoleEndpoint
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.managed.context.ManagedContext;
import org.apache.cloudstack.utils.consoleproxy.ConsoleAccessUtils;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
import org.apache.commons.lang3.EnumUtils;
import org.jetbrains.annotations.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.context.support.SpringBeanAutowiringSupport;
@ -65,6 +67,8 @@ import com.cloud.user.User;
import com.cloud.user.UserAccount;
import com.cloud.utils.HttpUtils;
import com.cloud.utils.HttpUtils.ApiSessionKeySameSite;
import com.cloud.utils.HttpUtils.ApiSessionKeyCheckOption;
import com.cloud.utils.StringUtils;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.net.NetUtils;
@ -255,7 +259,8 @@ public class ApiServlet extends HttpServlet {
}
responseString = apiAuthenticator.authenticate(command, params, session, remoteAddress, responseType, auditTrailSb, req, resp);
if (session != null && session.getAttribute(ApiConstants.SESSIONKEY) != null) {
resp.addHeader("SET-COOKIE", String.format("%s=%s;HttpOnly", ApiConstants.SESSIONKEY, session.getAttribute(ApiConstants.SESSIONKEY)));
String sameSite = getApiSessionKeySameSite();
resp.addHeader("SET-COOKIE", String.format("%s=%s;HttpOnly;%s", ApiConstants.SESSIONKEY, session.getAttribute(ApiConstants.SESSIONKEY), sameSite));
}
} catch (ServerApiException e) {
httpResponseCode = e.getErrorCode().getHttpCode();
@ -264,19 +269,22 @@ public class ApiServlet extends HttpServlet {
}
if (apiAuthenticator.getAPIType() == APIAuthenticationType.LOGOUT_API) {
if (session != null) {
final Long userId = (Long) session.getAttribute("userid");
final Account account = (Account) session.getAttribute("accountobj");
Long accountId = null;
if (account != null) {
accountId = account.getId();
}
auditTrailSb.insert(0, "(userId=" + userId + " accountId=" + accountId + " sessionId=" + session.getId() + ")");
if (userId != null) {
apiServer.logoutUser(userId);
}
invalidateHttpSession(session, "invalidating session after logout call");
if (session == null) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Session not found for the logout process.");
}
final Long userId = (Long) session.getAttribute("userid");
final Account account = (Account) session.getAttribute("accountobj");
Long accountId = null;
if (account != null) {
accountId = account.getId();
}
auditTrailSb.insert(0, "(userId=" + userId + " accountId=" + accountId + " sessionId=" + session.getId() + ")");
if (userId != null) {
apiServer.logoutUser(userId);
}
invalidateHttpSession(session, "invalidating session after logout call");
final Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (final Cookie cookie : cookies) {
@ -375,6 +383,22 @@ public class ApiServlet extends HttpServlet {
}
}
public static String getApiSessionKeySameSite() {
ApiSessionKeySameSite sameSite = EnumUtils.getEnumIgnoreCase(ApiSessionKeySameSite.class,
ApiServer.ApiSessionKeyCookieSameSiteSetting.value(), ApiSessionKeySameSite.Lax);
switch (sameSite) {
case Strict:
return "SameSite=Strict";
case NoneAndSecure:
return "SameSite=None;Secure";
case Null:
return "";
case Lax:
default:
return "SameSite=Lax";
}
}
private boolean checkIfAuthenticatorIsOf2FA(String command) {
boolean verify2FA = false;
APIAuthenticator apiAuthenticator = authManager.getAPIAuthenticator(command);
@ -510,7 +534,9 @@ public class ApiServlet extends HttpServlet {
}
private boolean invalidateHttpSessionIfNeeded(HttpServletRequest req, HttpServletResponse resp, StringBuilder auditTrailSb, String responseType, Map<String, Object[]> params, HttpSession session, String account) {
if (!HttpUtils.validateSessionKey(session, params, req.getCookies(), ApiConstants.SESSIONKEY)) {
ApiSessionKeyCheckOption sessionKeyCheckOption = EnumUtils.getEnumIgnoreCase(ApiSessionKeyCheckOption.class,
ApiServer.ApiSessionKeyCheckLocations.value(), ApiSessionKeyCheckOption.CookieAndParameter);
if (!HttpUtils.validateSessionKey(session, params, req.getCookies(), ApiConstants.SESSIONKEY, sessionKeyCheckOption)) {
String msg = String.format("invalidating session %s for account %s", session.getId(), account);
invalidateHttpSession(session, msg);
auditTrailSb.append(" " + HttpServletResponse.SC_UNAUTHORIZED + " " + "unable to verify user credentials");

View File

@ -32,8 +32,6 @@ import java.util.regex.Matcher;
import javax.inject.Inject;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.InfrastructureEntity;
import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.acl.SecurityChecker.AccessType;
import org.apache.cloudstack.api.ACL;
@ -334,19 +332,35 @@ public class ParamProcessWorker implements DispatchWorker {
_accountMgr.checkAccess(caller, null, false, owners);
}
if (!entitiesToAccess.isEmpty()) {
// check that caller can access the owner account.
_accountMgr.checkAccess(caller, null, false, owners);
for (Map.Entry<Object,AccessType>entry : entitiesToAccess.entrySet()) {
Object entity = entry.getKey();
if (entity instanceof ControlledEntity) {
_accountMgr.checkAccess(caller, entry.getValue(), true, (ControlledEntity) entity);
} else if (entity instanceof InfrastructureEntity) {
// FIXME: Move this code in adapter, remove code from
// Account manager
}
checkCallerAccessToEntities(caller, owners, entitiesToAccess);
}
protected Account[] getEntityOwners(BaseCmd cmd) {
List<Long> entityOwners = cmd.getEntityOwnerIds();
if (entityOwners != null) {
return entityOwners.stream().map(id -> _accountMgr.getAccount(id)).toArray(Account[]::new);
}
if (cmd.getEntityOwnerId() == Account.ACCOUNT_ID_SYSTEM && cmd instanceof BaseAsyncCmd && cmd.getApiResourceType() == ApiCommandResourceType.Network) {
logger.debug("Skipping access check on the network owner if the owner is ROOT/system.");
} else {
Account owner = _accountMgr.getAccount(cmd.getEntityOwnerId());
if (owner != null) {
return new Account[]{owner};
}
}
return new Account[]{};
}
protected void checkCallerAccessToEntities(Account caller, Account[] owners, Map<Object, AccessType> entitiesToAccess) {
if (entitiesToAccess.isEmpty()) {
return;
}
_accountMgr.checkAccess(caller, null, false, owners);
for (Map.Entry<Object, AccessType> entry : entitiesToAccess.entrySet()) {
Object entity = entry.getKey();
_accountMgr.validateAccountHasAccessToResource(caller, entry.getValue(), entity);
}
}
@SuppressWarnings({"unchecked", "rawtypes"})

View File

@ -23,10 +23,11 @@ import java.util.Map;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Component;
import org.apache.cloudstack.annotation.AnnotationService;
import org.apache.cloudstack.annotation.dao.AnnotationDao;
import org.apache.cloudstack.api.command.user.vpn.CreateVpnConnectionCmd;
import org.apache.cloudstack.api.command.user.vpn.CreateVpnCustomerGatewayCmd;
import org.apache.cloudstack.api.command.user.vpn.CreateVpnGatewayCmd;
@ -45,7 +46,6 @@ import com.cloud.configuration.Config;
import com.cloud.event.ActionEvent;
import com.cloud.event.EventTypes;
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.exception.NetworkRuleConflictException;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.network.Site2SiteCustomerGateway;
@ -106,7 +106,6 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
@Inject
private AnnotationDao annotationDao;
String _name;
int _connLimit;
int _subnetsLimit;
@ -253,7 +252,7 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
@Override
@ActionEvent(eventType = EventTypes.EVENT_S2S_VPN_CONNECTION_CREATE, eventDescription = "creating s2s vpn connection", create = true)
public Site2SiteVpnConnection createVpnConnection(CreateVpnConnectionCmd cmd) throws NetworkRuleConflictException {
public Site2SiteVpnConnection createVpnConnection(CreateVpnConnectionCmd cmd) {
Account caller = CallContext.current().getCallingAccount();
Account owner = _accountMgr.getAccount(cmd.getEntityOwnerId());
@ -261,27 +260,15 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
_accountMgr.checkAccess(caller, null, false, owner);
Long customerGatewayId = cmd.getCustomerGatewayId();
Site2SiteCustomerGateway customerGateway = _customerGatewayDao.findById(customerGatewayId);
if (customerGateway == null) {
throw new InvalidParameterValueException("Unable to found specified Site to Site VPN customer gateway " + customerGatewayId + " !");
}
_accountMgr.checkAccess(caller, null, false, customerGateway);
Site2SiteCustomerGateway customerGateway = getAndValidateSite2SiteCustomerGateway(customerGatewayId, caller);
Long vpnGatewayId = cmd.getVpnGatewayId();
Site2SiteVpnGateway vpnGateway = _vpnGatewayDao.findById(vpnGatewayId);
if (vpnGateway == null) {
throw new InvalidParameterValueException("Unable to found specified Site to Site VPN gateway " + vpnGatewayId + " !");
}
_accountMgr.checkAccess(caller, null, false, vpnGateway);
Site2SiteVpnGateway vpnGateway = getAndValidateSite2SiteVpnGateway(vpnGatewayId, caller);
if (customerGateway.getAccountId() != vpnGateway.getAccountId() || customerGateway.getDomainId() != vpnGateway.getDomainId()) {
throw new InvalidParameterValueException("VPN connection can only be esitablished between same account's VPN gateway and customer gateway!");
}
validateVpnConnectionOfTheRightAccount(customerGateway, vpnGateway);
validateVpnConnectionDoesntExist(vpnGatewayId, customerGatewayId);
validatePrerequisiteVpnGateway(vpnGateway);
if (_vpnConnectionDao.findByVpnGatewayIdAndCustomerGatewayId(vpnGatewayId, customerGatewayId) != null) {
throw new InvalidParameterValueException("The vpn connection with customer gateway id " + customerGatewayId + " and vpn gateway id " + vpnGatewayId +
" already existed!");
}
String[] cidrList = customerGateway.getGuestCidrList().split(",");
// Remote sub nets cannot overlap VPC's sub net
@ -324,13 +311,51 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
return conn;
}
private Site2SiteCustomerGateway getAndValidateSite2SiteCustomerGateway(Long customerGatewayId, Account caller) {
Site2SiteCustomerGateway customerGateway = _customerGatewayDao.findById(customerGatewayId);
if (customerGateway == null) {
throw new InvalidParameterValueException(String.format("Unable to find specified Site to Site VPN customer gateway %s !", customerGatewayId));
}
_accountMgr.checkAccess(caller, null, false, customerGateway);
return customerGateway;
}
private Site2SiteVpnGateway getAndValidateSite2SiteVpnGateway(Long vpnGatewayId, Account caller) {
Site2SiteVpnGateway vpnGateway = _vpnGatewayDao.findById(vpnGatewayId);
if (vpnGateway == null) {
throw new InvalidParameterValueException(String.format("Unable to find specified Site to Site VPN gateway %s !", vpnGatewayId));
}
_accountMgr.checkAccess(caller, null, false, vpnGateway);
return vpnGateway;
}
private void validateVpnConnectionOfTheRightAccount(Site2SiteCustomerGateway customerGateway, Site2SiteVpnGateway vpnGateway) {
if (customerGateway.getAccountId() != vpnGateway.getAccountId() || customerGateway.getDomainId() != vpnGateway.getDomainId()) {
throw new InvalidParameterValueException("VPN connection can only be established between same account's VPN gateway and customer gateway!");
}
}
private void validateVpnConnectionDoesntExist(Long vpnGatewayId, Long customerGatewayId) {
if (_vpnConnectionDao.findByVpnGatewayIdAndCustomerGatewayId(vpnGatewayId, customerGatewayId) != null) {
throw new InvalidParameterValueException("The vpn connection with customer gateway id " + customerGatewayId + " and vpn gateway id " + vpnGatewayId +
" already existed!");
}
}
private void validatePrerequisiteVpnGateway(Site2SiteVpnGateway vpnGateway) {
// check if gateway has been defined on the VPC
if (_vpnGatewayDao.findByVpcId(vpnGateway.getVpcId()) == null) {
throw new InvalidParameterValueException("we can not create a VPN connection for a VPC that does not have a VPN gateway defined");
}
}
@Override
@DB
@ActionEvent(eventType = EventTypes.EVENT_S2S_VPN_CONNECTION_CREATE, eventDescription = "starting s2s vpn connection", async = true)
public Site2SiteVpnConnection startVpnConnection(long id) throws ResourceUnavailableException {
Site2SiteVpnConnectionVO conn = _vpnConnectionDao.acquireInLockTable(id);
if (conn == null) {
throw new CloudRuntimeException("Unable to acquire lock on " + conn);
throw new CloudRuntimeException("Unable to acquire lock for starting of VPN connection with ID " + id);
}
try {
if (conn.getState() != State.Pending && conn.getState() != State.Disconnected) {
@ -380,11 +405,7 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
Account caller = CallContext.current().getCallingAccount();
Long id = cmd.getId();
Site2SiteCustomerGateway customerGateway = _customerGatewayDao.findById(id);
if (customerGateway == null) {
throw new InvalidParameterValueException("Fail to find customer gateway with " + id + " !");
}
_accountMgr.checkAccess(caller, null, false, customerGateway);
Site2SiteCustomerGateway customerGateway = getAndValidateSite2SiteCustomerGateway(id, caller);
return doDeleteCustomerGateway(customerGateway);
}
@ -392,7 +413,7 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
protected boolean doDeleteCustomerGateway(Site2SiteCustomerGateway gw) {
long id = gw.getId();
List<Site2SiteVpnConnectionVO> vpnConnections = _vpnConnectionDao.listByCustomerGatewayId(id);
if (vpnConnections != null && vpnConnections.size() != 0) {
if (!CollectionUtils.isEmpty(vpnConnections)) {
throw new InvalidParameterValueException("Unable to delete VPN customer gateway with id " + id + " because there is still related VPN connections!");
}
annotationDao.removeByEntityType(AnnotationService.EntityType.VPN_CUSTOMER_GATEWAY.name(), gw.getUuid());
@ -402,7 +423,7 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
protected void doDeleteVpnGateway(Site2SiteVpnGateway gw) {
List<Site2SiteVpnConnectionVO> conns = _vpnConnectionDao.listByVpnGatewayId(gw.getId());
if (conns != null && conns.size() != 0) {
if (!CollectionUtils.isEmpty(conns)) {
throw new InvalidParameterValueException("Unable to delete VPN gateway " + gw.getId() + " because there is still related VPN connections!");
}
_vpnGatewayDao.remove(gw.getId());
@ -415,12 +436,7 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
Account caller = CallContext.current().getCallingAccount();
Long id = cmd.getId();
Site2SiteVpnGateway vpnGateway = _vpnGatewayDao.findById(id);
if (vpnGateway == null) {
throw new InvalidParameterValueException("Fail to find vpn gateway with " + id + " !");
}
_accountMgr.checkAccess(caller, null, false, vpnGateway);
Site2SiteVpnGateway vpnGateway = getAndValidateSite2SiteVpnGateway(id, caller);
doDeleteVpnGateway(vpnGateway);
return true;
@ -576,7 +592,7 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
private void stopVpnConnection(Long id) throws ResourceUnavailableException {
Site2SiteVpnConnectionVO conn = _vpnConnectionDao.acquireInLockTable(id);
if (conn == null) {
throw new CloudRuntimeException("Unable to acquire lock on " + conn);
throw new CloudRuntimeException("Unable to acquire lock for stopping of VPN connection with ID " + id);
}
try {
if (conn.getState() == State.Pending) {
@ -637,10 +653,9 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
String keyword = cmd.getKeyword();
Account caller = CallContext.current().getCallingAccount();
List<Long> permittedAccounts = new ArrayList<Long>();
List<Long> permittedAccounts = new ArrayList<>();
Ternary<Long, Boolean, ListProjectResourcesCriteria> domainIdRecursiveListProject = new Ternary<Long, Boolean,
ListProjectResourcesCriteria>(domainId, isRecursive, null);
Ternary<Long, Boolean, ListProjectResourcesCriteria> domainIdRecursiveListProject = new Ternary<>(domainId, isRecursive, null);
_accountMgr.buildACLSearchParameters(caller, id, accountName, cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, listAll, false);
domainId = domainIdRecursiveListProject.first();
isRecursive = domainIdRecursiveListProject.second();
@ -665,7 +680,7 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
}
Pair<List<Site2SiteCustomerGatewayVO>, Integer> result = _customerGatewayDao.searchAndCount(sc, searchFilter);
return new Pair<List<? extends Site2SiteCustomerGateway>, Integer>(result.first(), result.second());
return new Pair<>(result.first(), result.second());
}
@Override
@ -682,10 +697,9 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
long pageSizeVal = cmd.getPageSizeVal();
Account caller = CallContext.current().getCallingAccount();
List<Long> permittedAccounts = new ArrayList<Long>();
List<Long> permittedAccounts = new ArrayList<>();
Ternary<Long, Boolean, ListProjectResourcesCriteria> domainIdRecursiveListProject = new Ternary<Long, Boolean,
ListProjectResourcesCriteria>(domainId, isRecursive, null);
Ternary<Long, Boolean, ListProjectResourcesCriteria> domainIdRecursiveListProject = new Ternary<>(domainId, isRecursive, null);
_accountMgr.buildACLSearchParameters(caller, id, accountName, cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, listAll, false);
domainId = domainIdRecursiveListProject.first();
isRecursive = domainIdRecursiveListProject.second();
@ -715,7 +729,7 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
}
Pair<List<Site2SiteVpnGatewayVO>, Integer> result = _vpnGatewayDao.searchAndCount(sc, searchFilter);
return new Pair<List<? extends Site2SiteVpnGateway>, Integer>(result.first(), result.second());
return new Pair<>(result.first(), result.second());
}
@Override
@ -732,10 +746,9 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
long pageSizeVal = cmd.getPageSizeVal();
Account caller = CallContext.current().getCallingAccount();
List<Long> permittedAccounts = new ArrayList<Long>();
List<Long> permittedAccounts = new ArrayList<>();
Ternary<Long, Boolean, ListProjectResourcesCriteria> domainIdRecursiveListProject = new Ternary<Long, Boolean,
ListProjectResourcesCriteria>(domainId, isRecursive, null);
Ternary<Long, Boolean, ListProjectResourcesCriteria> domainIdRecursiveListProject = new Ternary<>(domainId, isRecursive, null);
_accountMgr.buildACLSearchParameters(caller, id, accountName, cmd.getProjectId(), permittedAccounts, domainIdRecursiveListProject, listAll, false);
domainId = domainIdRecursiveListProject.first();
isRecursive = domainIdRecursiveListProject.second();
@ -769,7 +782,7 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
}
Pair<List<Site2SiteVpnConnectionVO>, Integer> result = _vpnConnectionDao.searchAndCount(sc, searchFilter);
return new Pair<List<? extends Site2SiteVpnConnection>, Integer>(result.first(), result.second());
return new Pair<>(result.first(), result.second());
}
@Override
@ -816,7 +829,7 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
@Override
public List<Site2SiteVpnConnectionVO> getConnectionsForRouter(DomainRouterVO router) {
List<Site2SiteVpnConnectionVO> conns = new ArrayList<Site2SiteVpnConnectionVO>();
List<Site2SiteVpnConnectionVO> conns = new ArrayList<>();
// One router for one VPC
Long vpcId = router.getVpcId();
if (router.getVpcId() == null) {
@ -829,7 +842,6 @@ public class Site2SiteVpnManagerImpl extends ManagerBase implements Site2SiteVpn
@Override
public boolean deleteCustomerGatewayByAccount(long accountId) {
boolean result = true;
;
List<Site2SiteCustomerGatewayVO> gws = _customerGatewayDao.listByAccountId(accountId);
for (Site2SiteCustomerGatewayVO gw : gws) {
result = result & doDeleteCustomerGateway(gw);

View File

@ -4859,7 +4859,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
sb.and("id", sb.entity().getId(), SearchCriteria.Op.EQ);
sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ);
sb.and("name", sb.entity().getName(), SearchCriteria.Op.EQ);
sb.and("keyword", sb.entity().getName(), SearchCriteria.Op.LIKE);
final SearchCriteria<UserDataVO> sc = sb.create();
_accountMgr.buildACLSearchCriteria(sc, domainId, isRecursive, permittedAccounts, listProjectResourcesCriteria);
@ -4872,7 +4872,7 @@ public class ManagementServerImpl extends ManagerBase implements ManagementServe
}
if (keyword != null) {
sc.setParameters("name", "%" + keyword + "%");
sc.setParameters("keyword", "%" + keyword + "%");
}
final Pair<List<UserDataVO>, Integer> result = userDataDao.searchAndCount(sc, searchFilter);

View File

@ -43,6 +43,7 @@ import javax.naming.ConfigurationException;
import org.apache.cloudstack.acl.APIChecker;
import org.apache.cloudstack.acl.ControlledEntity;
import org.apache.cloudstack.acl.InfrastructureEntity;
import org.apache.cloudstack.acl.QuerySelector;
import org.apache.cloudstack.acl.Role;
import org.apache.cloudstack.acl.RoleService;
@ -740,6 +741,19 @@ public class AccountManagerImpl extends ManagerBase implements AccountManager, M
}
@Override
public void validateAccountHasAccessToResource(Account account, AccessType accessType, Object resource) {
Class<?> resourceClass = resource.getClass();
if (ControlledEntity.class.isAssignableFrom(resourceClass)) {
checkAccess(account, accessType, true, (ControlledEntity) resource);
} else if (Domain.class.isAssignableFrom(resourceClass)) {
checkAccess(account, (Domain) resource);
} else if (InfrastructureEntity.class.isAssignableFrom(resourceClass)) {
s_logger.trace("Validation of access to infrastructure entity has been disabled in CloudStack version 4.4.");
}
s_logger.debug(String.format("Account [%s] has access to resource.", account.getUuid()));
}
@Override
public Long checkAccessAndSpecifyAuthority(Account caller, Long zoneId) {
// We just care for resource domain admins for now, and they should be permitted to see only their zone.

View File

@ -18,6 +18,30 @@
*/
package com.cloud.api.dispatch;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.InjectMocks;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.apache.cloudstack.api.ApiArgValidator;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.command.user.address.AssociateIPAddrCmd;
import org.apache.cloudstack.acl.SecurityChecker;
import org.apache.cloudstack.context.CallContext;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.NetworkRuleConflictException;
@ -26,29 +50,33 @@ import com.cloud.exception.ResourceUnavailableException;
import com.cloud.user.Account;
import com.cloud.user.AccountManager;
import com.cloud.user.User;
import org.apache.cloudstack.api.ApiArgValidator;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.context.CallContext;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.HashMap;
import com.cloud.vm.VMInstanceVO;
@RunWith(MockitoJUnitRunner.class)
public class ParamProcessWorkerTest {
@Mock
protected AccountManager accountManager;
@Spy
@InjectMocks
private ParamProcessWorker paramProcessWorkerSpy;
protected ParamProcessWorker paramProcessWorker;
@Mock
private AccountManager accountManagerMock;
@Mock
private Account callingAccountMock;
@Mock
private User callingUserMock;
@Mock
private Account ownerAccountMock;
@Mock
BaseCmd baseCmdMock;
private Account[] owners = new Account[]{ownerAccountMock};
private Map<Object, SecurityChecker.AccessType> entities = new HashMap<>();
public static class TestCmd extends BaseCmd {
@ -69,7 +97,7 @@ public class ParamProcessWorkerTest {
@Override
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException,
ResourceAllocationException, NetworkRuleConflictException {
ResourceAllocationException, NetworkRuleConflictException {
// well documented nothing
}
@ -87,9 +115,7 @@ public class ParamProcessWorkerTest {
@Before
public void setup() {
CallContext.register(Mockito.mock(User.class), Mockito.mock(Account.class));
paramProcessWorker = new ParamProcessWorker();
paramProcessWorker._accountMgr = accountManager;
CallContext.register(callingUserMock, callingAccountMock);
}
@After
@ -106,7 +132,7 @@ public class ParamProcessWorkerTest {
params.put("doubleparam1", "11.89");
params.put("vmHostNameParam", "test-host-name-123");
final TestCmd cmd = new TestCmd();
paramProcessWorker.processParameters(cmd, params);
paramProcessWorkerSpy.processParameters(cmd, params);
Assert.assertEquals("foo", cmd.strparam1);
Assert.assertEquals(100, cmd.intparam1);
Assert.assertTrue(Double.compare(cmd.doubleparam1, 11.89) == 0);
@ -118,7 +144,7 @@ public class ParamProcessWorkerTest {
final HashMap<String, String> params = new HashMap<String, String>();
params.put("vmHostNameParam", "123test");
final TestCmd cmd = new TestCmd();
paramProcessWorker.processParameters(cmd, params);
paramProcessWorkerSpy.processParameters(cmd, params);
}
@Test(expected = ServerApiException.class)
@ -126,7 +152,7 @@ public class ParamProcessWorkerTest {
final HashMap<String, String> params = new HashMap<String, String>();
params.put("vmHostNameParam", "-test");
final TestCmd cmd = new TestCmd();
paramProcessWorker.processParameters(cmd, params);
paramProcessWorkerSpy.processParameters(cmd, params);
}
@Test(expected = ServerApiException.class)
@ -134,7 +160,7 @@ public class ParamProcessWorkerTest {
final HashMap<String, String> params = new HashMap<String, String>();
params.put("vmHostNameParam", "test-");
final TestCmd cmd = new TestCmd();
paramProcessWorker.processParameters(cmd, params);
paramProcessWorkerSpy.processParameters(cmd, params);
}
@Test(expected = ServerApiException.class)
@ -142,6 +168,68 @@ public class ParamProcessWorkerTest {
final HashMap<String, String> params = new HashMap<String, String>();
params.put("vmHostNameParam", "test-f2405112-d5a1-47c1-9f00-976909e3a6d3-1e6f3264-955ee76011a99");
final TestCmd cmd = new TestCmd();
paramProcessWorker.processParameters(cmd, params);
paramProcessWorkerSpy.processParameters(cmd, params);
Mockito.verify(paramProcessWorkerSpy).doAccessChecks(Mockito.any(), Mockito.any());
}
@Test
public void doAccessChecksTestChecksCallerAccessToOwnerWhenCmdExtendsBaseAsyncCreateCmd() {
Mockito.doReturn(owners).when(paramProcessWorkerSpy).getEntityOwners(Mockito.any());
Mockito.doNothing().when(paramProcessWorkerSpy).checkCallerAccessToEntities(Mockito.any(), Mockito.any(), Mockito.any());
paramProcessWorkerSpy.doAccessChecks(new AssociateIPAddrCmd(), entities);
Mockito.verify(accountManagerMock).checkAccess(callingAccountMock, null, false, owners);
}
@Test
public void doAccessChecksTestChecksCallerAccessToEntities() {
Mockito.doReturn(owners).when(paramProcessWorkerSpy).getEntityOwners(Mockito.any());
Mockito.doNothing().when(paramProcessWorkerSpy).checkCallerAccessToEntities(Mockito.any(), Mockito.any(), Mockito.any());
paramProcessWorkerSpy.doAccessChecks(new AssociateIPAddrCmd(), entities);
Mockito.verify(paramProcessWorkerSpy).checkCallerAccessToEntities(callingAccountMock, owners, entities);
}
@Test
public void getEntityOwnersTestReturnsAccountsWhenCmdHasMultipleEntityOwners() {
Mockito.when(baseCmdMock.getEntityOwnerIds()).thenReturn(List.of(1L, 2L));
Mockito.doReturn(callingAccountMock).when(accountManagerMock).getAccount(1L);
Mockito.doReturn(ownerAccountMock).when(accountManagerMock).getAccount(2L);
List<Account> result = List.of(paramProcessWorkerSpy.getEntityOwners(baseCmdMock));
Assert.assertEquals(List.of(callingAccountMock, ownerAccountMock), result);
}
@Test
public void getEntityOwnersTestReturnsAccountWhenCmdHasOneEntityOwner() {
Mockito.when(baseCmdMock.getEntityOwnerId()).thenReturn(1L);
Mockito.when(baseCmdMock.getEntityOwnerIds()).thenReturn(null);
Mockito.doReturn(ownerAccountMock).when(accountManagerMock).getAccount(1L);
List<Account> result = List.of(paramProcessWorkerSpy.getEntityOwners(baseCmdMock));
Assert.assertEquals(List.of(ownerAccountMock), result);
}
@Test
public void checkCallerAccessToEntitiesTestChecksCallerAccessToOwners() {
entities.put(ownerAccountMock, SecurityChecker.AccessType.UseEntry);
paramProcessWorkerSpy.checkCallerAccessToEntities(callingAccountMock, owners, entities);
Mockito.verify(accountManagerMock).checkAccess(callingAccountMock, null, false, owners);
}
@Test
public void checkCallerAccessToEntitiesTestChecksCallerAccessToResource() {
VMInstanceVO vmInstanceVo = new VMInstanceVO();
entities.put(vmInstanceVo, SecurityChecker.AccessType.UseEntry);
paramProcessWorkerSpy.checkCallerAccessToEntities(callingAccountMock, owners, entities);
Mockito.verify(accountManagerMock).validateAccountHasAccessToResource(callingAccountMock, SecurityChecker.AccessType.UseEntry, vmInstanceVo);
}
}

View File

@ -438,6 +438,11 @@ public class MockAccountManagerImpl extends ManagerBase implements Manager, Acco
// TODO Auto-generated method stub
}
@Override
public void validateAccountHasAccessToResource(Account account, AccessType accessType, Object resource) {
// TODO Auto-generated method stub
}
@Override
public Long finalyzeAccountId(String accountName, Long domainId, Long projectId, boolean enabledOnly) {
// TODO Auto-generated method stub

View File

@ -0,0 +1,51 @@
// 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.storage.formatinspector;
public enum Qcow2HeaderField {
MAGIC(0, 4),
VERSION(4, 4),
BACKING_FILE_OFFSET(8, 8),
BACKING_FILE_NAME_LENGTH(16, 4),
CLUSTER_BITS(20, 4),
SIZE(24, 8),
CRYPT_METHOD(32, 4),
L1_SIZE(36, 4),
LI_TABLE_OFFSET(40, 8),
REFCOUNT_TABLE_OFFSET(48, 8),
REFCOUNT_TABLE_CLUSTERS(56, 4),
NB_SNAPSHOTS(60, 4),
SNAPSHOTS_OFFSET(64, 8),
INCOMPATIBLE_FEATURES(72, 8);
private final int offset;
private final int length;
Qcow2HeaderField(int offset, int length) {
this.offset = offset;
this.length = length;
}
public int getLength() {
return length;
}
public int getOffset() {
return offset;
}
}

View File

@ -0,0 +1,267 @@
// 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.storage.formatinspector;
import com.cloud.utils.NumbersUtil;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.log4j.Logger;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Class to inspect QCOW2 files/objects. In our context, a QCOW2 might be a threat to the environment if it meets one of the following criteria when coming from external sources
* (like registering or uploading volumes and templates):
* <ul>
* <li>has a backing file reference;</li>
* <li>has an external data file reference;</li>
* <li>has unknown incompatible features.</li>
* </ul>
*
* The implementation was done based on the <a href="https://gitlab.com/qemu-project/qemu/-/blob/master/docs/interop/qcow2.txt"> QEMU's official interoperability documentation</a>
* and on the <a href="https://review.opendev.org/c/openstack/cinder/+/923247/2/cinder/image/format_inspector.py">OpenStack's Cinder implementation for Python</a>.
*/
public class Qcow2Inspector {
protected static Logger LOGGER = Logger.getLogger(Qcow2Inspector.class);
private static final byte[] QCOW_MAGIC_STRING = ArrayUtils.add("QFI".getBytes(), (byte) 0xfb);
private static final int INCOMPATIBLE_FEATURES_MAX_KNOWN_BIT = 4;
private static final int INCOMPATIBLE_FEATURES_MAX_KNOWN_BYTE = 0;
private static final int EXTERNAL_DATA_FILE_BYTE_POSITION = 7;
private static final int EXTERNAL_DATA_FILE_BIT = 2;
private static final byte EXTERNAL_DATA_FILE_BITMASK = (byte) (1 << EXTERNAL_DATA_FILE_BIT);
private static final Set<Qcow2HeaderField> SET_OF_HEADER_FIELDS_TO_READ = Set.of(Qcow2HeaderField.MAGIC,
Qcow2HeaderField.VERSION,
Qcow2HeaderField.SIZE,
Qcow2HeaderField.BACKING_FILE_OFFSET,
Qcow2HeaderField.INCOMPATIBLE_FEATURES);
/**
* Validates if the file is a valid and allowed QCOW2 (i.e.: does not contain external references).
* @param filePath Path of the file to be validated.
* @throws RuntimeException If the QCOW2 file meets one of the following criteria:
* <ul>
* <li>has a backing file reference;</li>
* <li>has an external data file reference;</li>
* <li>has unknown incompatible features.</li>
* </ul>
*/
public static void validateQcow2File(String filePath) throws RuntimeException {
LOGGER.info(String.format("Verifying if [%s] is a valid and allowed QCOW2 file .", filePath));
Map<String, byte[]> headerFieldsAndValues;
try (InputStream inputStream = new FileInputStream(filePath)) {
headerFieldsAndValues = unravelQcow2Header(inputStream, filePath);
} catch (IOException ex) {
throw new RuntimeException(String.format("Unable to validate file [%s] due to: ", filePath), ex);
}
validateQcow2HeaderFields(headerFieldsAndValues, filePath);
LOGGER.info(String.format("[%s] is a valid and allowed QCOW2 file.", filePath));
}
/**
* Unravels the QCOW2 header in a serial fashion, iterating through the {@link Qcow2HeaderField}, reading the fields specified in
* {@link Qcow2Inspector#SET_OF_HEADER_FIELDS_TO_READ} and skipping the others.
* @param qcow2InputStream InputStream of the QCOW2 being unraveled.
* @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions.
* @return A map of the header fields and their values according to the {@link Qcow2Inspector#SET_OF_HEADER_FIELDS_TO_READ}.
* @throws IOException If the field cannot be read or skipped.
*/
public static Map<String, byte[]> unravelQcow2Header(InputStream qcow2InputStream, String qcow2LogReference) throws IOException {
Map<String, byte[]> result = new HashMap<>();
LOGGER.debug(String.format("Unraveling QCOW2 [%s] headers.", qcow2LogReference));
for (Qcow2HeaderField qcow2Header : Qcow2HeaderField.values()) {
if (!SET_OF_HEADER_FIELDS_TO_READ.contains(qcow2Header)) {
skipHeader(qcow2InputStream, qcow2Header, qcow2LogReference);
continue;
}
byte[] headerValue = readHeader(qcow2InputStream, qcow2Header, qcow2LogReference);
result.put(qcow2Header.name(), headerValue);
}
return result;
}
/**
* Skips the field's length in the InputStream.
* @param qcow2InputStream InputStream of the QCOW2 being unraveled.
* @param field Field being skipped (name and length).
* @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions.
* @throws IOException If the bytes skipped do not match the field length.
*/
protected static void skipHeader(InputStream qcow2InputStream, Qcow2HeaderField field, String qcow2LogReference) throws IOException {
LOGGER.trace(String.format("Skipping field [%s] of QCOW2 [%s].", field, qcow2LogReference));
if (qcow2InputStream.skip(field.getLength()) != field.getLength()) {
throw new IOException(String.format("Unable to skip field [%s] of QCOW2 [%s].", field, qcow2LogReference));
}
}
/**
* Reads the field's length in the InputStream.
* @param qcow2InputStream InputStream of the QCOW2 being unraveled.
* @param field Field being read (name and length).
* @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions.
* @throws IOException If the bytes read do not match the field length.
*/
protected static byte[] readHeader(InputStream qcow2InputStream, Qcow2HeaderField field, String qcow2LogReference) throws IOException {
byte[] readBytes = new byte[field.getLength()];
LOGGER.trace(String.format("Reading field [%s] of QCOW2 [%s].", field, qcow2LogReference));
if (qcow2InputStream.read(readBytes) != field.getLength()) {
throw new IOException(String.format("Unable to read field [%s] of QCOW2 [%s].", field, qcow2LogReference));
}
LOGGER.trace(String.format("Read %s as field [%s] of QCOW2 [%s].", ArrayUtils.toString(readBytes), field, qcow2LogReference));
return readBytes;
}
/**
* Validates the values of the header fields {@link Qcow2HeaderField#MAGIC}, {@link Qcow2HeaderField#BACKING_FILE_OFFSET}, and {@link Qcow2HeaderField#INCOMPATIBLE_FEATURES}.
* @param headerFieldsAndValues A map of the header fields and their values.
* @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions.
* @throws SecurityException If the QCOW2 does not contain the QCOW magic string or contains a backing file reference or incompatible features.
*/
public static void validateQcow2HeaderFields(Map<String, byte[]> headerFieldsAndValues, String qcow2LogReference) throws SecurityException{
byte[] fieldValue = headerFieldsAndValues.get(Qcow2HeaderField.MAGIC.name());
validateQcowMagicString(fieldValue, qcow2LogReference);
fieldValue = headerFieldsAndValues.get(Qcow2HeaderField.BACKING_FILE_OFFSET.name());
validateAbsenceOfBackingFileReference(NumbersUtil.bytesToLong(fieldValue), qcow2LogReference);
fieldValue = headerFieldsAndValues.get(Qcow2HeaderField.INCOMPATIBLE_FEATURES.name());
validateAbsenceOfIncompatibleFeatures(fieldValue, qcow2LogReference);
}
/**
* Verifies if the first 4 bytes of the header are the QCOW magic string. Throws an exception if not.
* @param headerMagicString The first 4 bytes of the header.
* @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions.
* @throws SecurityException If the header's magic string is not the QCOW magic string.
*/
private static void validateQcowMagicString(byte[] headerMagicString, String qcow2LogReference) throws SecurityException {
LOGGER.debug(String.format("Verifying if [%s] has a valid QCOW magic string.", qcow2LogReference));
if (!Arrays.equals(QCOW_MAGIC_STRING, headerMagicString)) {
throw new SecurityException(String.format("[%s] is not a valid QCOW2 because its first 4 bytes are not the QCOW magic string.", qcow2LogReference));
}
LOGGER.debug(String.format("[%s] has a valid QCOW magic string.", qcow2LogReference));
}
/**
* Verifies if the QCOW2 has a backing file and throws an exception if so.
* @param backingFileOffset The backing file offset value of the QCOW2 header.
* @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions.
* @throws SecurityException If the QCOW2 has a backing file reference.
*/
private static void validateAbsenceOfBackingFileReference(long backingFileOffset, String qcow2LogReference) throws SecurityException {
LOGGER.debug(String.format("Verifying if [%s] has a backing file reference.", qcow2LogReference));
if (backingFileOffset != 0) {
throw new SecurityException(String.format("[%s] has a backing file reference. This can be an attack to the infrastructure; therefore, we will not accept" +
" this QCOW2.", qcow2LogReference));
}
LOGGER.debug(String.format("[%s] does not have a backing file reference.", qcow2LogReference));
}
/**
* Verifies if the QCOW2 has incompatible features and throw an exception if it has an external data file reference or unknown incompatible features.
* @param incompatibleFeatures The incompatible features bytes of the QCOW2 header.
* @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions.
* @throws SecurityException If the QCOW2 has an external data file reference or unknown incompatible features.
*/
private static void validateAbsenceOfIncompatibleFeatures(byte[] incompatibleFeatures, String qcow2LogReference) throws SecurityException {
LOGGER.debug(String.format("Verifying if [%s] has incompatible features.", qcow2LogReference));
if (NumbersUtil.bytesToLong(incompatibleFeatures) == 0) {
LOGGER.debug(String.format("[%s] does not have incompatible features.", qcow2LogReference));
return;
}
LOGGER.debug(String.format("[%s] has incompatible features.", qcow2LogReference));
validateAbsenceOfExternalDataFileReference(incompatibleFeatures, qcow2LogReference);
validateAbsenceOfUnknownIncompatibleFeatures(incompatibleFeatures, qcow2LogReference);
}
/**
* Verifies if the QCOW2 has an external data file reference and throw an exception if so.
* @param incompatibleFeatures The incompatible features bytes of the QCOW2 header.
* @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions.
* @throws SecurityException If the QCOW2 has an external data file reference.
*/
private static void validateAbsenceOfExternalDataFileReference(byte[] incompatibleFeatures, String qcow2LogReference) throws SecurityException {
LOGGER.debug(String.format("Verifying if [%s] has an external data file reference.", qcow2LogReference));
if ((incompatibleFeatures[EXTERNAL_DATA_FILE_BYTE_POSITION] & EXTERNAL_DATA_FILE_BITMASK) != 0) {
throw new SecurityException(String.format("[%s] has an external data file reference. This can be an attack to the infrastructure; therefore, we will discard" +
" this file.", qcow2LogReference));
}
LOGGER.info(String.format("[%s] does not have an external data file reference.", qcow2LogReference));
}
/**
* Verifies if the QCOW2 has unknown incompatible features and throw an exception if so.
* <br/><br/>
* Unknown incompatible features are those with bit greater than
* {@link Qcow2Inspector#INCOMPATIBLE_FEATURES_MAX_KNOWN_BIT}, which will be the represented by bytes in positions greater than
* {@link Qcow2Inspector#INCOMPATIBLE_FEATURES_MAX_KNOWN_BYTE} (in Big Endian order). Therefore, we expect that those bytes are always zero. If not, an exception is thrown.
* @param incompatibleFeatures The incompatible features bytes of the QCOW2 header.
* @param qcow2LogReference A reference (like the filename) of the QCOW2 being unraveled to print in the logs and exceptions.
* @throws SecurityException If the QCOW2 has unknown incompatible features.
*/
private static void validateAbsenceOfUnknownIncompatibleFeatures(byte[] incompatibleFeatures, String qcow2LogReference) throws SecurityException {
LOGGER.debug(String.format("Verifying if [%s] has unknown incompatible features [%s].", qcow2LogReference, ArrayUtils.toString(incompatibleFeatures)));
for (int byteNum = incompatibleFeatures.length - 1; byteNum >= 0; byteNum--) {
int bytePosition = incompatibleFeatures.length - 1 - byteNum;
LOGGER.trace(String.format("Looking for unknown incompatible feature bit in position [%s].", bytePosition));
byte bitmask = 0;
if (byteNum == INCOMPATIBLE_FEATURES_MAX_KNOWN_BYTE) {
bitmask = ((1 << INCOMPATIBLE_FEATURES_MAX_KNOWN_BIT) - 1);
}
LOGGER.trace(String.format("Bitmask for byte in position [%s] is [%s].", bytePosition, Integer.toBinaryString(bitmask)));
int featureBit = incompatibleFeatures[bytePosition] & ~bitmask;
if (featureBit != 0) {
throw new SecurityException(String.format("Found unknown incompatible feature bit [%s] in byte [%s] of [%s]. This can be an attack to the infrastructure; " +
"therefore, we will discard this QCOW2.", featureBit, bytePosition + Qcow2HeaderField.INCOMPATIBLE_FEATURES.getOffset(), qcow2LogReference));
}
LOGGER.trace(String.format("Did not find unknown incompatible feature in position [%s].", bytePosition));
}
LOGGER.info(String.format("[%s] does not have unknown incompatible features.", qcow2LogReference));
}
}

View File

@ -71,6 +71,7 @@ import org.apache.cloudstack.storage.command.UploadStatusCommand;
import org.apache.cloudstack.storage.command.browser.ListDataStoreObjectsCommand;
import org.apache.cloudstack.storage.configdrive.ConfigDrive;
import org.apache.cloudstack.storage.configdrive.ConfigDriveBuilder;
import org.apache.cloudstack.storage.formatinspector.Qcow2Inspector;
import org.apache.cloudstack.storage.template.DownloadManager;
import org.apache.cloudstack.storage.template.DownloadManagerImpl;
import org.apache.cloudstack.storage.template.UploadEntity;
@ -3482,8 +3483,19 @@ public class NfsSecondaryStorageResource extends ServerResourceBase implements S
return result;
}
String finalFilename = resourcePath + "/" + templateFilename;
if (ImageStoreUtil.isCorrectExtension(finalFilename, "qcow2")) {
try {
Qcow2Inspector.validateQcow2File(finalFilename);
} catch (RuntimeException e) {
s_logger.error(String.format("Uploaded file [%s] is not a valid QCOW2.", finalFilename), e);
return "The uploaded file is not a valid QCOW2. Ask the administrator to check the logs for more details.";
}
}
// Set permissions for the downloaded template
File downloadedTemplate = new File(resourcePath + "/" + templateFilename);
File downloadedTemplate = new File(finalFilename);
_storage.setWorldReadableAndWriteable(downloadedTemplate);
// Set permissions for template/volume.properties

View File

@ -16,8 +16,6 @@
// under the License.
package org.apache.cloudstack.storage.template;
import static com.cloud.utils.NumbersUtil.toHumanReadableSize;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@ -51,9 +49,10 @@ import org.apache.cloudstack.storage.command.DownloadProgressCommand.RequestType
import org.apache.cloudstack.storage.resource.IpTablesHelper;
import org.apache.cloudstack.storage.resource.NfsSecondaryStorageResource;
import org.apache.cloudstack.storage.resource.SecondaryStorageResource;
import org.apache.cloudstack.storage.formatinspector.Qcow2HeaderField;
import org.apache.cloudstack.storage.formatinspector.Qcow2Inspector;
import org.apache.cloudstack.utils.security.ChecksumValue;
import org.apache.cloudstack.utils.security.DigestHelper;
import org.apache.commons.lang3.StringUtils;
import com.cloud.agent.api.storage.DownloadAnswer;
import com.cloud.agent.api.to.DataStoreTO;
@ -89,11 +88,14 @@ import com.cloud.utils.NumbersUtil;
import com.cloud.utils.component.ManagerBase;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.Proxy;
import com.cloud.utils.StringUtils;
import com.cloud.utils.script.Script;
import com.cloud.utils.storage.QCOW2Utils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import static com.cloud.utils.NumbersUtil.toHumanReadableSize;
public class DownloadManagerImpl extends ManagerBase implements DownloadManager {
protected static Logger LOGGER = LogManager.getLogger(DownloadManagerImpl.class);
private String _name;
@ -366,11 +368,17 @@ public class DownloadManagerImpl extends ManagerBase implements DownloadManager
// The QCOW2 is the only format with a header,
// and as such can be easily read.
try (InputStream inputStream = td.getS3ObjectInputStream();) {
dnld.setTemplatesize(QCOW2Utils.getVirtualSize(inputStream, false));
}
catch (IOException e) {
result = "Couldn't read QCOW2 virtual size. Error: " + e.getMessage();
try (InputStream inputStream = td.getS3ObjectInputStream()) {
Map<String, byte[]> qcow2HeaderFieldsAndValues = Qcow2Inspector.unravelQcow2Header(inputStream, td.getDownloadUrl());
Qcow2Inspector.validateQcow2HeaderFields(qcow2HeaderFieldsAndValues, td.getDownloadUrl());
dnld.setTemplatesize(NumbersUtil.bytesToLong(qcow2HeaderFieldsAndValues.get(Qcow2HeaderField.SIZE.name())));
} catch (IOException ex) {
result = String.format("Unable to read QCOW2 metadata. Error: %s", ex.getMessage());
LOGGER.error(result, ex);
} catch (SecurityException ex) {
result = String.format("[%s] is not a valid QCOW2:", td.getDownloadUrl());
LOGGER.error(result, ex);
}
}
@ -516,8 +524,19 @@ public class DownloadManagerImpl extends ManagerBase implements DownloadManager
return result;
}
String finalFilename = resourcePath + "/" + templateFilename;
if (ImageFormat.QCOW2.equals(dnld.getFormat())) {
try {
Qcow2Inspector.validateQcow2File(finalFilename);
} catch (RuntimeException e) {
LOGGER.error(String.format("The downloaded file [%s] is not a valid QCOW2.", finalFilename), e);
return "The downloaded file is not a valid QCOW2. Ask the administrator to check the logs for more details.";
}
}
// Set permissions for the downloaded template
File downloadedTemplate = new File(resourcePath + "/" + templateFilename);
File downloadedTemplate = new File(finalFilename);
_storage.setWorldReadableAndWriteable(downloadedTemplate);
setPermissionsForTheDownloadedTemplate(resourcePath, resourceType);

View File

@ -133,6 +133,7 @@ class TestLogin(cloudstackTestCase):
args["command"] = 'listUsers'
args["listall"] = 'true'
args["response"] = "json"
args["sessionkey"] = response.json()['loginresponse']['sessionkey']
response = session.get(self.server_url, params=args)
self.assertEqual(
response.status_code,

View File

@ -15,8 +15,13 @@
// specific language governing permissions and limitations
// under the License.
import Cookies from 'js-cookie'
import { axios, sourceToken } from '@/utils/request'
import { message, notification } from 'ant-design-vue'
import { vueProps } from '@/vue-app'
import {
ACCESS_TOKEN
} from '@/store/mutation-types'
export function api (command, args = {}, method = 'GET', data = {}) {
let params = {}
@ -30,6 +35,11 @@ export function api (command, args = {}, method = 'GET', data = {}) {
})
}
const sessionkey = vueProps.$localStorage.get(ACCESS_TOKEN) || Cookies.get('sessionkey')
if (sessionkey) {
args.sessionkey = sessionkey
}
return axios({
params: {
...args
@ -65,7 +75,6 @@ export function login (arg) {
}
export function logout () {
sourceToken.cancel()
message.destroy()
notification.destroy()
return api('logout')

View File

@ -444,11 +444,6 @@ const user = {
cloudianUrl = state.cloudian.url + 'logout.htm?redirect=' + encodeURIComponent(window.location.href)
}
Object.keys(Cookies.get()).forEach(cookieName => {
Cookies.remove(cookieName)
Cookies.remove(cookieName, { path: '/client' })
})
commit('SET_TOKEN', '')
commit('SET_APIS', {})
commit('SET_PROJECT', {})
@ -476,6 +471,11 @@ const user = {
}
}).catch(() => {
resolve()
}).finally(() => {
Object.keys(Cookies.get()).forEach(cookieName => {
Cookies.remove(cookieName)
Cookies.remove(cookieName, { path: '/client' })
})
})
})
},

View File

@ -84,7 +84,7 @@
}"
:options="groups.opts" />
</a-form-item>
<a-form-item>
<a-form-item v-if="userDataEnabled">
<template #label>
<tooltip-label :title="$t('label.userdata')" :tooltip="apiParams.userdata.description"/>
</template>
@ -150,6 +150,7 @@ export default {
return {
serviceOffering: {},
template: {},
userDataEnabled: false,
securityGroupsEnabled: false,
dynamicScalingVmConfig: false,
loading: false,
@ -297,15 +298,37 @@ export default {
return decodedData.toString('utf-8')
},
fetchUserData () {
const params = {
id: this.resource.id,
userdata: true
let networkId
this.resource.nic.forEach(nic => {
if (nic.isdefault) {
networkId = nic.networkid
}
})
if (!networkId) {
return
}
const listNetworkParams = {
id: networkId,
listall: true
}
api(`listNetworks`, listNetworkParams).then(json => {
json.listnetworksresponse.network[0].service.forEach(service => {
if (service.name === 'UserData') {
this.userDataEnabled = true
api('listVirtualMachines', params).then(json => {
this.form.userdata = this.decodeUserData(json.listvirtualmachinesresponse.virtualmachine[0].userdata || '')
const listVmParams = {
id: this.resource.id,
userdata: true,
listall: true
}
api('listVirtualMachines', listVmParams).then(json => {
this.form.userdata = atob(json.listvirtualmachinesresponse.virtualmachine[0].userdata || '')
})
}
})
})
},
handleSubmit () {
this.formRef.value.validate().then(() => {
const values = toRaw(this.form)

View File

@ -800,12 +800,12 @@ export default {
this.formRef.value.validate().then(() => {
const values = toRaw(this.form)
api('createVpnConnection', {
s2svpngatewayid: this.vpnGateways[0].id,
const params = {
s2svpngatewayid: this.vpnGateways[0] ? this.vpnGateways[0].id : null,
s2scustomergatewayid: values.vpncustomergateway,
passive: values.passive ? values.passive : false
}).then(response => {
}
api('createVpnConnection', params).then(response => {
this.$pollJob({
jobId: response.createvpnconnectionresponse.jobid,
title: this.$t('label.vpn.connection'),

View File

@ -142,7 +142,11 @@ const vueConfig = {
secure: false,
ws: false,
changeOrigin: true,
proxyTimeout: 10 * 60 * 1000 // 10 minutes
proxyTimeout: 10 * 60 * 1000, // 10 minutes
cookieDomainRewrite: '*',
cookiePathRewrite: {
'/client': '/'
}
}
},
https: process.env.HTTPS_KEY ? {

View File

@ -38,6 +38,14 @@ public class HttpUtils {
public static final String JSON_CONTENT_TYPE = "application/json; charset=UTF-8";
public static final String XML_CONTENT_TYPE = "text/xml; charset=UTF-8";
public enum ApiSessionKeySameSite {
Lax, Strict, NoneAndSecure, Null
}
public enum ApiSessionKeyCheckOption {
CookieOrParameter, ParameterOnly, CookieAndParameter
}
public static void addSecurityHeaders(final HttpServletResponse resp) {
if (resp.containsHeader("X-Content-Type-Options")) {
resp.setHeader("X-Content-Type-Options", "nosniff");
@ -104,23 +112,43 @@ public class HttpUtils {
return null;
}
public static boolean validateSessionKey(final HttpSession session, final Map<String, Object[]> params, final Cookie[] cookies, final String sessionKeyString) {
public static boolean validateSessionKey(final HttpSession session, final Map<String, Object[]> params, final Cookie[] cookies, final String sessionKeyString, final ApiSessionKeyCheckOption apiSessionKeyCheckLocations) {
if (session == null || sessionKeyString == null) {
return false;
}
final String jsessionidFromCookie = HttpUtils.findCookie(cookies, "JSESSIONID");
if (jsessionidFromCookie == null
|| !(jsessionidFromCookie.startsWith(session.getId() + '.'))) {
s_logger.error("JSESSIONID from cookie is invalid.");
return false;
}
final String sessionKey = (String) session.getAttribute(sessionKeyString);
if (sessionKey == null) {
s_logger.error("sessionkey attribute of the session is null.");
return false;
}
final String sessionKeyFromCookie = HttpUtils.findCookie(cookies, sessionKeyString);
boolean isSessionKeyFromCookieValid = sessionKeyFromCookie != null && sessionKey.equals(sessionKeyFromCookie);
String[] sessionKeyFromParams = null;
if (params != null) {
sessionKeyFromParams = (String[]) params.get(sessionKeyString);
}
if ((sessionKey == null)
|| (sessionKeyFromParams == null && sessionKeyFromCookie == null)
|| (sessionKeyFromParams != null && !sessionKey.equals(sessionKeyFromParams[0]))
|| (sessionKeyFromCookie != null && !sessionKey.equals(sessionKeyFromCookie))) {
return false;
boolean isSessionKeyFromParamsValid = sessionKeyFromParams != null && sessionKey.equals(sessionKeyFromParams[0]);
switch (apiSessionKeyCheckLocations) {
case CookieOrParameter:
return (sessionKeyFromCookie != null || sessionKeyFromParams != null)
&& (sessionKeyFromCookie == null || isSessionKeyFromCookieValid)
&& (sessionKeyFromParams == null || isSessionKeyFromParamsValid);
case ParameterOnly:
return sessionKeyFromParams != null && isSessionKeyFromParamsValid
&& (sessionKeyFromCookie == null || isSessionKeyFromCookieValid);
case CookieAndParameter:
default:
return sessionKeyFromCookie != null && isSessionKeyFromCookieValid
&& sessionKeyFromParams != null && isSessionKeyFromParamsValid;
}
return true;
}
}

View File

@ -60,35 +60,97 @@ public class HttpUtilsTest {
final String sessionKeyValue = "randomUniqueSessionID";
// session and sessionKeyString null test
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
sessionKeyString = "sessionkey";
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// param and cookie null test
session = new MockHttpSession();
final String sessionId = session.getId();
session.setAttribute(sessionKeyString, sessionKeyValue);
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// param null, cookies not null test
// param null, cookies not null test (JSESSIONID is null)
params = null;
cookies = new Cookie[]{new Cookie(sessionKeyString, sessionKeyValue)};
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, "randomString"));
assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, "randomString", HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// param null, cookies not null test (JSESSIONID is not null and matches)
cookies = new Cookie[2];
cookies[0] = new Cookie(sessionKeyString, sessionKeyValue);
cookies[1] = new Cookie("JSESSIONID", sessionId + ".node0");
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, "randomString", HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// param null, cookies not null test (JSESSIONID is not null but mismatches)
cookies = new Cookie[2];
cookies[0] = new Cookie(sessionKeyString, sessionKeyValue);
cookies[1] = new Cookie("JSESSIONID", "node0xxxxxxxxxxxxx.node0");
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, "randomString", HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// param not null, cookies null test
params = new HashMap<String, Object[]>();
params.put(sessionKeyString, new String[]{"randomString"});
cookies = null;
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
params.put(sessionKeyString, new String[]{sessionKeyValue});
assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// both param and cookies not null test
// both param and cookies not null test (JSESSIONID is null)
params = new HashMap<String, Object[]>();
cookies = new Cookie[]{new Cookie(sessionKeyString, sessionKeyValue)};
cookies = new Cookie[2];
cookies[0] = new Cookie(sessionKeyString, sessionKeyValue);
params.put(sessionKeyString, new String[]{"incorrectValue"});
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
params.put(sessionKeyString, new String[]{sessionKeyValue});
assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// both param and cookies not null test (JSESSIONID is not null but mismatches)
params = new HashMap<String, Object[]>();
cookies = new Cookie[2];
cookies[0] = new Cookie(sessionKeyString, sessionKeyValue);
cookies[1] = new Cookie("JSESSIONID", "node0xxxxxxxxxxxxx.node0");
params.put(sessionKeyString, new String[]{"incorrectValue"});
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
params.put(sessionKeyString, new String[]{sessionKeyValue});
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// both param and cookies not null test (JSESSIONID is not null amd matches)
params = new HashMap<String, Object[]>();
cookies = new Cookie[2];
cookies[0] = new Cookie(sessionKeyString, sessionKeyValue);
cookies[1] = new Cookie("JSESSIONID", sessionId + ".node0");
params.put(sessionKeyString, new String[]{"incorrectValue"});
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
params.put(sessionKeyString, new String[]{sessionKeyValue});
assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
// param not null, cookies null test (JSESSIONID is not null amd matches)
params = new HashMap<String, Object[]>();
cookies = new Cookie[1];
cookies[0] = new Cookie("JSESSIONID", sessionId + ".node0");
params.put(sessionKeyString, new String[]{"incorrectValue"});
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.ParameterOnly));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieAndParameter));
params.put(sessionKeyString, new String[]{sessionKeyValue});
assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.ParameterOnly));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieAndParameter));
// param not null (correct), cookies not null test (correct)
cookies = new Cookie[2];
cookies[0] = new Cookie(sessionKeyString, sessionKeyValue);
cookies[1] = new Cookie("JSESSIONID", sessionId + ".node0");
assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.ParameterOnly));
assertTrue(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieAndParameter));
// param not null (correct), cookies not null test (wrong)
cookies = new Cookie[2];
cookies[0] = new Cookie(sessionKeyString, "incorrectValue");
cookies[1] = new Cookie("JSESSIONID", sessionId + ".node0");
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieOrParameter));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.ParameterOnly));
assertFalse(HttpUtils.validateSessionKey(session, params, cookies, sessionKeyString, HttpUtils.ApiSessionKeyCheckOption.CookieAndParameter));
}
}