| // 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.discovery; |
| |
| import java.lang.reflect.Field; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import javax.inject.Inject; |
| |
| import org.apache.cloudstack.acl.APIChecker; |
| import org.apache.cloudstack.api.APICommand; |
| import org.apache.cloudstack.api.BaseAsyncCmd; |
| import org.apache.cloudstack.api.BaseAsyncCreateCmd; |
| import org.apache.cloudstack.api.BaseCmd; |
| import org.apache.cloudstack.api.BaseResponse; |
| import org.apache.cloudstack.api.Parameter; |
| import org.apache.cloudstack.acl.Role; |
| import org.apache.cloudstack.acl.RoleService; |
| import org.apache.cloudstack.acl.RoleType; |
| import org.apache.cloudstack.api.command.user.discovery.ListApisCmd; |
| import org.apache.cloudstack.api.response.ApiDiscoveryResponse; |
| import org.apache.cloudstack.api.response.ApiParameterResponse; |
| import org.apache.cloudstack.api.response.ApiResponseResponse; |
| import org.apache.cloudstack.api.response.ListResponse; |
| import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToStringBuilderUtils; |
| import org.apache.commons.lang3.StringUtils; |
| import org.reflections.ReflectionUtils; |
| import org.springframework.stereotype.Component; |
| |
| import com.cloud.exception.PermissionDeniedException; |
| import com.cloud.serializer.Param; |
| import com.cloud.user.Account; |
| import com.cloud.user.AccountService; |
| import com.cloud.user.User; |
| import com.cloud.utils.ReflectUtil; |
| import com.cloud.utils.component.ComponentLifecycleBase; |
| import com.cloud.utils.component.PluggableService; |
| import com.google.gson.annotations.SerializedName; |
| |
| @Component |
| public class ApiDiscoveryServiceImpl extends ComponentLifecycleBase implements ApiDiscoveryService { |
| |
| List<APIChecker> _apiAccessCheckers = null; |
| List<PluggableService> _services = null; |
| protected static Map<String, ApiDiscoveryResponse> s_apiNameDiscoveryResponseMap = null; |
| |
| @Inject |
| AccountService accountService; |
| |
| @Inject |
| RoleService roleService; |
| |
| protected ApiDiscoveryServiceImpl() { |
| super(); |
| } |
| |
| @Override |
| public boolean start() { |
| if (s_apiNameDiscoveryResponseMap == null) { |
| long startTime = System.nanoTime(); |
| s_apiNameDiscoveryResponseMap = new HashMap<String, ApiDiscoveryResponse>(); |
| Set<Class<?>> cmdClasses = new LinkedHashSet<Class<?>>(); |
| for (PluggableService service : _services) { |
| logger.debug(String.format("getting api commands of service: %s", service.getClass().getName())); |
| cmdClasses.addAll(service.getCommands()); |
| } |
| cmdClasses.addAll(this.getCommands()); |
| cacheResponseMap(cmdClasses); |
| long endTime = System.nanoTime(); |
| logger.info("Api Discovery Service: Annotation, docstrings, api relation graph processed in " + (endTime - startTime) / 1000000.0 + " ms"); |
| } |
| |
| return true; |
| } |
| |
| protected Map<String, List<String>> cacheResponseMap(Set<Class<?>> cmdClasses) { |
| Map<String, List<String>> responseApiNameListMap = new HashMap<String, List<String>>(); |
| |
| for (Class<?> cmdClass : cmdClasses) { |
| APICommand apiCmdAnnotation = cmdClass.getAnnotation(APICommand.class); |
| if (apiCmdAnnotation == null) { |
| apiCmdAnnotation = cmdClass.getSuperclass().getAnnotation(APICommand.class); |
| } |
| if (apiCmdAnnotation == null || !apiCmdAnnotation.includeInApiDoc() || apiCmdAnnotation.name().isEmpty()) { |
| continue; |
| } |
| |
| String apiName = apiCmdAnnotation.name(); |
| if (logger.isTraceEnabled()) { |
| logger.trace("Found api: " + apiName); |
| } |
| ApiDiscoveryResponse response = getCmdRequestMap(cmdClass, apiCmdAnnotation); |
| |
| Class<? extends BaseResponse> responseClass = apiCmdAnnotation.responseObject(); |
| String responseName = responseClass.getName(); |
| if (!responseName.contains("SuccessResponse")) { |
| if (!responseApiNameListMap.containsKey(responseName)) { |
| responseApiNameListMap.put(responseName, new ArrayList<String>()); |
| } |
| responseApiNameListMap.get(responseName).add(apiName); |
| } |
| response.setRelated(responseName); |
| |
| Set<Field> responseFields = ReflectionUtils.getAllFields(responseClass); |
| for (Field responseField : responseFields) { |
| ApiResponseResponse responseResponse = getFieldResponseMap(responseField); |
| response.addApiResponse(responseResponse); |
| } |
| |
| response.setObjectName("api"); |
| s_apiNameDiscoveryResponseMap.put(apiName, response); |
| } |
| |
| for (String apiName : s_apiNameDiscoveryResponseMap.keySet()) { |
| ApiDiscoveryResponse response = s_apiNameDiscoveryResponseMap.get(apiName); |
| Set<ApiParameterResponse> processedParams = new HashSet<ApiParameterResponse>(); |
| for (ApiParameterResponse param : response.getParams()) { |
| if (responseApiNameListMap.containsKey(param.getRelated())) { |
| List<String> relatedApis = responseApiNameListMap.get(param.getRelated()); |
| param.setRelated(StringUtils.defaultString(StringUtils.join(relatedApis, ","))); |
| } else { |
| param.setRelated(null); |
| } |
| processedParams.add(param); |
| } |
| response.setParams(processedParams); |
| |
| if (responseApiNameListMap.containsKey(response.getRelated())) { |
| List<String> relatedApis = responseApiNameListMap.get(response.getRelated()); |
| relatedApis.remove(apiName); |
| response.setRelated(StringUtils.join(relatedApis, ",")); |
| } else { |
| response.setRelated(null); |
| } |
| s_apiNameDiscoveryResponseMap.put(apiName, response); |
| } |
| return responseApiNameListMap; |
| } |
| |
| private ApiResponseResponse getFieldResponseMap(Field responseField) { |
| ApiResponseResponse responseResponse = new ApiResponseResponse(); |
| SerializedName serializedName = responseField.getAnnotation(SerializedName.class); |
| Param param = responseField.getAnnotation(Param.class); |
| if (serializedName != null && param != null) { |
| responseResponse.setName(serializedName.value()); |
| responseResponse.setDescription(param.description()); |
| responseResponse.setType(responseField.getType().getSimpleName().toLowerCase()); |
| //If response is not of primitive type - we have a nested entity |
| Class fieldClass = param.responseObject(); |
| if (fieldClass != null) { |
| Class<?> superClass = fieldClass.getSuperclass(); |
| if (superClass != null) { |
| String superName = superClass.getName(); |
| if (superName.equals(BaseResponse.class.getName())) { |
| Field[] fields = fieldClass.getDeclaredFields(); |
| for (Field field : fields) { |
| ApiResponseResponse innerResponse = getFieldResponseMap(field); |
| if (innerResponse != null) { |
| responseResponse.addApiResponse(innerResponse); |
| } |
| } |
| } |
| } |
| } |
| } |
| return responseResponse; |
| } |
| |
| private ApiDiscoveryResponse getCmdRequestMap(Class<?> cmdClass, APICommand apiCmdAnnotation) { |
| String apiName = apiCmdAnnotation.name(); |
| ApiDiscoveryResponse response = new ApiDiscoveryResponse(); |
| response.setName(apiName); |
| response.setDescription(apiCmdAnnotation.description()); |
| if (!apiCmdAnnotation.since().isEmpty()) { |
| response.setSince(apiCmdAnnotation.since()); |
| } |
| |
| Set<Field> fields = ReflectUtil.getAllFieldsForClass(cmdClass, new Class<?>[] {BaseCmd.class, BaseAsyncCmd.class, BaseAsyncCreateCmd.class}); |
| |
| boolean isAsync = ReflectUtil.isCmdClassAsync(cmdClass, new Class<?>[] {BaseAsyncCmd.class, BaseAsyncCreateCmd.class}); |
| |
| response.setAsync(isAsync); |
| |
| for (Field field : fields) { |
| Parameter parameterAnnotation = field.getAnnotation(Parameter.class); |
| if (parameterAnnotation != null && parameterAnnotation.expose() && parameterAnnotation.includeInApiDoc()) { |
| |
| ApiParameterResponse paramResponse = new ApiParameterResponse(); |
| paramResponse.setName(parameterAnnotation.name()); |
| paramResponse.setDescription(parameterAnnotation.description()); |
| paramResponse.setType(parameterAnnotation.type().toString().toLowerCase()); |
| paramResponse.setLength(parameterAnnotation.length()); |
| paramResponse.setRequired(parameterAnnotation.required()); |
| if (!parameterAnnotation.since().isEmpty()) { |
| paramResponse.setSince(parameterAnnotation.since()); |
| } |
| paramResponse.setRelated(parameterAnnotation.entityType()[0].getName()); |
| response.addParam(paramResponse); |
| } |
| } |
| return response; |
| } |
| |
| @Override |
| public List<String> listApiNames(Account account) { |
| List<String> apiNames = new ArrayList<>(); |
| for (String apiName : s_apiNameDiscoveryResponseMap.keySet()) { |
| boolean isAllowed = true; |
| for (APIChecker apiChecker : _apiAccessCheckers) { |
| try { |
| apiChecker.checkAccess(account, apiName); |
| } catch (Exception ex) { |
| isAllowed = false; |
| } |
| } |
| if (isAllowed) |
| apiNames.add(apiName); |
| } |
| return apiNames; |
| } |
| |
| @Override |
| public ListResponse<? extends BaseResponse> listApis(User user, String name) { |
| ListResponse<ApiDiscoveryResponse> response = new ListResponse<>(); |
| List<ApiDiscoveryResponse> responseList = new ArrayList<>(); |
| List<String> apisAllowed = new ArrayList<>(s_apiNameDiscoveryResponseMap.keySet()); |
| |
| if (user == null) |
| return null; |
| |
| if (name != null) { |
| if (!s_apiNameDiscoveryResponseMap.containsKey(name)) |
| return null; |
| |
| for (APIChecker apiChecker : _apiAccessCheckers) { |
| try { |
| apiChecker.checkAccess(user, name); |
| } catch (Exception ex) { |
| logger.error(String.format("API discovery access check failed for [%s] with error [%s].", name, ex.getMessage()), ex); |
| return null; |
| } |
| } |
| responseList.add(s_apiNameDiscoveryResponseMap.get(name)); |
| |
| } else { |
| Account account = accountService.getAccount(user.getAccountId()); |
| if (account == null) { |
| throw new PermissionDeniedException(String.format("The account with id [%s] for user [%s] is null.", user.getAccountId(), user)); |
| } |
| |
| final Role role = roleService.findRole(account.getRoleId()); |
| if (role == null || role.getId() < 1L) { |
| throw new PermissionDeniedException(String.format("The account [%s] has role null or unknown.", |
| ReflectionToStringBuilderUtils.reflectOnlySelectedFields(account, "accountName", "uuid"))); |
| } |
| |
| if (role.getRoleType() == RoleType.Admin && role.getId() == RoleType.Admin.getId()) { |
| logger.info(String.format("Account [%s] is Root Admin, all APIs are allowed.", |
| ReflectionToStringBuilderUtils.reflectOnlySelectedFields(account, "accountName", "uuid"))); |
| } else { |
| for (APIChecker apiChecker : _apiAccessCheckers) { |
| apisAllowed = apiChecker.getApisAllowedToUser(role, user, apisAllowed); |
| } |
| } |
| |
| for (String apiName: apisAllowed) { |
| responseList.add(s_apiNameDiscoveryResponseMap.get(apiName)); |
| } |
| } |
| response.setResponses(responseList); |
| return response; |
| } |
| |
| @Override |
| public List<Class<?>> getCommands() { |
| List<Class<?>> cmdList = new ArrayList<Class<?>>(); |
| cmdList.add(ListApisCmd.class); |
| return cmdList; |
| } |
| |
| public List<APIChecker> getApiAccessCheckers() { |
| return _apiAccessCheckers; |
| } |
| |
| public void setApiAccessCheckers(List<APIChecker> apiAccessCheckers) { |
| this._apiAccessCheckers = apiAccessCheckers; |
| } |
| |
| public List<PluggableService> getServices() { |
| return _services; |
| } |
| |
| @Inject |
| public void setServices(List<PluggableService> services) { |
| this._services = services; |
| } |
| } |