Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
240af4c
Add remaining environments (azure, gcp), evergreen testing, API namin…
katcharov Apr 19, 2024
b84ca99
Add remaining tests, refactor, increase GCP test machine
katcharov Apr 22, 2024
df3ef8d
Cleanup, update since annotations, updates to match spec API
katcharov Apr 23, 2024
581ae2e
Test fixes
katcharov Apr 23, 2024
00e4c15
PR fixes
katcharov Apr 23, 2024
6898b4f
Remove admin credentials
katcharov Apr 24, 2024
2621ae8
PR fixes
katcharov Apr 25, 2024
c842d22
PR fixes
katcharov Apr 25, 2024
3cea409
Apply suggestions from code review
katcharov Apr 26, 2024
e883279
Update driver-core/src/main/com/mongodb/MongoCredential.java
katcharov Apr 26, 2024
bc30a2f
Update driver-core/src/main/com/mongodb/internal/authentication/Crede…
katcharov Apr 26, 2024
d856d84
Implement OIDC map value splitting
katcharov Apr 26, 2024
479fcdd
PR fixes, doc updates
katcharov Apr 26, 2024
4a844b1
PR fixes for OIDC feature branch
katcharov Apr 26, 2024
cc1c7ec
Update connection-string latest specifications/pull/1569
katcharov Apr 26, 2024
678d7b7
PR fixes
katcharov Apr 26, 2024
fcb65dc
PR fixes
katcharov Apr 26, 2024
71b3846
PR fixes
katcharov Apr 26, 2024
f6cb3da
PR Fixes
katcharov Apr 26, 2024
be63643
PR fixes
katcharov Apr 27, 2024
0532a87
PR fixes
katcharov Apr 29, 2024
761918e
PR fixes: mustDecodeNonOidcAsWhole
katcharov Apr 29, 2024
7428fd1
PR fixes
katcharov Apr 29, 2024
fcdab29
Update driver-sync/src/test/functional/com/mongodb/internal/connectio…
katcharov Apr 29, 2024
8971a79
Doc fix
katcharov Apr 29, 2024
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add remaining tests, refactor, increase GCP test machine
  • Loading branch information
