Skip to content

Commit b2fbe7b

Browse files
authored
console: Console access enhancements (#6577)
This PR creates a new API createConsoleAccess to create VM console URL allowing it to connect using other UI implementations. To avoid reply attacks, the console access is enhanced to use a one time token per session New configuration added: consoleproxy.extra.security.validation.enabled: Enable/disable extra security validation for console proxy using a token Documentation PR: apache/cloudstack-documentation#284
1 parent 7be7ef6 commit b2fbe7b

File tree

47 files changed

+1744
-290
lines changed

Some content is hidden

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

47 files changed

+1744
-290
lines changed

agent/src/main/java/com/cloud/agent/resource/consoleproxy/ConsoleProxyResource.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
import javax.naming.ConfigurationException;
3434

35+
import com.cloud.agent.api.proxy.AllowConsoleAccessCommand;
3536
import org.apache.cloudstack.managed.context.ManagedContextRunnable;
3637
import org.apache.log4j.Logger;
3738

@@ -105,12 +106,28 @@ public Answer executeRequest(final Command cmd) {
105106
} else if (cmd instanceof CheckHealthCommand) {
106107
return new CheckHealthAnswer((CheckHealthCommand)cmd, true);
107108
} else if (cmd instanceof StartConsoleProxyAgentHttpHandlerCommand) {
108-
return execute((StartConsoleProxyAgentHttpHandlerCommand)cmd);
109+
return execute((StartConsoleProxyAgentHttpHandlerCommand) cmd);
110+
} else if (cmd instanceof AllowConsoleAccessCommand) {
111+
return execute((AllowConsoleAccessCommand) cmd);
109112
} else {
110113
return Answer.createUnsupportedCommandAnswer(cmd);
111114
}
112115
}
113116

117+
private Answer execute(AllowConsoleAccessCommand cmd) {
118+
String sessionUuid = cmd.getSessionUuid();
119+
try {
120+
Class<?> consoleProxyClazz = Class.forName("com.cloud.consoleproxy.ConsoleProxy");
121+
Method methodSetup = consoleProxyClazz.getMethod("addAllowedSession", String.class);
122+
methodSetup.invoke(null, sessionUuid);
123+
return new Answer(cmd);
124+
} catch (SecurityException | NoSuchMethodException | ClassNotFoundException | InvocationTargetException | IllegalAccessException e) {
125+
String errorMsg = "Unable to add allowed session due to: " + e.getMessage();
126+
s_logger.error(errorMsg, e);
127+
return new Answer(cmd, false, errorMsg);
128+
}
129+
}
130+
114131
private Answer execute(StartConsoleProxyAgentHttpHandlerCommand cmd) {
115132
s_logger.info("Invoke launchConsoleProxy() in responding to StartConsoleProxyAgentHttpHandlerCommand");
116133
launchConsoleProxy(cmd.getKeystoreBits(), cmd.getKeystorePassword(), cmd.getEncryptorPassword(), cmd.isSourceIpCheckEnabled());
@@ -382,9 +399,10 @@ protected void runInContext() {
382399
}
383400
}
384401

