CLOUDSTACK-8485: listAPIs are taking too long to return results

- Removed regex. based search/replace of sensitive data on API response introduced as part of commit b0c6d4734724358df97b6fa4d8c5beb0f447745e
- Added new response serializer to skip sensitive data from getting logged based on annotation present in resposne object fields
- Added new parameter 'isSensitive' to @Param for marking a field as sensitive in response objects
This commit is contained in:
Koushik Das 2015-11-02 14:59:00 +05:30
parent acce645119
commit e13df96348
16 changed files with 154 additions and 78 deletions

View File

@ -37,4 +37,6 @@ public @interface Param {
String since() default "";
RoleType[] authorized() default {};
boolean isSensitive() default false;
}

View File

@ -17,13 +17,12 @@
package org.apache.cloudstack.api.response;
import com.google.gson.annotations.SerializedName;
import com.cloud.serializer.Param;
public class CreateSSHKeyPairResponse extends SSHKeyPairResponse {
@SerializedName("privatekey")
@Param(description = "Private key")
@Param(description = "Private key", isSensitive = true)
private String privateKey;
public CreateSSHKeyPairResponse() {

View File

@ -25,7 +25,7 @@ import com.cloud.serializer.Param;
public class GetVMPasswordResponse extends BaseResponse {
@SerializedName("encryptedpassword")
@Param(description = "The base64 encoded encrypted password of the VM")
@Param(description = "The base64 encoded encrypted password of the VM", isSensitive = true)
private String encryptedPassword;
public GetVMPasswordResponse() {

View File

@ -63,7 +63,7 @@ public class LoginCmdResponse extends AuthenticationCmdResponse {
private String registered;
@SerializedName(value = ApiConstants.SESSIONKEY)
@Param(description = "Session key that can be passed in subsequent Query command calls")
@Param(description = "Session key that can be passed in subsequent Query command calls", isSensitive = true)
private String sessionKey;
public String getUsername() {

View File

@ -24,11 +24,11 @@ import com.cloud.serializer.Param;
public class RegisterResponse extends BaseResponse {
@SerializedName("apikey")
@Param(description = "the api key of the registered user")
@Param(description = "the api key of the registered user", isSensitive = true)
private String apiKey;
@SerializedName("secretkey")
@Param(description = "the secret key of the registered user")
@Param(description = "the secret key of the registered user", isSensitive = true)
private String secretKey;
public String getApiKey() {

View File

@ -42,7 +42,7 @@ public class RemoteAccessVpnResponse extends BaseResponse implements ControlledE
private String ipRange;
@SerializedName("presharedkey")
@Param(description = "the ipsec preshared key")
@Param(description = "the ipsec preshared key", isSensitive = true)
private String presharedKey;
@SerializedName(ApiConstants.ACCOUNT)

View File

@ -51,7 +51,7 @@ public class Site2SiteCustomerGatewayResponse extends BaseResponse implements Co
private String guestCidrList;
@SerializedName(ApiConstants.IPSEC_PSK)
@Param(description = "IPsec preshared-key of customer gateway")
@Param(description = "IPsec preshared-key of customer gateway", isSensitive = true)
private String ipsecPsk;
@SerializedName(ApiConstants.IKE_POLICY)

View File

@ -58,7 +58,7 @@ public class Site2SiteVpnConnectionResponse extends BaseResponse implements Cont
private String guestCidrList;
@SerializedName(ApiConstants.IPSEC_PSK)
@Param(description = "IPsec Preshared-Key of the customer gateway")
@Param(description = "IPsec Preshared-Key of the customer gateway", isSensitive = true)
//from CustomerGateway
private String ipsecPsk;

View File

@ -78,11 +78,11 @@ public class UserResponse extends BaseResponse {
private String timezone;
@SerializedName("apikey")
@Param(description = "the api key of the user")
@Param(description = "the api key of the user", isSensitive = true)
private String apiKey;
@SerializedName("secretkey")
@Param(description = "the secret key of the user")
@Param(description = "the secret key of the user", isSensitive = true)
private String secretKey;
@SerializedName("accountid")

View File

@ -221,7 +221,7 @@ public class UserVmResponse extends BaseResponse implements ControlledEntityResp
private Set<SecurityGroupResponse> securityGroupList;
@SerializedName(ApiConstants.PASSWORD)
@Param(description = "the password (if exists) of the virtual machine")
@Param(description = "the password (if exists) of the virtual machine", isSensitive = true)
private String password;
@SerializedName("nic")

View File

@ -54,7 +54,7 @@ public class BigSwitchBcfDeviceResponse extends BaseResponse {
@SerializedName(ApiConstants.USERNAME) @Param(description="the controller username")
private String username;
@SerializedName(ApiConstants.PASSWORD) @Param(description="the controller password")
@SerializedName(ApiConstants.PASSWORD) @Param(description="the controller password", isSensitive = true)
private String password;
@SerializedName(BcfConstants.BIGSWITCH_BCF_DEVICE_NAT)

View File

@ -53,7 +53,7 @@ public class LDAPConfigResponse extends BaseResponse {
private String bindDN;
@SerializedName(ApiConstants.BIND_PASSWORD)
@Param(description = "DN password")
@Param(description = "DN password", isSensitive = true)
private String bindPassword;
public String getHostname() {

View File

@ -27,30 +27,40 @@ import com.google.gson.FieldAttributes;
import com.google.gson.GsonBuilder;
/**
* The ApiResonseGsonHelper is different from ApiGsonHelper - it registeres one more adapter for String type required for api response encoding
* The ApiResonseGsonHelper is different from ApiGsonHelper - it registers one more adapter for String type required for api response encoding
*/
public class ApiResponseGsonHelper {
private static final GsonBuilder s_gBuilder;
private static final GsonBuilder s_gLogBuilder;
static {
s_gBuilder = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
s_gBuilder.setVersion(1.3);
s_gBuilder.registerTypeAdapter(ResponseObject.class, new ResponseObjectTypeAdapter());
s_gBuilder.registerTypeAdapter(String.class, new EncodedStringTypeAdapter());
s_gBuilder.setExclusionStrategies(new ExclStrat());
s_gBuilder.setExclusionStrategies(new ApiResponseExclusionStrategy());
s_gLogBuilder = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
s_gLogBuilder.setVersion(1.3);
s_gLogBuilder.registerTypeAdapter(ResponseObject.class, new ResponseObjectTypeAdapter());
s_gLogBuilder.registerTypeAdapter(String.class, new EncodedStringTypeAdapter());
s_gLogBuilder.setExclusionStrategies(new LogExclusionStrategy());
}
public static GsonBuilder getBuilder() {
return s_gBuilder;
}
private static class ExclStrat implements ExclusionStrategy {
public static GsonBuilder getLogBuilder() {
return s_gLogBuilder;
}
private static class ApiResponseExclusionStrategy implements ExclusionStrategy {
public boolean shouldSkipClass(Class<?> arg0) {
return false;
}
public boolean shouldSkipField(FieldAttributes f) {
public boolean shouldSkipField(FieldAttributes f) {
Param param = f.getAnnotation(Param.class);
if (param != null) {
RoleType[] allowedRoles = param.authorized();
@ -71,4 +81,19 @@ public class ApiResponseGsonHelper {
return false;
}
}
private static class LogExclusionStrategy extends ApiResponseExclusionStrategy implements ExclusionStrategy {
public boolean shouldSkipClass(Class<?> arg0) {
return false;
}
public boolean shouldSkipField(FieldAttributes f) {
Param param = f.getAnnotation(Param.class);
boolean skip = (param != null && param.isSensitive());
if (!skip) {
skip = super.shouldSkipField(f);
}
return skip;
}
}
}

View File

@ -525,14 +525,9 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
cmdObj.setHttpMethod(paramMap.get(ApiConstants.HTTPMETHOD).toString());
// This is where the command is either serialized, or directly dispatched
response = queueCommand(cmdObj, paramMap);
if (annotation.responseHasSensitiveInfo())
{
buildAuditTrail(auditTrailSb, command[0],
StringUtils.cleanString(response));
}
else
buildAuditTrail(auditTrailSb, command[0], response);
StringBuilder log = new StringBuilder();
response = queueCommand(cmdObj, paramMap, log);
buildAuditTrail(auditTrailSb, command[0], log.toString());
} else {
final String errorString = "Unknown API command: " + command[0];
s_logger.warn(errorString);
@ -617,7 +612,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
return ApiResponseSerializer.toSerializedString(response, cmd.getResponseType());
}
private String queueCommand(final BaseCmd cmdObj, final Map<String, String> params) throws Exception {
private String queueCommand(final BaseCmd cmdObj, final Map<String, String> params, StringBuilder log) throws Exception {
final CallContext ctx = CallContext.current();
final Long callerUserId = ctx.getCallingUserId();
final Account caller = ctx.getCallingAccount();
@ -717,7 +712,7 @@ public class ApiServer extends ManagerBase implements HttpRequestHandler, ApiSer
}
SerializationContext.current().setUuidTranslation(true);
return ApiResponseSerializer.toSerializedString((ResponseObject)cmdObj.getResponseObject(), cmdObj.getResponseType());
return ApiResponseSerializer.toSerializedStringWithSecureLogs((ResponseObject)cmdObj.getResponseObject(), cmdObj.getResponseType(), log);
}
}

View File

@ -147,8 +147,9 @@ public class ApiServlet extends HttpServlet {
// logging the request start and end in management log for easy debugging
String reqStr = "";
String cleanQueryString = StringUtils.cleanString(req.getQueryString());
if (s_logger.isDebugEnabled()) {
reqStr = auditTrailSb.toString() + " " + StringUtils.cleanString(req.getQueryString());
reqStr = auditTrailSb.toString() + " " + cleanQueryString;
s_logger.debug("===START=== " + reqStr);
}
@ -233,7 +234,7 @@ public class ApiServlet extends HttpServlet {
}
}
auditTrailSb.append(StringUtils.cleanString(req.getQueryString()));
auditTrailSb.append(cleanQueryString);
final boolean isNew = ((session == null) ? true : session.isNew());
// Initialize an empty context and we will update it after we have verified the request below,

View File

@ -27,6 +27,7 @@ import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.exception.ExceptionProxyObject;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseCmd;
@ -56,9 +57,18 @@ public class ApiResponseSerializer {
public static String toSerializedString(ResponseObject result, String responseType) {
s_logger.trace("===Serializing Response===");
if (HttpUtils.RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) {
return toJSONSerializedString(result);
return toJSONSerializedString(result, new StringBuilder());
} else {
return toXMLSerializedString(result);
return toXMLSerializedString(result, new StringBuilder());
}
}
public static String toSerializedStringWithSecureLogs(ResponseObject result, String responseType, StringBuilder log) {
s_logger.trace("===Serializing Response===");
if (HttpUtils.RESPONSE_TYPE_JSON.equalsIgnoreCase(responseType)) {
return toJSONSerializedString(result, log);
} else {
return toXMLSerializedString(result, log);
}
}
@ -73,51 +83,65 @@ public class ApiResponseSerializer {
return str;
}
public static String toJSONSerializedString(ResponseObject result) {
if (result != null) {
Gson gson = ApiResponseGsonHelper.getBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT).create();
public static String toJSONSerializedString(ResponseObject result, StringBuilder log) {
if (result != null && log != null) {
Gson responseBuilder = ApiResponseGsonHelper.getBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT).create();
Gson logBuilder = ApiResponseGsonHelper.getLogBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT).create();
StringBuilder sb = new StringBuilder();
sb.append("{\"").append(result.getResponseName()).append("\":");
log.append("{\"").append(result.getResponseName()).append("\":");
if (result instanceof ListResponse) {
List<? extends ResponseObject> responses = ((ListResponse)result).getResponses();
Integer count = ((ListResponse)result).getCount();
boolean nonZeroCount = (count != null && count.longValue() != 0);
if (nonZeroCount) {
sb.append("{\"").append(ApiConstants.COUNT).append("\":").append(count);
log.append("{\"").append(ApiConstants.COUNT).append("\":").append(count);
}
if ((responses != null) && !responses.isEmpty()) {
String jsonStr = gson.toJson(responses.get(0));
String jsonStr = responseBuilder.toJson(responses.get(0));
jsonStr = unescape(jsonStr);
String logStr = logBuilder.toJson(responses.get(0));
logStr = unescape(logStr);
if (nonZeroCount) {
sb.append(",\"").append(responses.get(0).getObjectName()).append("\":[").append(jsonStr);
log.append(",\"").append(responses.get(0).getObjectName()).append("\":[").append(logStr);
}
for (int i = 1; i < ((ListResponse)result).getResponses().size(); i++) {
jsonStr = gson.toJson(responses.get(i));
jsonStr = responseBuilder.toJson(responses.get(i));
jsonStr = unescape(jsonStr);
logStr = logBuilder.toJson(responses.get(i));
logStr = unescape(logStr);
sb.append(",").append(jsonStr);
log.append(",").append(logStr);
}
sb.append("]}");
log.append("]}");
} else {
if (!nonZeroCount) {
sb.append("{");
log.append("{");
}
sb.append("}");
log.append("}");
}
} else if (result instanceof SuccessResponse) {
sb.append("{\"success\":\"").append(((SuccessResponse)result).getSuccess()).append("\"}");
log.append("{\"success\":\"").append(((SuccessResponse)result).getSuccess()).append("\"}");
} else if (result instanceof ExceptionResponse) {
String jsonErrorText = gson.toJson(result);
String jsonErrorText = responseBuilder.toJson(result);
jsonErrorText = unescape(jsonErrorText);
sb.append(jsonErrorText);
log.append(jsonErrorText);
} else {
String jsonStr = gson.toJson(result);
if ((jsonStr != null) && !"".equals(jsonStr)) {
String jsonStr = responseBuilder.toJson(result);
if (jsonStr != null && !jsonStr.isEmpty()) {
jsonStr = unescape(jsonStr);
if (result instanceof AsyncJobResponse || result instanceof CreateCmdResponse || result instanceof AuthenticationCmdResponse) {
sb.append(jsonStr);
@ -127,53 +151,74 @@ public class ApiResponseSerializer {
} else {
sb.append("{}");
}
String logStr = logBuilder.toJson(result);
if (logStr != null && !logStr.isEmpty()) {
logStr = unescape(logStr);
if (result instanceof AsyncJobResponse || result instanceof CreateCmdResponse || result instanceof AuthenticationCmdResponse) {
log.append(logStr);
} else {
log.append("{\"").append(result.getObjectName()).append("\":").append(logStr).append("}");
}
} else {
log.append("{}");
}
}
sb.append("}");
log.append("}");
return sb.toString();
}
return null;
}
private static String toXMLSerializedString(ResponseObject result) {
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
sb.append("<").append(result.getResponseName()).append(" cloud-stack-version=\"").append(ApiDBUtils.getVersion()).append("\">");
private static String toXMLSerializedString(ResponseObject result, StringBuilder log) {
if (result != null && log != null) {
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
sb.append("<").append(result.getResponseName()).append(" cloud-stack-version=\"").append(ApiDBUtils.getVersion()).append("\">");
log.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
log.append("<").append(result.getResponseName()).append(" cloud-stack-version=\"").append(ApiDBUtils.getVersion()).append("\">");
if (result instanceof ListResponse) {
Integer count = ((ListResponse)result).getCount();
if (result instanceof ListResponse) {
Integer count = ((ListResponse)result).getCount();
if (count != null && count != 0) {
sb.append("<").append(ApiConstants.COUNT).append(">").append(((ListResponse)result).getCount()).append("</").append(ApiConstants.COUNT).append(">");
}
List<? extends ResponseObject> responses = ((ListResponse)result).getResponses();
if ((responses != null) && !responses.isEmpty()) {
for (ResponseObject obj : responses) {
serializeResponseObjXML(sb, obj);
if (count != null && count != 0) {
sb.append("<").append(ApiConstants.COUNT).append(">").append(((ListResponse)result).getCount()).append("</").append(ApiConstants.COUNT).append(">");
log.append("<").append(ApiConstants.COUNT).append(">").append(((ListResponse)result).getCount()).append("</").append(ApiConstants.COUNT).append(">");
}
List<? extends ResponseObject> responses = ((ListResponse)result).getResponses();
if ((responses != null) && !responses.isEmpty()) {
for (ResponseObject obj : responses) {
serializeResponseObjXML(sb, log, obj);
}
}
} else {
if (result instanceof CreateCmdResponse || result instanceof AsyncJobResponse || result instanceof AuthenticationCmdResponse) {
serializeResponseObjFieldsXML(sb, log, result);
} else {
serializeResponseObjXML(sb, log, result);
}
}
} else {
if (result instanceof CreateCmdResponse || result instanceof AsyncJobResponse || result instanceof AuthenticationCmdResponse) {
serializeResponseObjFieldsXML(sb, result);
} else {
serializeResponseObjXML(sb, result);
}
}
sb.append("</").append(result.getResponseName()).append(">");
return sb.toString();
sb.append("</").append(result.getResponseName()).append(">");
log.append("</").append(result.getResponseName()).append(">");
return sb.toString();
}
return null;
}
private static void serializeResponseObjXML(StringBuilder sb, ResponseObject obj) {
private static void serializeResponseObjXML(StringBuilder sb, StringBuilder log, ResponseObject obj) {
if (!(obj instanceof SuccessResponse) && !(obj instanceof ExceptionResponse)) {
sb.append("<").append(obj.getObjectName()).append(">");
log.append("<").append(obj.getObjectName()).append(">");
}
serializeResponseObjFieldsXML(sb, obj);
serializeResponseObjFieldsXML(sb, log, obj);
if (!(obj instanceof SuccessResponse) && !(obj instanceof ExceptionResponse)) {
sb.append("</").append(obj.getObjectName()).append(">");
log.append("</").append(obj.getObjectName()).append(">");
}
}
public static Field[] getFlattenFields(Class<?> clz) {
private static Field[] getFlattenFields(Class<?> clz) {
List<Field> fields = new ArrayList<Field>();
fields.addAll(Arrays.asList(clz.getDeclaredFields()));
if (clz.getSuperclass() != null) {
@ -182,24 +227,23 @@ public class ApiResponseSerializer {
return fields.toArray(new Field[] {});
}
private static void serializeResponseObjFieldsXML(StringBuilder sb, ResponseObject obj) {
private static void serializeResponseObjFieldsXML(StringBuilder sb, StringBuilder log, ResponseObject obj) {
boolean isAsync = false;
if (obj instanceof AsyncJobResponse)
isAsync = true;
//Field[] fields = obj.getClass().getDeclaredFields();
Field[] fields = getFlattenFields(obj.getClass());
for (Field field : fields) {
if ((field.getModifiers() & Modifier.TRANSIENT) != 0) {
continue; // skip transient fields
}
SerializedName serializedName = field.getAnnotation(SerializedName.class);
if (serializedName == null) {
continue; // skip fields w/o serialized name
}
boolean logField = true;
Param param = field.getAnnotation(Param.class);
if (param != null) {
RoleType[] allowedRoles = param.authorized();
@ -213,10 +257,13 @@ public class ApiResponseSerializer {
}
}
if (!permittedParameter) {
s_logger.trace("Ignoring paremeter " + param.name() + " as the caller is not authorized to see it");
s_logger.trace("Ignoring parameter " + param.name() + " as the caller is not authorized to see it");
continue;
}
}
if (param.isSensitive()) {
logField = false;
}
}
field.setAccessible(true);
@ -233,10 +280,12 @@ public class ApiResponseSerializer {
ResponseObject subObj = (ResponseObject)fieldValue;
if (isAsync) {
sb.append("<jobresult>");
log.append("<jobresult>");
}
serializeResponseObjXML(sb, subObj);
serializeResponseObjXML(sb, log, subObj);
if (isAsync) {
sb.append("</jobresult>");
log.append("</jobresult>");
}
} else if (fieldValue instanceof Collection<?>) {
Collection<?> subResponseList = (Collection<?>)fieldValue;
@ -247,7 +296,7 @@ public class ApiResponseSerializer {
if (serializedName != null) {
subObj.setObjectName(serializedName.value());
}
serializeResponseObjXML(sb, subObj);
serializeResponseObjXML(sb, log, subObj);
} else if (value instanceof ExceptionProxyObject) {
// Only exception reponses carry a list of
// ExceptionProxyObject objects.
@ -256,30 +305,32 @@ public class ApiResponseSerializer {
// encountered, put in a uuidList tag.
if (!usedUuidList) {
sb.append("<" + serializedName.value() + ">");
log.append("<" + serializedName.value() + ">");
usedUuidList = true;
}
sb.append("<" + "uuid" + ">" + idProxy.getUuid() + "</" + "uuid" + ">");
log.append("<" + "uuid" + ">" + idProxy.getUuid() + "</" + "uuid" + ">");
// Append the new descriptive property also.
String idFieldName = idProxy.getDescription();
if (idFieldName != null) {
sb.append("<" + "uuidProperty" + ">" + idFieldName + "</" + "uuidProperty" + ">");
log.append("<" + "uuidProperty" + ">" + idFieldName + "</" + "uuidProperty" + ">");
}
} else if (value instanceof String) {
sb.append("<").append(serializedName.value()).append(">").append(value).append("</").append(serializedName.value()).append(">");
if (logField) {
log.append("<").append(serializedName.value()).append(">").append(value).append("</").append(serializedName.value()).append(">");
}
}
}
if (usedUuidList) {
// close the uuidList.
sb.append("</").append(serializedName.value()).append(">");
log.append("</").append(serializedName.value()).append(">");
}
} else if (fieldValue instanceof Date) {
sb.append("<")
.append(serializedName.value())
.append(">")
.append(BaseCmd.getDateString((Date)fieldValue))
.append("</")
.append(serializedName.value())
.append(">");
sb.append("<").append(serializedName.value()).append(">").append(BaseCmd.getDateString((Date)fieldValue)).append("</").append(serializedName.value()).append(">");
log.append("<").append(serializedName.value()).append(">").append(BaseCmd.getDateString((Date)fieldValue)).append("</").append(serializedName.value()).append(">");
} else {
String resultString = escapeSpecialXmlChars(fieldValue.toString());
if (!(obj instanceof ExceptionResponse)) {
@ -287,6 +338,9 @@ public class ApiResponseSerializer {
}
sb.append("<").append(serializedName.value()).append(">").append(resultString).append("</").append(serializedName.value()).append(">");
if (logField) {
log.append("<").append(serializedName.value()).append(">").append(resultString).append("</").append(serializedName.value()).append(">");
}
}
}
}