Skip to content

Preserve error code, code name, and error labels when redacting command monitoring/logging #1225

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 4 additions & 11 deletions driver-core/src/main/com/mongodb/MongoCommandException.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@

package com.mongodb;

import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonString;
import org.bson.codecs.BsonDocumentCodec;
import org.bson.codecs.EncoderContext;
import org.bson.json.JsonWriter;

import java.io.StringWriter;

import static com.mongodb.internal.ExceptionUtils.MongoCommandExceptionUtils.extractErrorCode;
import static com.mongodb.internal.ExceptionUtils.MongoCommandExceptionUtils.extractErrorCodeName;
import static com.mongodb.internal.ExceptionUtils.MongoCommandExceptionUtils.extractErrorLabelsAsBson;
import static java.lang.String.format;

/**
Expand All @@ -50,7 +51,7 @@ public MongoCommandException(final BsonDocument response, final ServerAddress ad
format("Command failed with error %s: '%s' on server %s. The full response is %s", extractErrorCodeAndName(response),
extractErrorMessage(response), address, getResponseAsJson(response)), address);
this.response = response;
addLabels(response.getArray("errorLabels", new BsonArray()));
addLabels(extractErrorLabelsAsBson(response));
}

/**
Expand Down Expand Up @@ -109,14 +110,6 @@ private static String extractErrorCodeAndName(final BsonDocument response) {
}
}

private static int extractErrorCode(final BsonDocument response) {
return response.getNumber("code", new BsonInt32(-1)).intValue();
}

private static String extractErrorCodeName(final BsonDocument response) {
return response.getString("codeName", new BsonString("")).getValue();
}

private static String extractErrorMessage(final BsonDocument response) {
String errorMessage = response.getString("errmsg", new BsonString("")).getValue();
// Satisfy nullability checker
Expand Down
109 changes: 109 additions & 0 deletions driver-core/src/main/com/mongodb/internal/ExceptionUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed 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 com.mongodb.internal;

import com.mongodb.MongoCommandException;
import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonNumber;
import org.bson.BsonString;
import org.bson.BsonValue;

import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE;

/**
* <p>This class is not part of the public API and may be removed or changed at any time</p>
*/
public final class ExceptionUtils {
public static final class MongoCommandExceptionUtils {
public static int extractErrorCode(final BsonDocument response) {
return extractErrorCodeAsBson(response).intValue();
}

public static String extractErrorCodeName(final BsonDocument response) {
return extractErrorCodeNameAsBson(response).getValue();
}

public static BsonArray extractErrorLabelsAsBson(final BsonDocument response) {
return response.getArray("errorLabels", new BsonArray());
}

private static BsonNumber extractErrorCodeAsBson(final BsonDocument response) {
return response.getNumber("code", new BsonInt32(-1));
}

private static BsonString extractErrorCodeNameAsBson(final BsonDocument response) {
return response.getString("codeName", new BsonString(""));
}

/**
* Constructs a {@link MongoCommandException} with the data from the {@code original} redacted for security purposes.
*/
public static MongoCommandException redacted(final MongoCommandException original) {
BsonDocument originalResponse = original.getResponse();
BsonDocument redactedResponse = new BsonDocument();
for (SecurityInsensitiveResponseField field : SecurityInsensitiveResponseField.values()) {
redactedResponse.append(field.fieldName(), field.fieldValue(originalResponse));
}
MongoCommandException result = new MongoCommandException(redactedResponse, original.getServerAddress());
result.setStackTrace(original.getStackTrace());
return result;
}

@VisibleForTesting(otherwise = PRIVATE)
public enum SecurityInsensitiveResponseField {
CODE("code", MongoCommandExceptionUtils::extractErrorCodeAsBson),
CODE_NAME("codeName", MongoCommandExceptionUtils::extractErrorCodeNameAsBson),
ERROR_LABELS("errorLabels", MongoCommandExceptionUtils::extractErrorLabelsAsBson);

private final String fieldName;
private final Function<BsonDocument, BsonValue> fieldValueExtractor;

SecurityInsensitiveResponseField(final String fieldName, final Function<BsonDocument, BsonValue> fieldValueExtractor) {
this.fieldName = fieldName;
this.fieldValueExtractor = fieldValueExtractor;
}

String fieldName() {
return fieldName;
}

BsonValue fieldValue(final BsonDocument response) {
return fieldValueExtractor.apply(response);
}

@VisibleForTesting(otherwise = PRIVATE)
public static Set<String> fieldNames() {
return Stream.of(SecurityInsensitiveResponseField.values())
.map(SecurityInsensitiveResponseField::fieldName)
.collect(Collectors.toSet());
}
}

private MongoCommandExceptionUtils() {
}
}

private ExceptionUtils() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.mongodb.connection.ClusterId;
import com.mongodb.connection.ConnectionDescription;
import com.mongodb.event.CommandListener;
import com.mongodb.internal.ExceptionUtils.MongoCommandExceptionUtils;
import com.mongodb.internal.logging.LogMessage;
import com.mongodb.internal.logging.LogMessage.Entry;
import com.mongodb.internal.logging.StructuredLogger;
Expand Down Expand Up @@ -124,9 +125,7 @@ public void sendStartedEvent() {
public void sendFailedEvent(final Throwable t) {
Throwable commandEventException = t;
if (t instanceof MongoCommandException && redactionRequired) {
MongoCommandException originalCommandException = (MongoCommandException) t;
commandEventException = new MongoCommandException(new BsonDocument(), originalCommandException.getServerAddress());
commandEventException.setStackTrace(t.getStackTrace());
commandEventException = MongoCommandExceptionUtils.redacted((MongoCommandException) t);
}
long elapsedTimeNanos = System.nanoTime() - startTimeNanos;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed 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 com.mongodb.internal;

import com.mongodb.MongoCommandException;
import com.mongodb.ServerAddress;
import com.mongodb.internal.ExceptionUtils.MongoCommandExceptionUtils;
import org.bson.BsonArray;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonString;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.util.HashSet;

import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

final class ExceptionUtilsTest {
@Nested
final class MongoCommandExceptionUtilsTest {
@Test
void redacted() {
MongoCommandException original = new MongoCommandException(
new BsonDocument("ok", BsonBoolean.FALSE)
.append("code", new BsonInt32(26))
.append("codeName", new BsonString("TimeoutError"))
.append("errorLabels", new BsonArray(asList(new BsonString("label"), new BsonString("label2"))))
.append("errmsg", new BsonString("err msg")),
new ServerAddress());
MongoCommandException redacted = MongoCommandExceptionUtils.redacted(original);
assertArrayEquals(original.getStackTrace(), redacted.getStackTrace());
String message = redacted.getMessage();
assertTrue(message.contains("26"));
assertTrue(message.contains("TimeoutError"));
assertTrue(message.contains("label"));
assertFalse(message.contains("err msg"));
assertTrue(redacted.getErrorMessage().isEmpty());
assertEquals(26, redacted.getErrorCode());
assertEquals("TimeoutError", redacted.getErrorCodeName());
assertEquals(new HashSet<>(asList("label", "label2")), redacted.getErrorLabels());
assertEquals(MongoCommandExceptionUtils.SecurityInsensitiveResponseField.fieldNames(), redacted.getResponse().keySet());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import com.mongodb.connection.StreamFactory
import com.mongodb.event.CommandFailedEvent
import com.mongodb.event.CommandStartedEvent
import com.mongodb.event.CommandSucceededEvent
import com.mongodb.internal.ExceptionUtils.MongoCommandExceptionUtils
import com.mongodb.internal.IgnorableRequestContext
import com.mongodb.internal.session.SessionContext
import com.mongodb.internal.validator.NoOpFieldNameValidator
Expand Down Expand Up @@ -875,7 +876,7 @@ class InternalStreamConnectionSpecification extends Specification {
]
}

def 'should send failed event with elided exception in failed security-sensitive commands'() {
def 'should send failed event with redacted exception in failed security-sensitive commands'() {
given:
def connection = getOpenedConnection()
def commandMessage = new CommandMessage(cmdNamespace, securitySensitiveCommand, fieldNameValidator, primary(), messageSettings,
Expand All @@ -893,7 +894,7 @@ class InternalStreamConnectionSpecification extends Specification {
CommandFailedEvent failedEvent = commandListener.getEvents().get(1)
failedEvent.throwable.class == MongoCommandException
MongoCommandException e = failedEvent.throwable
e.response == new BsonDocument()
MongoCommandExceptionUtils.SecurityInsensitiveResponseField.fieldNames().containsAll(e.getResponse().keySet())

where:
securitySensitiveCommand << [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.mongodb.client.unified;

import com.mongodb.MongoCommandException;
import com.mongodb.internal.ExceptionUtils.MongoCommandExceptionUtils;
import com.mongodb.internal.logging.LogMessage;
import org.bson.BsonArray;
import org.bson.BsonBoolean;
Expand Down Expand Up @@ -79,7 +80,9 @@ static BsonDocument asDocument(final LogMessage message) {
}

private static boolean exceptionIsRedacted(final Throwable exception) {
return exception instanceof MongoCommandException && ((MongoCommandException) exception).getResponse().isEmpty();
return exception instanceof MongoCommandException
&& MongoCommandExceptionUtils.SecurityInsensitiveResponseField.fieldNames()
.containsAll(((MongoCommandException) exception).getResponse().keySet());
}

private static BsonValue asBsonValue(final Object value) {
Expand Down