Skip to content

Commit 2a70634

Browse files
committed
Preserve error code, code name, and error labels when redacting command monitoring/logging
JAVA-4843
1 parent c0e86e0 commit 2a70634

File tree

6 files changed

+186
-17
lines changed

6 files changed

+186
-17
lines changed

driver-core/src/main/com/mongodb/MongoCommandException.java

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@
1616

1717
package com.mongodb;
1818

19-
import org.bson.BsonArray;
2019
import org.bson.BsonDocument;
21-
import org.bson.BsonInt32;
2220
import org.bson.BsonString;
2321
import org.bson.codecs.BsonDocumentCodec;
2422
import org.bson.codecs.EncoderContext;
2523
import org.bson.json.JsonWriter;
2624

2725
import java.io.StringWriter;
2826

27+
import static com.mongodb.internal.Exceptions.MongoCommandExceptions.extractErrorCode;
28+
import static com.mongodb.internal.Exceptions.MongoCommandExceptions.extractErrorCodeName;
29+
import static com.mongodb.internal.Exceptions.MongoCommandExceptions.extractErrorLabelsAsBson;
2930
import static java.lang.String.format;
3031

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

5657
/**
@@ -109,14 +110,6 @@ private static String extractErrorCodeAndName(final BsonDocument response) {
109110
}
110111
}
111112

112-
private static int extractErrorCode(final BsonDocument response) {
113-
return response.getNumber("code", new BsonInt32(-1)).intValue();
114-
}
115-
116-
private static String extractErrorCodeName(final BsonDocument response) {
117-
return response.getString("codeName", new BsonString("")).getValue();
118-
}
119-
120113
private static String extractErrorMessage(final BsonDocument response) {
121114
String errorMessage = response.getString("errmsg", new BsonString("")).getValue();
122115
// Satisfy nullability checker
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.mongodb.internal;
18+
19+
import com.mongodb.MongoCommandException;
20+
import org.bson.BsonArray;
21+
import org.bson.BsonDocument;
22+
import org.bson.BsonInt32;
23+
import org.bson.BsonNumber;
24+
import org.bson.BsonString;
25+
import org.bson.BsonValue;
26+
27+
import java.util.Set;
28+
import java.util.function.Function;
29+
import java.util.stream.Collectors;
30+
import java.util.stream.Stream;
31+
32+
import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE;
33+
34+
/**
35+
* <p>This class is not part of the public API and may be removed or changed at any time</p>
36+
*/
37+
public final class Exceptions {
38+
public static final class MongoCommandExceptions {
39+
public static int extractErrorCode(final BsonDocument response) {
40+
return extractErrorCodeAsBson(response).intValue();
41+
}
42+
43+
public static String extractErrorCodeName(final BsonDocument response) {
44+
return extractErrorCodeNameAsBson(response).getValue();
45+
}
46+
47+
public static BsonArray extractErrorLabelsAsBson(final BsonDocument response) {
48+
return response.getArray("errorLabels", new BsonArray());
49+
}
50+
51+
/**
52+
* Constructs a {@link MongoCommandException} with the data from the {@code original} redacted for security purposes.
53+
*/
54+
public static MongoCommandException redacted(final MongoCommandException original) {
55+
BsonDocument originalResponse = original.getResponse();
56+
BsonDocument redactedResponse = new BsonDocument();
57+
for (SecurityInsensitiveResponseField field : SecurityInsensitiveResponseField.values()) {
58+
redactedResponse.append(field.fieldName(), field.fieldValue(originalResponse));
59+
}
60+
MongoCommandException result = new MongoCommandException(redactedResponse, original.getServerAddress());
61+
result.setStackTrace(original.getStackTrace());
62+
return result;
63+
}
64+
65+
private static BsonNumber extractErrorCodeAsBson(final BsonDocument response) {
66+
return response.getNumber("code", new BsonInt32(-1));
67+
}
68+
69+
private static BsonString extractErrorCodeNameAsBson(final BsonDocument response) {
70+
return response.getString("codeName", new BsonString(""));
71+
}
72+
73+
@VisibleForTesting(otherwise = PRIVATE)
74+
public enum SecurityInsensitiveResponseField {
75+
CODE("code", MongoCommandExceptions::extractErrorCodeAsBson),
76+
CODE_NAME("codeName", MongoCommandExceptions::extractErrorCodeNameAsBson),
77+
ERROR_LABELS("errorLabels", MongoCommandExceptions::extractErrorLabelsAsBson);
78+
79+
private final String fieldName;
80+
private final Function<BsonDocument, BsonValue> fieldValueExtractor;
81+
82+
SecurityInsensitiveResponseField(final String fieldName, final Function<BsonDocument, BsonValue> fieldValueExtractor) {
83+
this.fieldName = fieldName;
84+
this.fieldValueExtractor = fieldValueExtractor;
85+
}
86+
87+
String fieldName() {
88+
return fieldName;
89+
}
90+
91+
BsonValue fieldValue(final BsonDocument response) {
92+
return fieldValueExtractor.apply(response);
93+
}
94+
95+
@VisibleForTesting(otherwise = PRIVATE)
96+
public static Set<String> fieldNames() {
97+
return Stream.of(SecurityInsensitiveResponseField.values())
98+
.map(SecurityInsensitiveResponseField::fieldName)
99+
.collect(Collectors.toSet());
100+
}
101+
}
102+
103+
private MongoCommandExceptions() {
104+
}
105+
}
106+
107+
private Exceptions() {
108+
}
109+
}