katcharov committed Apr 23, 2024
commit b84ca992208fd73ed2568145280d2db0ae6da005
1 change: 1 addition & 0 deletions .evergreen/.evg.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2174,6 +2174,7 @@ task_groups:
binary: bash
env:
GCPOIDC_VMNAME_PREFIX: "JAVA_DRIVER"
GCPKMS_MACHINETYPE: "e2-medium" # comparable elapsed time to Azure; default was starved, caused timeouts
args:
- ${DRIVERS_TOOLS}/.evergreen/auth_oidc/gcp/setup.sh
teardown_task:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,31 +33,32 @@
*/
public final class GcpCredentialHelper {

public static CredentialInfo fetchGcpCredentialInfo(final String resource) {
String endpoint = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?" + resource;
return new CredentialInfo(
getBsonDocument(endpoint).getValue(),
Duration.ZERO);
}

public static BsonDocument obtainFromEnvironment() {
String endpoint = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token";
return new BsonDocument("accessToken", getBsonDocument(endpoint));
}

private static BsonString getBsonDocument(final String endpoint) {
Map<String, String> header = new HashMap<>();
header.put("Metadata-Flavor", "Google");
header.put("Accept", "application/json");
String response = getHttpContents("GET", endpoint, header);
BsonDocument responseDocument = BsonDocument.parse(response);
if (responseDocument.containsKey("access_token")) {
return responseDocument.get("access_token").asString();
return new BsonDocument("accessToken", responseDocument.get("access_token"));
} else {
throw new MongoClientException("access_token is missing from GCE metadata response. Full response is ''" + response);
throw new MongoClientException("access_token is missing from GCE metadata response. Full response is ''"
+ response);
}
}

public static CredentialInfo fetchGcpCredentialInfo(final String resource) {
String endpoint = "http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience="
+ resource;
Map<String, String> header = new HashMap<>();
header.put("Metadata-Flavor", "Google");;
String response = getHttpContents("GET", endpoint, header);
return new CredentialInfo(
new BsonString(response).getValue(),
Duration.ZERO);
}

private GcpCredentialHelper() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.bson.BsonString;
import org.bson.Document;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -71,7 +72,6 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static util.ThreadTestHelpers.executeAll;

Expand Down Expand Up @@ -201,7 +201,7 @@ public void test2p2RequestCallbackReturnsNull() {
//noinspection ConstantConditions
OidcCallback callback = (context) -> null;
MongoClientSettings clientSettings = this.createSettings(callback);
performFind(clientSettings, MongoConfigurationException.class,
assertFindFails(clientSettings, MongoConfigurationException.class,
"Result of callback must not be null");
}

Expand All @@ -216,12 +216,9 @@ public void test2p3CallbackReturnsMissingData() {
// we ensure that the error is propagated
MongoClientSettings clientSettings = createSettings(callback);
try (MongoClient mongoClient = createMongoClient(clientSettings)) {
try {
performFind(mongoClient);
fail();
} catch (Exception e) {
assertCause(IllegalArgumentException.class, "accessToken can not be null", e);
}
assertCause(IllegalArgumentException.class,
"accessToken can not be null",
() -> performFind(mongoClient));
}
}

Expand All @@ -230,13 +227,9 @@ public void test2p4InvalidClientConfigurationWithCallback() {
String uri = getOidcUri() + "&authMechanismProperties=ENVIRONMENT:" + getOidcEnv();
MongoClientSettings settings = createSettings(
uri, createCallback(), null, OIDC_CALLBACK_KEY);
try {
performFind(settings);
fail();
} catch (Exception e) {
assertCause(IllegalArgumentException.class,
"OIDC_CALLBACK must not be specified when ENVIRONMENT is specified", e);
}
assertCause(IllegalArgumentException.class,
"OIDC_CALLBACK must not be specified when ENVIRONMENT is specified",
() -> performFind(settings));
}

@Test
Expand Down Expand Up @@ -282,13 +275,9 @@ public void test3p2AuthFailsWithoutCachedToken() {
(x) -> new OidcCallbackResult("invalid_token", Duration.ZERO);
MongoClientSettings clientSettings = createSettings(callback);
try (MongoClient mongoClient = createMongoClient(clientSettings)) {
try {
performFind(mongoClient);
fail();
} catch (Exception e) {
assertCause(MongoCommandException.class,
"Command failed with error 18 (AuthenticationFailed):", e);
}
assertCause(MongoCommandException.class,
"Command failed with error 18 (AuthenticationFailed):",
() -> performFind(mongoClient));
}
}

Expand Down Expand Up @@ -321,8 +310,6 @@ public void test3p3UnexpectedErrorDoesNotClearCache() {
}
}

// TODO-OIDC reinstate 2 broken(?) tests in mongodb-oidc-no-retry.json

@Test
public void test4p1Reauthentication() {
TestCallback callback = createCallback();
Expand All @@ -335,6 +322,59 @@ public void test4p1Reauthentication() {
assertEquals(2, callback.invocations.get());
}

@Test
public void test4p2ReadCommandsFailIfReauthenticationFails() {
// Create a `MongoClient` whose OIDC callback returns one good token
// and then bad tokens after the first call.
TestCallback wrappedCallback = createCallback();
OidcCallback callback = (context) -> {
OidcCallbackResult result1 = wrappedCallback.callback(context);
return new OidcCallbackResult(
wrappedCallback.getInvocations() > 1 ? "bad" : result1.getAccessToken(),
Duration.ZERO,
null);
};
MongoClientSettings clientSettings = createSettings(callback);
try (MongoClient mongoClient = createMongoClient(clientSettings)) {
performFind(mongoClient);
failCommand(391, 1, "find");
assertCause(MongoCommandException.class,
"Command failed with error 18",
() -> performFind(mongoClient));
}
assertEquals(2, wrappedCallback.invocations.get());
}

@Test
public void test4p3WriteCommandsFailIfReauthenticationFails() {
// Create a `MongoClient` whose OIDC callback returns one good token
// and then bad tokens after the first call.
TestCallback wrappedCallback = createCallback();
OidcCallback callback = (context) -> {
OidcCallbackResult result1 = wrappedCallback.callback(context);
return new OidcCallbackResult(
wrappedCallback.getInvocations() > 1 ? "bad" : result1.getAccessToken(),
Duration.ZERO,
null);
};
MongoClientSettings clientSettings = createSettings(callback);
try (MongoClient mongoClient = createMongoClient(clientSettings)) {
performInsert(mongoClient);
failCommand(391, 1, "insert");
assertCause(MongoCommandException.class,
"Command failed with error 18",
() -> performInsert(mongoClient));
}
assertEquals(2, wrappedCallback.invocations.get());
}

private static void performInsert(final MongoClient mongoClient) {
mongoClient
.getDatabase("test")
.getCollection("test")
.insertOne(Document.parse("{ x: 1 }"));
}

@Test
public void test5p1Azure() {
assumeTrue(getOidcEnv().equals("azure"));
Expand Down Expand Up @@ -410,7 +450,7 @@ public void testh1p5MultiplePrincipalNoUser() {
// Create an OIDC configured client with `MONGODB_URI_MULTI` and no username.
MongoClientSettings clientSettings = createSettingsMulti(null, createHumanCallback());
// Assert that a `find` operation fails.
performFind(clientSettings, MongoCommandException.class, "Authentication failed");
assertFindFails(clientSettings, MongoCommandException.class, "Authentication failed");
}

@Test
Expand All @@ -420,15 +460,15 @@ public void testh1p6AllowedHostsBlocked() {
//- Assert that a ``find`` operation fails with a client-side error.
MongoClientSettings clientSettings1 = createSettings(getOidcUri(),
createHumanCallback(), null, OIDC_HUMAN_CALLBACK_KEY, Collections.emptyList());
performFind(clientSettings1, MongoSecurityException.class, "not permitted by ALLOWED_HOSTS");
assertFindFails(clientSettings1, MongoSecurityException.class, "not permitted by ALLOWED_HOSTS");

//- Create a client that uses the URL
// ``mongodb://localhost/?authMechanism=MONGODB-OIDC&ignored=example.com``, a
// human callback, and an ``ALLOWED_HOSTS`` that contains ``["example.com"]``.
//- Assert that a ``find`` operation fails with a client-side error.
MongoClientSettings clientSettings2 = createSettings(getOidcUri() + "&ignored=example.com",
createHumanCallback(), null, OIDC_HUMAN_CALLBACK_KEY, Arrays.asList("example.com"));
performFind(clientSettings2, MongoSecurityException.class, "not permitted by ALLOWED_HOSTS");
assertFindFails(clientSettings2, MongoSecurityException.class, "not permitted by ALLOWED_HOSTS");
}

// Not a prose test
Expand Down Expand Up @@ -485,14 +525,14 @@ public void testh2p2HumanCallbackReturnsMissingData() {
assumeTestEnvironment();
//noinspection ConstantConditions
OidcCallback callbackNull = (context) -> null;
performFind(createHumanSettings(callbackNull, null),
assertFindFails(createHumanSettings(callbackNull, null),
MongoConfigurationException.class,
"Result of callback must not be null");

//noinspection ConstantConditions
OidcCallback callback =
(context) -> new OidcCallbackResult(null, Duration.ZERO);
performFind(createHumanSettings(callback, null),
assertFindFails(createHumanSettings(callback, null),
IllegalArgumentException.class,
"accessToken can not be null");
}
Expand All @@ -503,7 +543,7 @@ public void testRefreshTokenAbsent() {
// additionally, check validation for refresh in machine workflow:
OidcCallback callbackMachineRefresh =
(context) -> new OidcCallbackResult("access", Duration.ZERO, "exists");
performFind(createSettings(callbackMachineRefresh),
assertFindFails(createSettings(callbackMachineRefresh),
MongoConfigurationException.class,
"Refresh token must only be provided in human workflow");
}
Expand Down Expand Up @@ -549,7 +589,7 @@ public void testh3p2NoSpecAuthIfNoCachedToken() {
failCommand(18, 1, "saslStart");
TestListener listener = new TestListener();
TestCommandListener commandListener = new TestCommandListener(listener);
performFind(createHumanSettings(createHumanCallback(), commandListener),
assertFindFails(createHumanSettings(createHumanCallback(), commandListener),
MongoCommandException.class,
"Command failed with error 18");
assertEquals(Arrays.asList(
Expand Down Expand Up @@ -833,7 +873,7 @@ private void performFind(final MongoClientSettings settings) {
}
}

private <T extends Throwable> void performFind(
private <T extends Throwable> void assertFindFails(
final MongoClientSettings settings,
final Class<T> expectedExceptionOrCause,
final String expectedMessage) {
Expand All @@ -852,21 +892,15 @@ private void performFind(final MongoClient mongoClient) {

private static <T extends Throwable> void assertCause(
final Class<T> expectedCause, final String expectedMessageFragment, final Executable e) {
Throwable actualException = assertThrows(Throwable.class, e);
assertCause(expectedCause, expectedMessageFragment, actualException);
}

private static <T extends Throwable> void assertCause(
final Class<T> expectedCause, final String expectedMessageFragment, final Throwable actualException) {
Throwable cause = actualException;
Throwable cause = assertThrows(Throwable.class, e);
while (cause.getCause() != null) {
cause = cause.getCause();
}
if (!expectedCause.isInstance(cause)) {
throw new AssertionFailedError("Unexpected cause: " + actualException.getClass(), actualException);
throw new AssertionFailedError("Unexpected cause: " + assertThrows(Throwable.class, e).getClass(), assertThrows(Throwable.class, e));
}
if (!cause.getMessage().contains(expectedMessageFragment)) {
throw new AssertionFailedError("Unexpected message", actualException);
throw new AssertionFailedError("Unexpected message", assertThrows(Throwable.class, e));
}
}

Expand Down