| // Licensed to the Apache Software Foundation (ASF) under one | |
| // or more contributor license agreements. See the NOTICE file | |
| // distributed with this work for additional information | |
| // regarding copyright ownership. The ASF licenses this file | |
| // to you under the Apache License, Version 2.0 (the | |
| // "License"); you may not use this file except in compliance | |
| // with the License. You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, | |
| // software distributed under the License is distributed on an | |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| // KIND, either express or implied. See the License for the | |
| // specific language governing permissions and limitations | |
| // under the License. | |
| package com.cloud.api.dispatch; | |
| import java.lang.reflect.Field; | |
| import java.text.DateFormat; | |
| import java.text.ParseException; | |
| import java.text.SimpleDateFormat; | |
| import java.util.ArrayList; | |
| import java.util.Calendar; | |
| import java.util.Date; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.StringTokenizer; | |
| import java.util.regex.Matcher; | |
| import javax.inject.Inject; | |
| import org.apache.cloudstack.acl.SecurityChecker; | |
| import org.apache.cloudstack.acl.SecurityChecker.AccessType; | |
| import org.apache.cloudstack.api.ACL; | |
| import org.apache.cloudstack.api.ApiArgValidator; | |
| import org.apache.cloudstack.api.ApiCommandResourceType; | |
| import org.apache.cloudstack.api.ApiErrorCode; | |
| import org.apache.cloudstack.api.BaseAsyncCmd; | |
| import org.apache.cloudstack.api.BaseAsyncCreateCmd; | |
| import org.apache.cloudstack.api.BaseCmd; | |
| import org.apache.cloudstack.api.BaseCmd.CommandType; | |
| import org.apache.cloudstack.api.EntityReference; | |
| import org.apache.cloudstack.api.InternalIdentity; | |
| import org.apache.cloudstack.api.Parameter; | |
| import org.apache.cloudstack.api.ServerApiException; | |
| import org.apache.cloudstack.context.CallContext; | |
| import org.apache.commons.lang3.StringUtils; | |
| import org.apache.logging.log4j.Logger; | |
| import org.apache.logging.log4j.LogManager; | |
| import com.cloud.exception.InvalidParameterValueException; | |
| import com.cloud.user.Account; | |
| import com.cloud.user.AccountManager; | |
| import com.cloud.utils.DateUtil; | |
| import com.cloud.utils.UuidUtils; | |
| import com.cloud.utils.db.EntityManager; | |
| import com.cloud.utils.exception.CloudRuntimeException; | |
| import com.cloud.utils.net.NetUtils; | |
| public class ParamProcessWorker implements DispatchWorker { | |
| protected Logger logger = LogManager.getLogger(getClass()); | |
| private static final String inputFormatString = "yyyy-MM-dd"; | |
| private static final String newInputFormatString = "yyyy-MM-dd HH:mm:ss"; | |
| public static final DateFormat inputFormat = new SimpleDateFormat(inputFormatString); | |
| public static final DateFormat newInputFormat = new SimpleDateFormat(newInputFormatString); | |
| @Inject | |
| protected AccountManager _accountMgr; | |
| @Inject | |
| protected EntityManager _entityMgr; | |
| List<SecurityChecker> _secChecker; | |
| public List<SecurityChecker> getSecChecker() { | |
| return _secChecker; | |
| } | |
| @Inject | |
| public void setSecChecker(List<SecurityChecker> secChecker) { | |
| _secChecker = secChecker; | |
| } | |
| @Override | |
| public void handle(final DispatchTask task) { | |
| processParameters(task.getCmd(), task.getParams()); | |
| } | |
| private void validateNonEmptyString(final Object param, final String argName) { | |
| if (param == null || StringUtils.isEmpty(param.toString())) { | |
| throwInvalidParameterValueException(argName); | |
| } | |
| } | |
| private void validateNaturalNumber(final Object param, final String argName) { | |
| Long value = null; | |
| if (param != null && param instanceof Long) { | |
| value = (Long) param; | |
| } else if (param != null) { | |
| value = Long.valueOf(param.toString()); | |
| } | |
| if (value == null || value < 1L) { | |
| throwInvalidParameterValueException(argName); | |
| } | |
| } | |
| private void validateUuidString(final Object param, final String argName) { | |
| String value = String.valueOf(param); | |
| if (!UuidUtils.isUuid(value)) { | |
| throwInvalidParameterValueException(argName); | |
| } | |
| } | |
| private void validateNameForRFCCompliance(final Object param, final String argName) { | |
| String value = String.valueOf(param); | |
| if (StringUtils.isBlank(value) || !NetUtils.verifyDomainNameLabel(value, true)) { | |
| String msg = "it can contain ASCII letters 'a' through 'z', the digits '0' through '9', " | |
| + "and the hyphen ('-'), must be between 1 and 63 characters long, and can't start or end with \"-\" and can't start with digit"; | |
| throwInvalidParameterValueException(argName, msg); | |
| } | |
| } | |
| protected void throwInvalidParameterValueException(String argName) { | |
| throwInvalidParameterValueException(argName, null); | |
| } | |
| protected void throwInvalidParameterValueException(String argName, String customMsg) { | |
| throw new InvalidParameterValueException(String.format("Invalid value provided for API arg: %s%s", argName, StringUtils.isBlank(customMsg)? "" : " - " + customMsg)); | |
| } | |
| private void validateField(final Object paramObj, final Parameter annotation) throws ServerApiException { | |
| if (annotation == null) { | |
| return; | |
| } | |
| final String argName = annotation.name(); | |
| for (final ApiArgValidator validator : annotation.validations()) { | |
| if (validator == null) { | |
| continue; | |
| } | |
| switch (validator) { | |
| case NotNullOrEmpty: | |
| switch (annotation.type()) { | |
| case UUID: | |
| case STRING: | |
| validateNonEmptyString(paramObj, argName); | |
| break; | |
| } | |
| break; | |
| case PositiveNumber: | |
| switch (annotation.type()) { | |
| case SHORT: | |
| case INTEGER: | |
| case LONG: | |
| validateNaturalNumber(paramObj, argName); | |
| break; | |
| } | |
| break; | |
| case UuidString: | |
| switch (annotation.type()) { | |
| case STRING: | |
| validateUuidString(paramObj, argName); | |
| break; | |
| } | |
| break; | |
| case RFCComplianceDomainName: | |
| switch (annotation.type()) { | |
| case STRING: | |
| validateNameForRFCCompliance(paramObj, argName); | |
| break; | |
| } | |
| break; | |
| case SafeCommandOptions: | |
| validator.validate(paramObj, annotation); | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| } | |
| @SuppressWarnings({"unchecked", "rawtypes"}) | |
| public void processParameters(final BaseCmd cmd, final Map params) { | |
| final Map<Object, AccessType> entitiesToAccess = new HashMap<Object, AccessType>(); | |
| final List<Field> cmdFields = cmd.getParamFields(); | |
| String commandName = cmd.getCommandName(); | |
| if (commandName.endsWith(BaseCmd.RESPONSE_SUFFIX)) { | |
| commandName = cmd.getCommandName().substring(0, cmd.getCommandName().length() - 8); | |
| } | |
| for (final Field field : cmdFields) { | |
| final Parameter parameterAnnotation = field.getAnnotation(Parameter.class); | |
| final Object paramObj = params.get(parameterAnnotation.name()); | |
| if (paramObj == null) { | |
| if (parameterAnnotation.required()) { | |
| throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to execute API command " + | |
| commandName + " due to missing parameter " + parameterAnnotation.name()); | |
| } | |
| continue; | |
| } | |
| if (parameterAnnotation.required()){ | |
| validateNonEmptyString(paramObj, parameterAnnotation.name()); | |
| } | |
| // marshall the parameter into the correct type and set the field value | |
| try { | |
| validateField(paramObj, parameterAnnotation); | |
| setFieldValue(field, cmd, paramObj, parameterAnnotation); | |
| } catch (final IllegalArgumentException argEx) { | |
| if (logger.isDebugEnabled()) { | |
| logger.debug("Unable to execute API command " + commandName + " due to invalid value " + paramObj + " for parameter " + | |
| parameterAnnotation.name()); | |
| } | |
| throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to execute API command " + | |
| commandName + " due to invalid value " + paramObj + " for parameter " + | |
| parameterAnnotation.name()); | |
| } catch (final ParseException parseEx) { | |
| if (logger.isDebugEnabled()) { | |
| logger.debug("Invalid date parameter " + paramObj + " passed to command " + commandName); | |
| } | |
| throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to parse date " + paramObj + " for command " + | |
| commandName + ", please pass dates in the format mentioned in the api documentation"); | |
| } catch (final InvalidParameterValueException invEx) { | |
| throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Unable to execute API command " + | |
| commandName + " due to invalid value. " + invEx.getMessage()); | |
| } catch (final CloudRuntimeException cloudEx) { | |
| logger.error("CloudRuntimeException", cloudEx); | |
| // FIXME: Better error message? This only happens if the API command is not executable, which typically | |
| //means | |
| // there was | |
| // and IllegalAccessException setting one of the parameters. | |
| throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Internal error executing API command " + commandName); | |
| } | |
| //check access on the resource this field points to | |
| try { | |
| final ACL checkAccess = field.getAnnotation(ACL.class); | |
| final CommandType fieldType = parameterAnnotation.type(); | |
| if (checkAccess != null) { | |
| // Verify that caller can perform actions in behalf of vm | |
| // owner acumulate all Controlled Entities together. | |
| // parse the array of resource types and in case of map | |
| // check access on key or value or both as specified in @acl | |
| // implement external dao for classes that need findByName | |
| // for maps, specify access to be checkd on key or value. | |
| // Find the controlled entity DBid by uuid | |
| if (parameterAnnotation.entityType() != null && parameterAnnotation.entityType().length > 0 | |
| && parameterAnnotation.entityType()[0].getAnnotation(EntityReference.class) != null) { | |
| final Class<?>[] entityList = parameterAnnotation.entityType()[0].getAnnotation(EntityReference.class).value(); | |
| // Check if the parameter type is a single | |
| // Id or list of id's/name's | |
| switch (fieldType) { | |
| case LIST: | |
| final CommandType listType = parameterAnnotation.collectionType(); | |
| switch (listType) { | |
| case LONG: | |
| case UUID: | |
| final List<Long> listParam = (List<Long>) field.get(cmd); | |
| for (final Long entityId : listParam) { | |
| for (final Class entity : entityList) { | |
| final Object entityObj = _entityMgr.findById(entity, entityId); | |
| if(entityObj != null){ | |
| entitiesToAccess.put(entityObj, checkAccess.accessType()); | |
| break; | |
| } | |
| } | |
| } | |
| break; | |
| /* | |
| * case STRING: List<String> listParam = new | |
| * ArrayList<String>(); listParam = | |
| * (List)field.get(cmd); for(String entityName: | |
| * listParam){ ControlledEntity entityObj = | |
| * (ControlledEntity )daoClassInstance(entityId); | |
| * entitiesToAccess.add(entityObj); } break; | |
| */ | |
| default: | |
| break; | |
| } | |
| break; | |
| case LONG: | |
| case UUID: | |
| for (final Class entity : entityList) { | |
| final Object entityObj = _entityMgr.findById(entity, (Long) field.get(cmd)); | |
| if(entityObj != null){ | |
| entitiesToAccess.put(entityObj, checkAccess.accessType()); | |
| break; | |
| } | |
| } | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| } | |
| } catch (final IllegalArgumentException e) { | |
| throw new CloudRuntimeException("Internal error initializing parameters for command " + cmd.getCommandName() + " [field " + field.getName() + | |
| " is not accessible]", e); | |
| } catch (final IllegalAccessException e) { | |
| throw new CloudRuntimeException("Internal error initializing parameters for command " + cmd.getCommandName() + " [field " + field.getName() + | |
| " is not accessible]", e); | |
| } | |
| } | |
| doAccessChecks(cmd, entitiesToAccess); | |
| } | |
| protected void doAccessChecks(BaseCmd cmd, Map<Object, AccessType> entitiesToAccess) { | |
| Account caller = CallContext.current().getCallingAccount(); | |
| List<Long> entityOwners = cmd.getEntityOwnerIds(); | |
| Account[] owners = null; | |
| if (entityOwners != null) { | |
| owners = entityOwners.stream().map(id -> _accountMgr.getAccount(id)).toArray(Account[]::new); | |
| } else { | |
| if (cmd.getEntityOwnerId() == Account.ACCOUNT_ID_SYSTEM && cmd instanceof BaseAsyncCmd && ((BaseAsyncCmd)cmd).getApiResourceType() == ApiCommandResourceType.Network) { | |
| if (logger.isDebugEnabled()) { | |
| logger.debug("Skipping access check on the network owner if the owner is ROOT/system."); | |
| } | |
| owners = new Account[]{}; | |
| } else { | |
| owners = new Account[]{_accountMgr.getAccount(cmd.getEntityOwnerId())}; | |
| } | |
| } | |
| if (cmd instanceof BaseAsyncCreateCmd) { | |
| // check that caller can access the owner account. | |
| _accountMgr.checkAccess(caller, null, false, owners); | |
| } | |
| 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"}) | |
| private void setFieldValue(final Field field, final BaseCmd cmdObj, final Object paramObj, final Parameter annotation) throws IllegalArgumentException, ParseException { | |
| try { | |
| field.setAccessible(true); | |
| final CommandType fieldType = annotation.type(); | |
| switch (fieldType) { | |
| case BOOLEAN: | |
| field.set(cmdObj, Boolean.valueOf(paramObj.toString())); | |
| break; | |
| case DATE: | |
| parseAndSetDate(field, cmdObj, paramObj); | |
| break; | |
| case FLOAT: | |
| // Assuming that the parameters have been checked for required before now, | |
| // we ignore blank or null values and defer to the command to set a default | |
| // value for optional parameters ... | |
| if (paramObj != null && StringUtils.isNotBlank(paramObj.toString())) { | |
| field.set(cmdObj, Float.valueOf(paramObj.toString())); | |
| } | |
| break; | |
| case DOUBLE: | |
| // Assuming that the parameters have been checked for required before now, | |
| // we ignore blank or null values and defer to the command to set a default | |
| // value for optional parameters ... | |
| if (paramObj != null && StringUtils.isNotBlank(paramObj.toString())) { | |
| field.set(cmdObj, Double.valueOf(paramObj.toString())); | |
| } | |
| break; | |
| case INTEGER: | |
| // Assuming that the parameters have been checked for required before now, | |
| // we ignore blank or null values and defer to the command to set a default | |
| // value for optional parameters ... | |
| if (paramObj != null && StringUtils.isNotBlank(paramObj.toString())) { | |
| field.set(cmdObj, Integer.valueOf(paramObj.toString())); | |
| } | |
| break; | |
| case LIST: | |
| final List listParam = new ArrayList(); | |
| final StringTokenizer st = new StringTokenizer(paramObj.toString(), ","); | |
| while (st.hasMoreTokens()) { | |
| final String token = st.nextToken(); | |
| final CommandType listType = annotation.collectionType(); | |
| switch (listType) { | |
| case INTEGER: | |
| listParam.add(Integer.valueOf(token)); | |
| break; | |
| case UUID: | |
| if (token.isEmpty()) | |
| break; | |
| final Long internalId = translateUuidToInternalId(token, annotation); | |
| listParam.add(internalId); | |
| break; | |
| case LONG: { | |
| listParam.add(Long.valueOf(token)); | |
| } | |
| break; | |
| case SHORT: | |
| listParam.add(Short.valueOf(token)); | |
| break; | |
| case STRING: | |
| listParam.add(token); | |
| break; | |
| } | |
| } | |
| field.set(cmdObj, listParam); | |
| break; | |
| case UUID: | |
| final Long internalId = translateUuidToInternalId(paramObj.toString(), annotation); | |
| field.set(cmdObj, internalId); | |
| break; | |
| case LONG: | |
| field.set(cmdObj, Long.valueOf(paramObj.toString())); | |
| break; | |
| case SHORT: | |
| field.set(cmdObj, Short.valueOf(paramObj.toString())); | |
| break; | |
| case STRING: | |
| if ((paramObj != null)) { | |
| if (paramObj.toString().length() > annotation.length()) { | |
| logger.error("Value greater than max allowed length " + annotation.length() + " for param: " + field.getName()); | |
| throw new InvalidParameterValueException("Value greater than max allowed length " + annotation.length() + " for param: " + field.getName()); | |
| } else { | |
| field.set(cmdObj, paramObj.toString()); | |
| } | |
| } | |
| break; | |
| case MAP: | |
| default: | |
| field.set(cmdObj, paramObj); | |
| break; | |
| } | |
| } catch (final IllegalAccessException ex) { | |
| logger.error("Error initializing command " + cmdObj.getCommandName() + ", field " + field.getName() + " is not accessible."); | |
| throw new CloudRuntimeException("Internal error initializing parameters for command " + cmdObj.getCommandName() + " [field " + field.getName() + | |
| " is not accessible]"); | |
| } | |
| } | |
| private void parseAndSetDate(Field field, BaseCmd cmdObj, Object paramObj) throws IllegalAccessException, ParseException { | |
| try { | |
| field.set(cmdObj, DateUtil.parseTZDateString(paramObj.toString())); | |
| return; | |
| } catch (ParseException parseException) { | |
| logger.debug(String.format("Could not parse date [%s] with timezone parser, trying to parse without timezone.", paramObj)); | |
| } | |
| if (isObjInNewDateFormat(paramObj.toString())) { | |
| logger.debug(String.format("Parsing date [%s] using the [%s] format.", paramObj, newInputFormatString)); | |
| final DateFormat newFormat = newInputFormat; | |
| synchronized (newFormat) { | |
| field.set(cmdObj, newFormat.parse(paramObj.toString())); | |
| } | |
| } else { | |
| logger.debug(String.format("Parsing date [%s] using the [%s] format.", paramObj, inputFormatString)); | |
| final DateFormat format = inputFormat; | |
| synchronized (format) { | |
| Date date = format.parse(paramObj.toString()); | |
| if (field.getName().equals("startDate")) { | |
| date = messageDate(date, 0, 0, 0); | |
| } else if (field.getName().equals("endDate")) { | |
| date = messageDate(date, 23, 59, 59); | |
| } | |
| field.set(cmdObj, date); | |
| } | |
| } | |
| } | |
| private boolean isObjInNewDateFormat(final String string) { | |
| final Matcher matcher = BaseCmd.newInputDateFormat.matcher(string); | |
| return matcher.matches(); | |
| } | |
| private Date messageDate(final Date date, final int hourOfDay, final int minute, final int second) { | |
| final Calendar cal = Calendar.getInstance(); | |
| cal.setTime(date); | |
| cal.set(Calendar.HOUR_OF_DAY, hourOfDay); | |
| cal.set(Calendar.MINUTE, minute); | |
| cal.set(Calendar.SECOND, second); | |
| return cal.getTime(); | |
| } | |
| private Long translateUuidToInternalId(final String uuid, final Parameter annotation) { | |
| if (uuid.equals("-1")) { | |
| // FIXME: This is to handle a lot of hardcoded special cases where -1 is sent | |
| // APITODO: Find and get rid of all hardcoded params in API Cmds and service layer | |
| return -1L; | |
| } | |
| Long internalId = null; | |
| // If annotation's empty, the cmd existed before 3.x try conversion to long | |
| final boolean isPre3x = annotation.since().isEmpty(); | |
| // Match against Java's UUID regex to check if input is uuid string | |
| final boolean isUuid = UuidUtils.isUuid(uuid); | |
| // Enforce that it's uuid for newly added apis from version 3.x | |
| if (!isPre3x && !isUuid) | |
| return null; | |
| // There may be multiple entities defined on the @EntityReference of a Response.class | |
| // UUID CommandType would expect only one entityType, so use the first entityType | |
| final Class<?>[] entities = annotation.entityType()[0].getAnnotation(EntityReference.class).value(); | |
| // Allow both uuid and internal id for pre3x apis | |
| if (isPre3x && !isUuid) { | |
| try { | |
| internalId = Long.parseLong(uuid); | |
| } catch (final NumberFormatException e) { | |
| internalId = null; | |
| } | |
| if (internalId != null){ | |
| // Populate CallContext for each of the entity. | |
| for (final Class<?> entity : entities) { | |
| CallContext.current().putContextParameter(entity, internalId); | |
| } | |
| validateNaturalNumber(internalId, annotation.name()); | |
| return internalId; | |
| } | |
| } | |
| // Go through each entity which is an interface to a VO class and get a VO object | |
| // Try to getId() for the object using reflection, break on first non-null value | |
| for (final Class<?> entity : entities) { | |
| // For backward compatibility, we search within removed entities and let service layer deal | |
| // with removed ones, return empty response or error | |
| final Object objVO = _entityMgr.findByUuidIncludingRemoved(entity, uuid); | |
| if (objVO == null) { | |
| continue; | |
| } | |
| // Invoke the getId method, get the internal long ID | |
| // If that fails hide exceptions as the uuid may not exist s | |
| try { | |
| internalId = ((InternalIdentity)objVO).getId(); | |
| } catch (final IllegalArgumentException e) { | |
| } catch (final NullPointerException e) { | |
| } | |
| // Return on first non-null Id for the uuid entity | |
| if (internalId != null){ | |
| CallContext.current().putContextParameter(entity, uuid); | |
| break; | |
| } | |
| } | |
| if (internalId == null) { | |
| if (logger.isDebugEnabled()) | |
| logger.debug("Object entity uuid = " + uuid + " does not exist in the database."); | |
| throw new InvalidParameterValueException("Invalid parameter " + annotation.name() + " value=" + uuid + | |
| " due to incorrect long value format, or entity does not exist or due to incorrect parameter annotation for the field in api cmd class."); | |
| } | |
| validateNaturalNumber(internalId, annotation.name()); | |
| return internalId; | |
| } | |
| } |