driver-core/src/main/com/mongodb/internal/connection/LoggingCommandEventSender.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.mongodb.connection.ClusterId;
2323
import com.mongodb.connection.ConnectionDescription;
2424
import com.mongodb.event.CommandListener;
25+
import com.mongodb.internal.Exceptions.MongoCommandExceptions;
2526
import com.mongodb.internal.logging.LogMessage;
2627
import com.mongodb.internal.logging.LogMessage.Entry;
2728
import com.mongodb.internal.logging.StructuredLogger;
@@ -124,9 +125,7 @@ public void sendStartedEvent() {
124125
public void sendFailedEvent(final Throwable t) {
125126
Throwable commandEventException = t;
126127
if (t instanceof MongoCommandException && redactionRequired) {
127-
MongoCommandException originalCommandException = (MongoCommandException) t;
128-
commandEventException = new MongoCommandException(new BsonDocument(), originalCommandException.getServerAddress());
129-
commandEventException.setStackTrace(t.getStackTrace());
128+
commandEventException = MongoCommandExceptions.redacted((MongoCommandException) t);
130129
}
131130
long elapsedTimeNanos = System.nanoTime() - startTimeNanos;
132131

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.mongodb.internal;
17+
18+
import com.mongodb.MongoCommandException;
19+
import com.mongodb.ServerAddress;
20+
import com.mongodb.internal.Exceptions.MongoCommandExceptions;
21+
import org.bson.BsonArray;
22+
import org.bson.BsonBoolean;
23+
import org.bson.BsonDocument;
24+
import org.bson.BsonInt32;
25+
import org.bson.BsonString;
26+
import org.junit.jupiter.api.Nested;
27+
import org.junit.jupiter.api.Test;
28+
29+
import java.util.HashSet;
30+
31+
import static java.util.Arrays.asList;
32+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
33+
import static org.junit.jupiter.api.Assertions.assertEquals;
34+
import static org.junit.jupiter.api.Assertions.assertFalse;
35+
import static org.junit.jupiter.api.Assertions.assertTrue;
36+
37+
final class ExceptionsTest {
38+
@Nested
39+
final class MongoCommandExceptionsTest {
40+
@Test
41+
void redacted() {
42+
MongoCommandException original = new MongoCommandException(
43+
new BsonDocument("ok", BsonBoolean.FALSE)
44+
.append("code", new BsonInt32(26))
45+
.append("codeName", new BsonString("TimeoutError"))
46+
.append("errorLabels", new BsonArray(asList(new BsonString("label"), new BsonString("label2"))))
47+
.append("errmsg", new BsonString("err msg")),
48+
new ServerAddress());
49+
MongoCommandException redacted = MongoCommandExceptions.redacted(original);
50+
assertArrayEquals(original.getStackTrace(), redacted.getStackTrace());
51+
String message = redacted.getMessage();
52+
assertTrue(message.contains("26"));
53+
assertTrue(message.contains("TimeoutError"));
54+
assertTrue(message.contains("label"));
55+
assertFalse(message.contains("err msg"));
56+
assertTrue(redacted.getErrorMessage().isEmpty());
57+
assertEquals(26, redacted.getErrorCode());
58+
assertEquals("TimeoutError", redacted.getErrorCodeName());
59+
assertEquals(new HashSet<>(asList("label", "label2")), redacted.getErrorLabels());
60+
assertEquals(MongoCommandExceptions.SecurityInsensitiveResponseField.fieldNames(), redacted.getResponse().keySet());
61+
}
62+
}
63+
}

driver-core/src/test/unit/com/mongodb/internal/connection/InternalStreamConnectionSpecification.groovy

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import com.mongodb.connection.StreamFactory
4040
import com.mongodb.event.CommandFailedEvent
4141
import com.mongodb.event.CommandStartedEvent
4242
import com.mongodb.event.CommandSucceededEvent
43+
import com.mongodb.internal.Exceptions
4344
import com.mongodb.internal.IgnorableRequestContext
4445
import com.mongodb.internal.session.SessionContext
4546
import com.mongodb.internal.validator.NoOpFieldNameValidator
@@ -875,7 +876,7 @@ class InternalStreamConnectionSpecification extends Specification {
875876
]
876877
}
877878

878-
def 'should send failed event with elided exception in failed security-sensitive commands'() {
879+
def 'should send failed event with redacted exception in failed security-sensitive commands'() {
879880
given:
880881
def connection = getOpenedConnection()
881882
def commandMessage = new CommandMessage(cmdNamespace, securitySensitiveCommand, fieldNameValidator, primary(), messageSettings,
@@ -893,7 +894,8 @@ class InternalStreamConnectionSpecification extends Specification {
893894
CommandFailedEvent failedEvent = commandListener.getEvents().get(1)
894895
failedEvent.throwable.class == MongoCommandException
895896
MongoCommandException e = failedEvent.throwable
896-
e.response == new BsonDocument()
897+
Exceptions.MongoCommandExceptions.SecurityInsensitiveResponseField.fieldNames()
898+
.containsAll(e.getResponse().keySet())
897899

898900
where:
899901
securitySensitiveCommand << [

driver-sync/src/test/functional/com/mongodb/client/unified/LogMatcher.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.mongodb.client.unified;
1818

1919
import com.mongodb.MongoCommandException;
20+
import com.mongodb.internal.Exceptions.MongoCommandExceptions;
2021
import com.mongodb.internal.logging.LogMessage;
2122
import org.bson.BsonArray;
2223
import org.bson.BsonBoolean;
@@ -79,7 +80,9 @@ static BsonDocument asDocument(final LogMessage message) {
7980
}
8081

8182
private static boolean exceptionIsRedacted(final Throwable exception) {
82-
return exception instanceof MongoCommandException && ((MongoCommandException) exception).getResponse().isEmpty();
83+
return exception instanceof MongoCommandException
84+
&& MongoCommandExceptions.SecurityInsensitiveResponseField.fieldNames()
85+
.containsAll(((MongoCommandException) exception).getResponse().keySet());
8386
}
8487

8588
private static BsonValue asBsonValue(final Object value) {

0 commit comments

Comments
 (0)