Skip to content

Commit 96dc6cc

Browse files
committed
Merge remote-tracking branch 'upstream/main' into ExternalDeploymentInetgration
2 parents 7a9e246 + 749ddb9 commit 96dc6cc

File tree

266 files changed

+1683
-1480
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

266 files changed

+1683
-1480
lines changed

api/src/main/java/com/cloud/event/EventTypes.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,9 @@ public class EventTypes {
803803
// Resource Limit
804804
public static final String EVENT_RESOURCE_LIMIT_UPDATE = "RESOURCE.LIMIT.UPDATE";
805805

806+
// Management Server
807+
public static final String EVENT_MANAGEMENT_SERVER_REMOVE = "MANAGEMENT.SERVER.REMOVE";
808+
806809
// VM Lease
807810
public static final String VM_LEASE_EXPIRED = "VM.LEASE.EXPIRED";
808811
public static final String VM_LEASE_DISABLED = "VM.LEASE.DISABLED";
@@ -1317,6 +1320,9 @@ public class EventTypes {
13171320
entityEventDetails.put(EVENT_SHAREDFS_EXPUNGE, SharedFS.class);
13181321
entityEventDetails.put(EVENT_SHAREDFS_RECOVER, SharedFS.class);
13191322

1323+
// Management Server
1324+
entityEventDetails.put(EVENT_MANAGEMENT_SERVER_REMOVE, "ManagementServer");
1325+
13201326
// VM Lease
13211327
entityEventDetails.put(VM_LEASE_EXPIRED, VirtualMachine.class);
13221328
entityEventDetails.put(VM_LEASE_EXPIRING, VirtualMachine.class);

api/src/main/java/com/cloud/server/ManagementService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.apache.cloudstack.api.command.admin.guest.UpdateGuestOsMappingCmd;
3939
import org.apache.cloudstack.api.command.admin.host.ListHostsCmd;
4040
import org.apache.cloudstack.api.command.admin.host.UpdateHostPasswordCmd;
41+
import org.apache.cloudstack.api.command.admin.management.RemoveManagementServerCmd;
4142
import org.apache.cloudstack.api.command.admin.pod.ListPodsByCmd;
4243
import org.apache.cloudstack.api.command.admin.resource.ArchiveAlertsCmd;
4344
import org.apache.cloudstack.api.command.admin.resource.DeleteAlertsCmd;
@@ -506,4 +507,6 @@ VirtualMachine upgradeSystemVM(ScaleSystemVMCmd cmd) throws ResourceUnavailableE
506507

507508
Pair<Boolean, String> patchSystemVM(PatchSystemVMCmd cmd);
508509

510+
boolean removeManagementServer(RemoveManagementServerCmd cmd);
511+
509512
}

api/src/main/java/org/apache/cloudstack/api/ApiErrorCode.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
*/
2323
public enum ApiErrorCode {
2424

25+
BAD_REQUEST(400),
2526
UNAUTHORIZED(401),
2627
UNAUTHORIZED2FA(511),
2728
METHOD_NOT_ALLOWED(405),

api/src/main/java/org/apache/cloudstack/api/ApiServerService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,6 @@ public ResponseObject loginUser(HttpSession session, String username, String pas
4848
boolean forgotPassword(UserAccount userAccount, Domain domain);
4949

5050
boolean resetPassword(UserAccount userAccount, String token, String password);
51+
52+
boolean isPostRequestsAndTimestampsEnforced();
5153
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
package org.apache.cloudstack.api.command.admin.management;
18+
19+
import com.cloud.event.EventTypes;
20+
import org.apache.cloudstack.acl.RoleType;
21+
import org.apache.cloudstack.api.APICommand;
22+
import org.apache.cloudstack.api.Parameter;
23+
import org.apache.cloudstack.api.ServerApiException;
24+
import org.apache.cloudstack.api.ApiConstants;
25+
import org.apache.cloudstack.api.ApiErrorCode;
26+
import org.apache.cloudstack.api.BaseCmd;
27+
import org.apache.cloudstack.api.response.ManagementServerResponse;
28+
import org.apache.cloudstack.api.response.SuccessResponse;
29+
import org.apache.cloudstack.context.CallContext;
30+
31+
@APICommand(name = "removeManagementServer", description = "Removes a Management Server.", responseObject = SuccessResponse.class,
32+
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false, authorized = RoleType.Admin)
33+
public class RemoveManagementServerCmd extends BaseCmd {
34+
35+
@Parameter(name = ApiConstants.ID, type = CommandType.UUID, entityType = ManagementServerResponse.class, required = true, description = "the ID of the Management Server")
36+
private Long id;
37+
38+
public Long getId() {
39+
return id;
40+
}
41+
42+
@Override
43+
public void execute() {
44+
boolean result = _mgr.removeManagementServer(this);
45+
if (result) {
46+
SuccessResponse response = new SuccessResponse(getCommandName());
47+
this.setResponseObject(response);
48+
} else {
49+
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to remove Management Server.");
50+
}
51+
}
52+
53+
@Override
54+
public long getEntityOwnerId() {
55+
return CallContext.current().getCallingAccountId();
56+
}
57+
58+
public String getEventType() {
59+
return EventTypes.EVENT_MANAGEMENT_SERVER_REMOVE;
60+
}
61+
}

server/src/main/java/com/cloud/api/ApiServer.java

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
201201
private static final String SANITIZATION_REGEX = "[\n\r]";
202202

203203
private static boolean encodeApiResponse = false;
204+
private boolean isPostRequestsAndTimestampsEnforced = false;
204205

205206
/**
206207
* Non-printable ASCII characters - numbers 0 to 31 and 127 decimal
@@ -284,6 +285,13 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
284285
, "Session cookie is marked as secure if this is enabled. Secure cookies only work when HTTPS is used."
285286
, false
286287
, ConfigKey.Scope.Global);
288+
static final ConfigKey<Boolean> EnforcePostRequestsAndTimestamps = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED
289+
, Boolean.class
290+
, "enforce.post.requests.and.timestamps"
291+
, "false"
292+
, "Enable/Disable whether the ApiServer should only accept POST requests for state-changing APIs and requests with timestamps."
293+
, false
294+
, ConfigKey.Scope.Global);
287295
private static final ConfigKey<String> JSONDefaultContentType = new ConfigKey<> (ConfigKey.CATEGORY_ADVANCED
288296
, String.class
289297
, "json.content.type"
@@ -441,6 +449,7 @@ protected void setupIntegrationPortListener(Integer apiPort) {
441449
public boolean start() {
442450
Security.addProvider(new BouncyCastleProvider());
443451
Integer apiPort = IntegrationAPIPort.value(); // api port, null by default
452+
isPostRequestsAndTimestampsEnforced = EnforcePostRequestsAndTimestamps.value();
444453

445454
final Long snapshotLimit = ConcurrentSnapshotsThresholdPerHost.value();
446455
if (snapshotLimit == null || snapshotLimit <= 0) {
@@ -720,6 +729,11 @@ public String handleRequest(final Map params, final String responseType, final S
720729
return response;
721730
}
722731

732+
@Override
733+
public boolean isPostRequestsAndTimestampsEnforced() {
734+
return isPostRequestsAndTimestampsEnforced;
735+
}
736+
723737
private String getBaseAsyncResponse(final long jobId, final BaseAsyncCmd cmd) {
724738
final AsyncJobResponse response = new AsyncJobResponse();
725739

@@ -967,7 +981,6 @@ public boolean verifyRequest(final Map<String, Object[]> requestParameters, fina
967981

968982
// put the name in a list that we'll sort later
969983
final List<String> parameterNames = new ArrayList<>(requestParameters.keySet());
970-
971984
Collections.sort(parameterNames);
972985

973986
String signatureVersion = null;
@@ -1019,12 +1032,22 @@ public boolean verifyRequest(final Map<String, Object[]> requestParameters, fina
10191032
}
10201033

10211034
final Date now = new Date(System.currentTimeMillis());
1035+
final Date thresholdTime = new Date(now.getTime() + 15 * 60 * 1000);
10221036
if (expiresTS.before(now)) {
10231037
signature = signature.replaceAll(SANITIZATION_REGEX, "_");
10241038
apiKey = apiKey.replaceAll(SANITIZATION_REGEX, "_");
10251039
logger.debug("Request expired -- ignoring ...sig [{}], apiKey [{}].", signature, apiKey);
10261040
return false;
1041+
} else if (isPostRequestsAndTimestampsEnforced && expiresTS.after(thresholdTime)) {
1042+
signature = signature.replaceAll(SANITIZATION_REGEX, "_");
1043+
apiKey = apiKey.replaceAll(SANITIZATION_REGEX, "_");
1044+
logger.debug(String.format("Expiration parameter is set for too long -- ignoring ...sig [%s], apiKey [%s].", signature, apiKey));
1045+
return false;
10271046
}
1047+
} else if (isPostRequestsAndTimestampsEnforced) {
1048+
// Force expiration parameter
1049+
logger.debug("Signature Version must be 3, and should be along with the Expires parameter -- ignoring request.");
1050+
return false;
10281051
}
10291052

10301053
final TransactionLegacy txn = TransactionLegacy.open(TransactionLegacy.CLOUD_DB);
@@ -1648,6 +1671,7 @@ public String getConfigComponentName() {
16481671
@Override
16491672
public ConfigKey<?>[] getConfigKeys() {
16501673
return new ConfigKey<?>[] {
1674+
EnforcePostRequestsAndTimestamps,
16511675
IntegrationAPIPort,
16521676
ConcurrentSnapshotsThresholdPerHost,
16531677
EncodeApiResponse,

server/src/main/java/com/cloud/api/ApiServlet.java

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@
2222
import java.net.UnknownHostException;
2323
import java.util.Arrays;
2424
import java.util.HashMap;
25+
import java.util.HashSet;
2526
import java.util.List;
2627
import java.util.Map;
28+
import java.util.regex.Pattern;
29+
import java.util.Set;
2730

2831
import javax.inject.Inject;
2932
import javax.servlet.ServletConfig;
@@ -46,6 +49,7 @@
4649
import org.apache.cloudstack.context.CallContext;
4750
import org.apache.cloudstack.managed.context.ManagedContext;
4851
import org.apache.cloudstack.utils.consoleproxy.ConsoleAccessUtils;
52+
import org.apache.commons.collections.MapUtils;
4953

5054
import org.apache.logging.log4j.Logger;
5155
import org.apache.logging.log4j.LogManager;
@@ -78,6 +82,39 @@ public class ApiServlet extends HttpServlet {
7882
private static final Logger ACCESSLOGGER = LogManager.getLogger("apiserver." + ApiServlet.class.getName());
7983
private static final String REPLACEMENT = "_";
8084
private static final String LOGGER_REPLACEMENTS = "[\n\r\t]";
85+
private static final Pattern GET_REQUEST_COMMANDS = Pattern.compile("^(get|list|query|find)(\\w+)+$");
86+
private static final HashSet<String> GET_REQUEST_COMMANDS_LIST = new HashSet<>(Set.of("isaccountallowedtocreateofferingswithtags",
87+
"readyforshutdown", "cloudianisenabled", "quotabalance", "quotasummary", "quotatarifflist", "quotaisenabled", "quotastatement", "verifyoauthcodeandgetuser"));
88+
private static final HashSet<String> POST_REQUESTS_TO_DISABLE_LOGGING = new HashSet<>(Set.of(
89+
"login",
90+
"oauthlogin",
91+
"createaccount",
92+
"createuser",
93+
"updateuser",
94+
"forgotpassword",
95+
"resetpassword",
96+
"importrole",
97+
"updaterolepermission",
98+
"updateprojectrolepermission",
99+
"createstoragepool",
100+
"addhost",
101+
"updatehostpassword",
102+
"addcluster",
103+
"addvmwaredc",
104+
"configureoutofbandmanagement",
105+
"uploadcustomcertificate",
106+
"addciscovnmcresource",
107+
"addnetscalerloadbalancer",
108+
"createtungstenfabricprovider",
109+
"addnsxcontroller",
110+
"configtungstenfabricservice",
111+
"createnetworkacl",
112+
"updatenetworkaclitem",
113+
"quotavalidateactivationrule",
114+
"quotatariffupdate",
115+
"listandswitchsamlaccount",
116+
"uploadresourceicon"
117+
));
81118

82119
@Inject
83120
ApiServerService apiServer;
@@ -193,11 +230,24 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp
193230

194231
utf8Fixup(req, params);
195232

233+
final Object[] commandObj = params.get(ApiConstants.COMMAND);
234+
final String command = commandObj == null ? null : (String) commandObj[0];
235+
196236
// logging the request start and end in management log for easy debugging
197237
String reqStr = "";
198238
String cleanQueryString = StringUtils.cleanString(req.getQueryString());
199239
if (LOGGER.isDebugEnabled()) {
200240
reqStr = auditTrailSb.toString() + " " + cleanQueryString;
241+
if (req.getMethod().equalsIgnoreCase("POST") && org.apache.commons.lang3.StringUtils.isNotBlank(command)) {
242+
if (!POST_REQUESTS_TO_DISABLE_LOGGING.contains(command.toLowerCase()) && !reqParams.containsKey(ApiConstants.USER_DATA)) {
243+
String cleanParamsString = getCleanParamsString(reqParams);
244+
if (org.apache.commons.lang3.StringUtils.isNotBlank(cleanParamsString)) {
245+
reqStr += "\n" + cleanParamsString;
246+
}
247+
} else {
248+
reqStr += " " + command;
249+
}
250+
}
201251
LOGGER.debug("===START=== " + reqStr);
202252
}
203253

@@ -213,8 +263,6 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp
213263
responseType = (String)responseTypeParam[0];
214264
}
215265

216-
final Object[] commandObj = params.get(ApiConstants.COMMAND);
217-
final String command = commandObj == null ? null : (String) commandObj[0];
218266
final Object[] userObj = params.get(ApiConstants.USERNAME);
219267
String username = userObj == null ? null : (String)userObj[0];
220268
if (LOGGER.isTraceEnabled()) {
@@ -317,6 +365,19 @@ void processRequestInContext(final HttpServletRequest req, final HttpServletResp
317365
}
318366
}
319367

368+
if (apiServer.isPostRequestsAndTimestampsEnforced() && !isStateChangingCommandUsingPOST(command, req.getMethod(), params)) {
369+
String errorText = String.format("State changing command %s needs to be sent using POST request", command);
370+
if (command.equalsIgnoreCase("updateConfiguration") && params.containsKey("name")) {
371+
errorText = String.format("Changes for configuration %s needs to be sent using POST request", params.get("name")[0]);
372+
}
373+
auditTrailSb.append(" " + HttpServletResponse.SC_BAD_REQUEST + " " + errorText);
374+
final String serializedResponse =
375+
apiServer.getSerializedApiError(new ServerApiException(ApiErrorCode.BAD_REQUEST, errorText), params,
376+
responseType);
377+
HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_BAD_REQUEST, responseType, ApiServer.JSONcontentType.value());
378+
return;
379+
}
380+
320381
Long userId = null;
321382
if (!isNew) {
322383
userId = (Long)session.getAttribute("userid");
@@ -407,6 +468,15 @@ private boolean checkIfAuthenticatorIsOf2FA(String command) {
407468
return verify2FA;
408469
}
409470

471+
private boolean isStateChangingCommandUsingPOST(String command, String method, Map<String, Object[]> params) {
472+
if (command == null || (!GET_REQUEST_COMMANDS.matcher(command.toLowerCase()).matches() && !GET_REQUEST_COMMANDS_LIST.contains(command.toLowerCase())
473+
&& !command.equalsIgnoreCase("updateConfiguration") && !method.equals("POST"))) {
474+
return false;
475+
}
476+
return !command.equalsIgnoreCase("updateConfiguration") || method.equals("POST") || (params.containsKey("name")
477+
&& params.get("name")[0].toString().equalsIgnoreCase(ApiServer.EnforcePostRequestsAndTimestamps.key()));
478+
}
479+
410480
protected boolean skip2FAcheckForAPIs(String command) {
411481
boolean skip2FAcheck = false;
412482

@@ -644,4 +714,45 @@ private static String getCorrectIPAddress(String ip) {
644714
}
645715
return null;
646716
}
717+
718+
private String getCleanParamsString(Map<String, String[]> reqParams) {
719+
if (MapUtils.isEmpty(reqParams)) {
720+
return "";
721+
}
722+
723+
StringBuilder cleanParamsString = new StringBuilder();
724+
for (Map.Entry<String, String[]> reqParam : reqParams.entrySet()) {
725+
if (org.apache.commons.lang3.StringUtils.isBlank(reqParam.getKey())) {
726+
continue;
727+
}
728+
729+
cleanParamsString.append(reqParam.getKey());
730+
cleanParamsString.append("=");
731+
732+
if (reqParam.getKey().toLowerCase().contains("password")
733+
|| reqParam.getKey().toLowerCase().contains("privatekey")
734+
|| reqParam.getKey().toLowerCase().contains("accesskey")
735+
|| reqParam.getKey().toLowerCase().contains("secretkey")) {
736+
cleanParamsString.append("\n");
737+
continue;
738+
}
739+
740+
if (reqParam.getValue() == null || reqParam.getValue().length == 0) {
741+
cleanParamsString.append("\n");
742+
continue;
743+
}
744+
745+
for (String param : reqParam.getValue()) {
746+
if (org.apache.commons.lang3.StringUtils.isBlank(param)) {
747+
continue;
748+
}
749+
String cleanParamString = StringUtils.cleanString(param.trim());
750+
cleanParamsString.append(cleanParamString);
751+
cleanParamsString.append(" ");
752+
}
753+
cleanParamsString.append("\n");
754+
}
755+
756+
return cleanParamsString.toString();
757+
}
647758
}

server/src/main/java/com/cloud/api/query/vo/ManagementServerJoinVO.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,4 +170,8 @@ public String getJavaName() {
170170
public String getJavaVersion() {
171171
return javaVersion;
172172
}
173+
174+
public void setRemoved(Date removedDate) {
175+
removed = removedDate;
176+
}
173177
}

0 commit comments

Comments
 (0)