Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/src/main/java/org/apache/cloudstack/api/APICommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,6 @@
RoleType[] authorized() default {};

Class<?>[] entityType() default {};

String httpMethod() default "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ public class ApiConstants {
public static final String HOST = "host";
public static final String HOST_CONTROL_STATE = "hostcontrolstate";
public static final String HOSTS_MAP = "hostsmap";
public static final String HTTP_REQUEST_TYPE = "httprequesttype";
public static final String HYPERVISOR = "hypervisor";
public static final String INLINE = "inline";
public static final String INSTANCE = "instance";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
import org.apache.cloudstack.api.response.IsAccountAllowedToCreateOfferingsWithTagsResponse;

@APICommand(name = "isAccountAllowedToCreateOfferingsWithTags", description = "Return true if the specified account is allowed to create offerings with tags.",
responseObject = IsAccountAllowedToCreateOfferingsWithTagsResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
responseObject = IsAccountAllowedToCreateOfferingsWithTagsResponse.class, requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
httpMethod = "GET")
public class IsAccountAllowedToCreateOfferingsWithTagsCmd extends BaseCmd {

@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = AccountResponse.class, description = "Account UUID", required = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ public class ApiDiscoveryResponse extends BaseResponse {
@Param(description = "response field type")
private String type;

@SerializedName(ApiConstants.HTTP_REQUEST_TYPE)
@Param(description = "Preferred HTTP request type for the API", since = "4.22.0")
private String httpRequestType;

public ApiDiscoveryResponse() {
params = new HashSet<ApiParameterResponse>();
apiResponse = new HashSet<ApiResponseResponse>();
Expand All @@ -74,6 +78,7 @@ public ApiDiscoveryResponse(ApiDiscoveryResponse another) {
this.params = new HashSet<>(another.getParams());
this.apiResponse = new HashSet<>(another.getApiResponse());
this.type = another.getType();
this.httpRequestType = another.getHttpRequestType();
this.setObjectName(another.getObjectName());
}

Expand Down Expand Up @@ -140,4 +145,12 @@ public Set<ApiResponseResponse> getApiResponse() {
public String getType() {
return type;
}

public String getHttpRequestType() {
return httpRequestType;
}

public void setHttpRequestType(String httpRequestType) {
this.httpRequestType = httpRequestType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import org.reflections.ReflectionUtils;
import org.springframework.stereotype.Component;

import com.cloud.api.ApiServlet;
import com.cloud.exception.PermissionDeniedException;
import com.cloud.serializer.Param;
import com.cloud.user.Account;
Expand Down Expand Up @@ -189,14 +190,20 @@ private ApiResponseResponse getFieldResponseMap(Field responseField) {
return responseResponse;
}

private ApiDiscoveryResponse getCmdRequestMap(Class<?> cmdClass, APICommand apiCmdAnnotation) {
protected 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());
}
String httpRequestType = apiCmdAnnotation.httpMethod();
if (StringUtils.isBlank(httpRequestType)) {
httpRequestType = ApiServlet.GET_REQUEST_COMMANDS.matcher(apiName.toLowerCase()).matches() ?
"GET" : "POST";
}
response.setHttpRequestType(httpRequestType);

Set<Field> fields = ReflectUtil.getAllFieldsForClass(cmdClass, new Class<?>[] {BaseCmd.class, BaseAsyncCmd.class, BaseAsyncCreateCmd.class});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// 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 static org.mockito.ArgumentMatchers.any;

import java.lang.reflect.Field;
import java.util.Set;

import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.BaseAsyncCmd;
import org.apache.cloudstack.api.BaseCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.command.admin.account.CreateAccountCmd;
import org.apache.cloudstack.api.command.admin.user.GetUserCmd;
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.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.Spy;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.test.util.ReflectionTestUtils;

import com.cloud.utils.ReflectUtil;

@RunWith(MockitoJUnitRunner.class)
public class ApiDiscoveryServiceImplTest {

@Mock
APICommand apiCommandMock;

@Spy
@InjectMocks
ApiDiscoveryServiceImpl discoveryServiceSpy;

@Before
public void setUp() {
Mockito.when(apiCommandMock.name()).thenReturn("listApis");
Mockito.when(apiCommandMock.since()).thenReturn("");
}

@Test
public void getCmdRequestMapReturnsResponseWithCorrectApiNameAndDescription() {
Mockito.when(apiCommandMock.description()).thenReturn("Lists all APIs");
ApiDiscoveryResponse response = discoveryServiceSpy.getCmdRequestMap(ListApisCmd.class, apiCommandMock);
Assert.assertEquals("listApis", response.getName());
Assert.assertEquals("Lists all APIs", response.getDescription());
}

@Test
public void getCmdRequestMapSetsHttpRequestTypeToGetWhenApiNameMatchesGetPattern() {
Mockito.when(apiCommandMock.name()).thenReturn("getUser");
Mockito.when(apiCommandMock.httpMethod()).thenReturn("");
ApiDiscoveryResponse response = discoveryServiceSpy.getCmdRequestMap(GetUserCmd.class, apiCommandMock);
Assert.assertEquals("GET", response.getHttpRequestType());
}

@Test
public void getCmdRequestMapSetsHttpRequestTypeToPostWhenApiNameDoesNotMatchGetPattern() {
Mockito.when(apiCommandMock.name()).thenReturn("createAccount");
Mockito.when(apiCommandMock.httpMethod()).thenReturn("");
ApiDiscoveryResponse response = discoveryServiceSpy.getCmdRequestMap(CreateAccountCmd.class, apiCommandMock);
Assert.assertEquals("POST", response.getHttpRequestType());
}

@Test
public void getCmdRequestMapSetsAsyncToTrueForAsyncCommand() {
Mockito.when(apiCommandMock.name()).thenReturn("asyncApi");
ApiDiscoveryResponse response = discoveryServiceSpy.getCmdRequestMap(BaseAsyncCmd.class, apiCommandMock);
Assert.assertTrue(response.getAsync());
}

@Test
public void getCmdRequestMapDoesNotAddParamsWithoutParameterAnnotation() {
ApiDiscoveryResponse response = discoveryServiceSpy.getCmdRequestMap(BaseCmd.class, apiCommandMock);
Assert.assertFalse(response.getParams().isEmpty());
Assert.assertEquals(1, response.getParams().size());
}

@Test
public void getCmdRequestMapAddsParamsWithExposedAndIncludedInApiDocAnnotations() {
Field fieldMock = Mockito.mock(Field.class);
Parameter parameterMock = Mockito.mock(Parameter.class);
Mockito.when(parameterMock.expose()).thenReturn(true);
Mockito.when(parameterMock.includeInApiDoc()).thenReturn(true);
Mockito.when(parameterMock.name()).thenReturn("paramName");
Mockito.when(parameterMock.since()).thenReturn("");
Mockito.when(parameterMock.entityType()).thenReturn(new Class[]{Object.class});
Mockito.when(parameterMock.description()).thenReturn("paramDescription");
Mockito.when(parameterMock.type()).thenReturn(BaseCmd.CommandType.STRING);
Mockito.when(fieldMock.getAnnotation(Parameter.class)).thenReturn(parameterMock);
try (MockedStatic<ReflectUtil> reflectUtilMockedStatic = Mockito.mockStatic(ReflectUtil.class)) {
reflectUtilMockedStatic.when(() -> ReflectUtil.getAllFieldsForClass(any(Class.class), any(Class[].class)))
.thenReturn(Set.of(fieldMock));
ApiDiscoveryResponse response = discoveryServiceSpy.getCmdRequestMap(ListApisCmd.class, apiCommandMock);
Set<ApiParameterResponse> params = response.getParams();
Assert.assertEquals(1, params.size());
ApiParameterResponse paramResponse = params.iterator().next();
Assert.assertEquals("paramName", ReflectionTestUtils.getField(paramResponse, "name"));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
import org.apache.cloudstack.quota.vo.QuotaBalanceVO;
import org.apache.cloudstack.api.response.QuotaStatementItemResponse;

@APICommand(name = "quotaBalance", responseObject = QuotaStatementItemResponse.class, description = "Create a quota balance statement", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
@APICommand(name = "quotaBalance", responseObject = QuotaStatementItemResponse.class, description = "Create a quota balance statement", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
httpMethod = "GET")
public class QuotaBalanceCmd extends BaseCmd {


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@

import javax.inject.Inject;

@APICommand(name = "quotaIsEnabled", responseObject = QuotaEnabledResponse.class, description = "Return true if the plugin is enabled", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
@APICommand(name = "quotaIsEnabled", responseObject = QuotaEnabledResponse.class, description = "Return true if the plugin is enabled", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
httpMethod = "GET")
public class QuotaEnabledCmd extends BaseCmd {


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@

import com.cloud.user.Account;

@APICommand(name = "quotaStatement", responseObject = QuotaStatementItemResponse.class, description = "Create a quota statement", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
@APICommand(name = "quotaStatement", responseObject = QuotaStatementItemResponse.class, description = "Create a quota statement", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
httpMethod = "GET")
public class QuotaStatementCmd extends BaseCmd {


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@

import javax.inject.Inject;

@APICommand(name = "quotaSummary", responseObject = QuotaSummaryResponse.class, description = "Lists balance and quota usage for all accounts", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
@APICommand(name = "quotaSummary", responseObject = QuotaSummaryResponse.class, description = "Lists balance and quota usage for all accounts", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
httpMethod = "GET")
public class QuotaSummaryCmd extends BaseListCmd {

@Parameter(name = ApiConstants.ACCOUNT, type = CommandType.STRING, required = false, description = "Optional, Account Id for which statement needs to be generated")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
import java.util.Date;
import java.util.List;

@APICommand(name = "quotaTariffList", responseObject = QuotaTariffResponse.class, description = "Lists all quota tariff plans", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
@APICommand(name = "quotaTariffList", responseObject = QuotaTariffResponse.class, description = "Lists all quota tariff plans", since = "4.7.0", requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
httpMethod = "GET")
public class QuotaTariffListCmd extends BaseListCmd {

@Inject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
responseObject = CloudianEnabledResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
since = "4.11.0",
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User},
httpMethod = "GET")
public class CloudianIsEnabledCmd extends BaseCmd {

@Inject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
description = "Returns the status of CloudStack, whether a shutdown has been triggered and if ready to shutdown",
since = "4.19.0",
responseObject = ManagementServerMaintenanceResponse.class,
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false)
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
httpMethod = "GET")
public class ReadyForShutdownCmd extends BaseMSMaintenanceActionCmd {
public static final String APINAME = "readyForShutdown";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
import java.util.List;
import java.util.Map;

import com.cloud.api.response.ApiResponseSerializer;
import com.cloud.user.Account;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
Expand All @@ -37,13 +39,13 @@
import org.apache.cloudstack.oauth2.api.response.OauthProviderResponse;
import org.apache.commons.lang.ArrayUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import com.cloud.api.response.ApiResponseSerializer;
import com.cloud.user.Account;

@APICommand(name = "verifyOAuthCodeAndGetUser", description = "Verify the OAuth Code and fetch the corresponding user from provider", responseObject = OauthProviderResponse.class, entityType = {},
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, since = "4.19.0")
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User}, since = "4.19.0",
httpMethod = "GET")
public class VerifyOAuthCodeAndGetUserCmd extends BaseListCmd implements APIAuthenticator {

/////////////////////////////////////////////////////
Expand Down
Loading
Loading