385-
public String authenticateConsoleAccess(String host, String port, String vmId, String sid, String ticket, Boolean isReauthentication) {
402+
public String authenticateConsoleAccess(String host, String port, String vmId, String sid, String ticket,
403+
Boolean isReauthentication, String sessionToken) {
386404

387-
ConsoleAccessAuthenticationCommand cmd = new ConsoleAccessAuthenticationCommand(host, port, vmId, sid, ticket);
405+
ConsoleAccessAuthenticationCommand cmd = new ConsoleAccessAuthenticationCommand(host, port, vmId, sid, ticket, sessionToken);
388406
cmd.setReauthenticating(isReauthentication);
389407

390408
ConsoleProxyAuthenticationResult result = new ConsoleProxyAuthenticationResult();
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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.user.consoleproxy;
18+
19+
public class ConsoleEndpoint {
20+
21+
private boolean result;
22+
private String details;
23+
private String url;
24+
private String websocketToken;
25+
private String websocketPath;
26+
private String websocketHost;
27+
private String websocketPort;
28+
private String websocketExtra;
29+
30+
public ConsoleEndpoint(boolean result, String url) {
31+
this.result = result;
32+
this.url = url;
33+
}
34+
35+
public ConsoleEndpoint(boolean result, String url, String details) {
36+
this(result, url);
37+
this.details = details;
38+
}
39+
40+
public boolean isResult() {
41+
return result;
42+
}
43+
44+
public void setResult(boolean result) {
45+
this.result = result;
46+
}
47+
48+
public String getUrl() {
49+
return url;
50+
}
51+
52+
public void setUrl(String url) {
53+
this.url = url;
54+
}
55+
56+
public String getDetails() {
57+
return details;
58+
}
59+
60+
public void setDetails(String details) {
61+
this.details = details;
62+
}
63+
64+
public String getWebsocketToken() {
65+
return websocketToken;
66+
}
67+
68+
public void setWebsocketToken(String websocketToken) {
69+
this.websocketToken = websocketToken;
70+
}
71+
72+
public String getWebsocketPath() {
73+
return websocketPath;
74+
}
75+
76+
public void setWebsocketPath(String websocketPath) {
77+
this.websocketPath = websocketPath;
78+
}
79+
80+
public String getWebsocketHost() {
81+
return websocketHost;
82+
}
83+
84+
public void setWebsocketHost(String websocketHost) {
85+
this.websocketHost = websocketHost;
86+
}
87+
88+
public String getWebsocketPort() {
89+
return websocketPort;
90+
}
91+
92+
public void setWebsocketPort(String websocketPort) {
93+
this.websocketPort = websocketPort;
94+
}
95+
96+
public String getWebsocketExtra() {
97+
return websocketExtra;
98+
}
99+
100+
public void setWebsocketExtra(String websocketExtra) {
101+
this.websocketExtra = websocketExtra;
102+
}
103+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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.user.consoleproxy;
18+
19+
import com.cloud.exception.ConcurrentOperationException;
20+
import com.cloud.exception.InsufficientCapacityException;
21+
import com.cloud.exception.NetworkRuleConflictException;
22+
import com.cloud.exception.ResourceAllocationException;
23+
import com.cloud.exception.ResourceUnavailableException;
24+
import org.apache.cloudstack.acl.RoleType;
25+
import org.apache.cloudstack.api.APICommand;
26+
import org.apache.cloudstack.api.ApiConstants;
27+
import org.apache.cloudstack.api.ApiErrorCode;
28+
import org.apache.cloudstack.api.BaseCmd;
29+
import org.apache.cloudstack.api.Parameter;
30+
import org.apache.cloudstack.api.ServerApiException;
31+
import org.apache.cloudstack.api.response.ConsoleEndpointWebsocketResponse;
32+
import org.apache.cloudstack.api.response.CreateConsoleEndpointResponse;
33+
import org.apache.cloudstack.api.response.UserVmResponse;
34+
import org.apache.cloudstack.consoleproxy.ConsoleAccessManager;
35+
import org.apache.cloudstack.context.CallContext;
36+
import org.apache.cloudstack.utils.consoleproxy.ConsoleAccessUtils;
37+
import org.apache.commons.collections.MapUtils;
38+
import org.apache.log4j.Logger;
39+
40+
import javax.inject.Inject;
41+
import java.util.Map;
42+
43+
@APICommand(name = CreateConsoleEndpointCmd.APINAME, description = "Create a console endpoint to connect to a VM console",
44+
responseObject = CreateConsoleEndpointResponse.class, since = "4.18.0",
45+
requestHasSensitiveInfo = false, responseHasSensitiveInfo = false,
46+
authorized = {RoleType.Admin, RoleType.ResourceAdmin, RoleType.DomainAdmin, RoleType.User})
47+
public class CreateConsoleEndpointCmd extends BaseCmd {
48+
49+
public static final String APINAME = "createConsoleEndpoint";
50+
public static final Logger s_logger = Logger.getLogger(CreateConsoleEndpointCmd.class.getName());
51+
52+
@Inject
53+
private ConsoleAccessManager consoleManager;
54+
55+
@Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID,
56+
type = CommandType.UUID,
57+
entityType = UserVmResponse.class,
58+
required = true,
59+
description = "ID of the VM")
60+
private Long vmId;
61+
62+
@Parameter(name = ApiConstants.TOKEN,
63+
type = CommandType.STRING,
64+
required = false,
65+
description = "(optional) extra security token, valid when the extra validation is enabled")
66+
private String extraSecurityToken;
67+
68+
@Override
69+
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
70+
String clientAddress = getClientAddress();
71+
ConsoleEndpoint endpoint = consoleManager.generateConsoleEndpoint(vmId, extraSecurityToken, clientAddress);
72+
if (endpoint != null) {
73+
CreateConsoleEndpointResponse response = createResponse(endpoint);
74+
setResponseObject(response);
75+
} else {
76+
throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Unable to generate console endpoint for vm " + vmId);
77+
}
78+
}
79+
80+
private CreateConsoleEndpointResponse createResponse(ConsoleEndpoint endpoint) {
81+
CreateConsoleEndpointResponse response = new CreateConsoleEndpointResponse();
82+
response.setResult(endpoint.isResult());
83+
response.setDetails(endpoint.getDetails());
84+
response.setUrl(endpoint.getUrl());
85+
response.setWebsocketResponse(createWebsocketResponse(endpoint));
86+
response.setResponseName(getCommandName());
87+
response.setObjectName("consoleendpoint");
88+
return response;
89+
}
90+
91+
private ConsoleEndpointWebsocketResponse createWebsocketResponse(ConsoleEndpoint endpoint) {
92+
ConsoleEndpointWebsocketResponse wsResponse = new ConsoleEndpointWebsocketResponse();
93+
wsResponse.setHost(endpoint.getWebsocketHost());
94+
wsResponse.setPort(endpoint.getWebsocketPort());
95+
wsResponse.setPath(endpoint.getWebsocketPath());
96+
wsResponse.setToken(endpoint.getWebsocketToken());
97+
wsResponse.setExtra(endpoint.getWebsocketExtra());
98+
wsResponse.setObjectName("websocket");
99+
return wsResponse;
100+
}
101+
102+
private String getParameterBase(String paramKey) {
103+
Map<String, String> params = getFullUrlParams();
104+
return MapUtils.isNotEmpty(params) ? params.get(paramKey) : null;
105+
}
106+
107+
private String getClientAddress() {
108+
return getParameterBase(ConsoleAccessUtils.CLIENT_INET_ADDRESS_KEY);
109+
}
110+
111+
@Override
112+
public String getCommandName() {
113+
return APINAME.toLowerCase() + BaseCmd.RESPONSE_SUFFIX;
114+
}
115+
116+
@Override
117+
public long getEntityOwnerId() {
118+
return CallContext.current().getCallingAccount().getId();
119+
}
120+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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.response;
18+
19+
import com.cloud.serializer.Param;
20+
import com.google.gson.annotations.SerializedName;
21+
import org.apache.cloudstack.api.ApiConstants;
22+
import org.apache.cloudstack.api.BaseResponse;
23+
24+
public class ConsoleEndpointWebsocketResponse extends BaseResponse {
25+
26+
public ConsoleEndpointWebsocketResponse() {
27+
}
28+
29+
@SerializedName(ApiConstants.TOKEN)
30+
@Param(description = "the console websocket token")
31+
private String token;
32+
33+
@SerializedName("host")
34+
@Param(description = "the console websocket host")
35+
private String host;
36+
37+
@SerializedName(ApiConstants.PORT)
38+
@Param(description = "the console websocket port")
39+
private String port;
40+
41+
@SerializedName(ApiConstants.PATH)
42+
@Param(description = "the console websocket path")
43+
private String path;
44+
45+
@SerializedName("extra")
46+
@Param(description = "the console websocket extra field for validation (if enabled)")
47+
private String extra;
48+
49+
public String getToken() {
50+
return token;
51+
}
52+
53+
public void setToken(String token) {
54+
this.token = token;
55+
}
56+
57+
public String getHost() {
58+
return host;
59+
}
60+
61+
public void setHost(String host) {
62+
this.host = host;
63+
}
64+
65+
public String getPort() {
66+
return port;
67+
}
68+
69+
public void setPort(String port) {
70+
this.port = port;
71+
}
72+
73+
public String getPath() {
74+
return path;
75+
}
76+
77+
public void setPath(String path) {
78+
this.path = path;
79+
}
80+
81+
public String getExtra() {
82+
return extra;
83+
}
84+
85+
public void setExtra(String extra) {
86+
this.extra = extra;
87+
}
88+
}

0 commit comments

Comments
 (0)