Skip to content

CDRIVER-5580 support embedded URI in connection string options #1914

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 18 commits into from
Mar 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
66dfa3c
Support slashes in URI query component
eramongodb Mar 12, 2025
72c3e32
Delimit auth mechanism properties by comma first
eramongodb Mar 12, 2025
01ca6c0
Add regression tests for URI and auth mech prop parsing
eramongodb Mar 12, 2025
7342b9f
Merge remote-tracking branch 'upstream/master' into cdriver-5580
eramongodb Mar 14, 2025
c81b9a1
Revert to supporting most reserved characters in the userinfo component
eramongodb Mar 14, 2025
4043cf6
Update and fix NEWS entries
eramongodb Mar 14, 2025
ba695c4
Merge remote-tracking branch 'upstream/master' into cdriver-5580
eramongodb Mar 17, 2025
57372ba
Fix typo of authMechanism
eramongodb Mar 17, 2025
3856d40
Merge remote-tracking branch 'upstream/master' into cdriver-5580
eramongodb Mar 18, 2025
948ca07
Fix indentation level of auth cred entries in NEWS
eramongodb Mar 18, 2025
af9cc3e
Use consistent syntax to describe URI options
eramongodb Mar 18, 2025
628370e
Merge remote-tracking branch 'upstream/master' into cdriver-5580
eramongodb Mar 19, 2025
ac6e218
Merge remote-tracking branch 'upstream/master' into cdriver-5580
eramongodb Mar 20, 2025
86eb1ac
Merge remote-tracking branch 'upstream/master' into cdriver-5580
eramongodb Mar 20, 2025
c6d8e1b
Merge remote-tracking branch 'upstream/master' into cdriver-5580
eramongodb Mar 21, 2025
c24d8a0
Summarize NEWS entry for authentication-related credentials validatio…
eramongodb Mar 21, 2025
39d949e
Restore NEWS entry for authSource default behavior
eramongodb Mar 21, 2025
af61b28
Merge remote-tracking branch 'upstream/master' into cdriver-5580
eramongodb Mar 24, 2025
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
20 changes: 5 additions & 15 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,12 @@ Unreleased (2.0.0)
* `mongoc_server_description_host` changes the return type from `mongoc_host_list_t *` to `const mongoc_host_list_t *`.
* URI authentication credentials validation (only applicable during creation of a new `mongoc_uri_t` object from a connection string):
* The requirement that a username is non-empty when specified is now enforced regardless of authentication mechanism.
* `authMechanism` is now validated and returns a client error for invalid or unsupported values.
* `authSource` is now validated and returns a client error for invalid or unsupported values for the specified `authMechanism`.
* `authSource` is now correctly defaulted to `"$external"` for MONGODB-AWS (instead of the database name or `"admin"`).
* The requirement that a password is provided is now enforced when the authentication mechanism is specified for:
* PLAIN
* SCRAM-SHA-1
* SCRAM-SHA-256
* The requirement that neither or both a username and password is provided (optionally with a `AWS_SESSION_TOKEN`) is now enforced for MONGODB-AWS.
* `authMechanismProperties` is now prohibited (instead of ignored) when the authentication mechanism is specified for:
* PLAIN
* SCRAM-SHA-1
* SCRAM-SHA-256
* MONGODB-X509
* `authMechanismProperties` is now validated and returns a client error for invalid or unsupported fields when the authentication mechanism is specified for:
* GSSAPI: supported fields are SERVICE_NAME, CANONICALIZE_HOST_NAME, SERVICE_REALM, and SERVICE_HOST.
* MONGODB-AWS: supported fields are AWS_SESSION_TOKEN.
* `authMechanism` is now validated and returns a client error for invalid or unsupported values.
* Requirements for the inclusion, exclusion, and supported values of authentication-related URI components (e.g. username and password), options (e.g. `authSource`), and mechanism properties (e.g. `authMechanismProperties` and its key-value pairs) are now validated and return a client error when able for invalid or unsupported configurations according to the specified authentication mechanism (`authMechanism`).
* `authMechanismProperties` now correctly supports `':'` within property values.
* Old behavior: `authMechanismProperties=A:B,C:D:E,F:G` is parsed as `{'A': 'B', 'C': 'D:E,F:G'}`.
* New behavior: `authMechanismProperties=A:B,C:D:E,F:G` is parsed as `{'A': 'B': 'C': 'D:E', 'F': 'G'}`.
* Calling `mongoc_bulk_operation_execute` on the same `mongoc_bulk_operation_t` repeatedly is an error. Previously this was only discouraged in documentation.

## Removals
Expand Down
199 changes: 123 additions & 76 deletions src/libmongoc/src/mongoc/mongoc-uri.c
Original file line number Diff line number Diff line change
Expand Up @@ -516,25 +516,46 @@ mongoc_uri_parse_database (mongoc_uri_t *uri, const char *str, const char **end)
static bool
mongoc_uri_parse_auth_mechanism_properties (mongoc_uri_t *uri, const char *str)
{
char *field;
char *value;
const char *end_scan;
bson_t properties;

bson_init (&properties);
bson_t properties = BSON_INITIALIZER;

/* build up the properties document */
while ((field = scan_to_unichar (str, ':', "&", &end_scan))) {
// Key-value pairs are delimited by ','.
for (char *kvp; (kvp = scan_to_unichar (str, ',', "", &end_scan)); bson_free (kvp)) {
str = end_scan + 1;
if (!(value = scan_to_unichar (str, ',', ":&", &end_scan))) {
value = bson_strdup (str);
str = "";
} else {
str = end_scan + 1;

char *const key = scan_to_unichar (kvp, ':', "", &end_scan);

// Found delimiter: split into key and value.
if (key) {
char *const value = bson_strdup (end_scan + 1);
BSON_APPEND_UTF8 (&properties, key, value);
bson_free (key);
bson_free (value);
}

// No delimiter: entire string is the key. Use empty string as value.
else {
BSON_APPEND_UTF8 (&properties, kvp, "");
}
}

// Last (or only) pair.
if (*str != '\0') {
char *const key = scan_to_unichar (str, ':', "", &end_scan);

// Found delimiter: split into key and value.
if (key) {
char *const value = bson_strdup (end_scan + 1);
BSON_APPEND_UTF8 (&properties, key, value);
bson_free (key);
bson_free (value);
}

// No delimiter: entire string is the key. Use empty string as value.
else {
BSON_APPEND_UTF8 (&properties, str, "");
}
bson_append_utf8 (&properties, field, -1, value, -1);
bson_free (field);
bson_free (value);
}

/* append our auth properties to our credentials */
Expand Down Expand Up @@ -1846,100 +1867,126 @@ mongoc_uri_parse_before_slash (mongoc_uri_t *uri, const char *before_slash, bson
static bool
mongoc_uri_parse (mongoc_uri_t *uri, const char *str, bson_error_t *error)
{
BSON_ASSERT_PARAM (uri);
BSON_ASSERT_PARAM (str);

char *before_slash = NULL;
const char *tmp;
const size_t str_len = strlen (str);

if (!bson_utf8_validate (str, strlen (str), false /* allow_null */)) {
if (!bson_utf8_validate (str, str_len, false /* allow_null */)) {
MONGOC_URI_ERROR (error, "%s", "Invalid UTF-8 in URI");
goto error;
return false;
}

// Save for later.
const char *const str_end = str + str_len;

// Parse and remove scheme and its delimiter.
// e.g. "mongodb://user:pass@host1:27017,host2:27018/database?key1=value1&key2=value2"
// ~~~~~~~~~~
if (!mongoc_uri_parse_scheme (uri, str, &str)) {
MONGOC_URI_ERROR (error, "%s", "Invalid URI Schema, expecting 'mongodb://' or 'mongodb+srv://'");
goto error;
return false;
}
// str -> "user:pass@host1:27017,host2:27018/database?key1=value1&key2=value2"

before_slash = scan_to_unichar (str, '/', "", &tmp);
if (!before_slash) {
// Handle cases of optional delimiting slash
char *userpass = NULL;
char *hosts = NULL;
// From this point forward, use this cursor to find the split between "userhosts" and "dbopts".
const char *cursor = str;

// Skip any "?"s that exist in the userpass
userpass = scan_to_unichar (str, '@', "", &tmp);
if (!userpass) {
// If none found, safely check for "?" indicating beginning of options
before_slash = scan_to_unichar (str, '?', "", &tmp);
} else {
const size_t userpass_len = (size_t) (tmp - str);
// Otherwise, see if options exist after userpass and concatenate result
hosts = scan_to_unichar (tmp, '?', "", &tmp);
// Remove userinfo and its delimiter.
// e.g. "user:pass@host1:27017,host2:27018/database?key1=value1&key2=value2"
// ~~~~~~~~~~
{
const char *tmp;

if (hosts) {
const size_t hosts_len = (size_t) (tmp - str) - userpass_len;
// Only ':' is permitted among RFC-3986 gen-delims (":/?#[]@") in userinfo.
// However, continue supporting these characters for backward compatibility, as permitted by the Connection String
// spec: for backwards-compatibility reasons, drivers MAY allow reserved characters other than "@" and ":" to be
// present in user information without percent-encoding.
char *userinfo = scan_to_unichar (cursor, '@', "", &tmp);

before_slash = bson_strndup (str, userpass_len + hosts_len);
}
if (userinfo) {
cursor = tmp + 1; // Consume userinfo delimiter.
bson_free (userinfo);
}

bson_free (userpass);
bson_free (hosts);
}
// cursor -> "host1:27017,host2:27018/database?key1=value1&key2=value2"

if (!before_slash) {
before_slash = bson_strdup (str);
str += strlen (before_slash);
} else {
str = tmp;
// Find either the optional auth database delimiter or the query delimiter.
// e.g. "host1:27017,host2:27018/database?key1=value1&key2=value2"
// ^
// e.g. "host1:27017,host2:27018?key1=value1&key2=value2"
// ^
{
const char *tmp;

// Only ':', '[', and ']' are permitted among RFC-3986 gen-delims (":/?#[]@") in hostinfo.
const char *const terminators = "/?#@";

char *hostinfo;

// Optional auth delimiter is present.
if ((hostinfo = scan_to_unichar (cursor, '/', terminators, &tmp))) {
cursor = tmp; // Include the delimiter.
bson_free (hostinfo);
}

// Query delimiter is present.
else if ((hostinfo = scan_to_unichar (cursor, '?', terminators, &tmp))) {
cursor = tmp; // Include the delimiter.
bson_free (hostinfo);
}

// Neither delimiter is present. Entire rest of string is part of hostinfo.
else {
cursor = str_end; // Jump to end of string.
BSON_ASSERT (*cursor == '\0');
}
}
// cursor -> "/database?key1=value1&key2=value2"

if (!mongoc_uri_parse_before_slash (uri, before_slash, error)) {
goto error;
// Parse "userhosts". e.g. "user:pass@host1:27017,host2:27018"
{
char *const userhosts = bson_strndup (str, (size_t) (cursor - str));
const bool ret = mongoc_uri_parse_before_slash (uri, userhosts, error);
bson_free (userhosts);
if (!ret) {
return false;
}
}

BSON_ASSERT (str);
// Parse "dbopts". e.g. "/database?key1=value1&key2=value2"
if (*cursor != '\0') {
BSON_ASSERT (*cursor == '/' || *cursor == '?');

if (*str) {
// Check for valid end of hostname delimeter (skip slash if necessary)
if (*str != '/' && *str != '?') {
MONGOC_URI_ERROR (error, "%s", "Expected end of hostname delimiter");
goto error;
}
// Parse the auth database.
if (*cursor == '/') {
++cursor; // Consume the delimiter.

if (*str == '/') {
// Try to parse database.
str++;
if (*str) {
if (!mongoc_uri_parse_database (uri, str, &str)) {
// No auth database may be present even if the delimiter is present.
// e.g. "mongodb://localhost:27017/"
if (*cursor != '\0') {
if (!mongoc_uri_parse_database (uri, cursor, &cursor)) {
MONGOC_URI_ERROR (error, "%s", "Invalid database name in URI");
goto error;
return false;
}
}
}

if (*str == '?') {
// Try to parse options.
str++;
if (*str) {
if (!mongoc_uri_parse_options (uri, str, false /* from DNS */, error)) {
goto error;
// Parse the query options.
if (*cursor == '?') {
++cursor; // Consume the delimiter.

// No options may be present even if the delimiter is present.
// e.g. "mongodb://localhost:27017?"
if (*cursor != '\0') {
if (!mongoc_uri_parse_options (uri, cursor, false /* from DNS */, error)) {
return false;
}
}
}
}

if (!mongoc_uri_finalize (uri, error)) {
goto error;
}

bson_free (before_slash);
return true;

error:
bson_free (before_slash);
return false;
return mongoc_uri_finalize (uri, error);
}


Expand Down
72 changes: 72 additions & 0 deletions src/libmongoc/tests/test-mongoc-uri.c
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,14 @@ test_mongoc_uri_new (void)
ASSERT_CMPSTR (mongoc_uri_get_auth_mechanism (uri), "SCRAM-SHA-1");
mongoc_uri_destroy (uri);

/* should recognize many reserved characters in the userpass for backward compatibility */
uri = mongoc_uri_new ("mongodb://user?#[]:pass?#[]@localhost?" MONGOC_URI_AUTHMECHANISM "=SCRAM-SHA-1");
ASSERT (uri);
ASSERT_CMPSTR (mongoc_uri_get_username (uri), "user?#[]");
ASSERT_CMPSTR (mongoc_uri_get_password (uri), "pass?#[]");
ASSERT_CMPSTR (mongoc_uri_get_auth_mechanism (uri), "SCRAM-SHA-1");
mongoc_uri_destroy (uri);

/* should fail on invalid escaped characters */
capture_logs (true);
uri = mongoc_uri_new ("mongodb://u%ser:pwd@localhost:27017");
Expand Down Expand Up @@ -3306,6 +3314,69 @@ test_uri_depr (void)
}
}

// Additional slashes and commas for embedded URIs given to connection options.
// e.g. authMechanismProperties=TOKEN_RESOURCE=mongodb://foo,ENVIRONMENT=azure
// ^^ ^
static void
test_uri_uri_in_options (void)
{
#define TEST_QUERY MONGOC_URI_AUTHMECHANISMPROPERTIES "=TOKEN_RESOURCE:mongodb://token-resource,ENVIRONMENT:azure"
#define TEST_PROPS "{'TOKEN_RESOURCE': 'mongodb://token-resource', 'ENVIRONMENT': 'azure'}"

capture_logs (true);

bson_error_t error;

// Simple.
{
mongoc_uri_t *const uri = mongoc_uri_new_with_error ("mongodb://localhost?" TEST_QUERY, &error);
ASSERT_NO_CAPTURED_LOGS ("mongoc_uri_new_with_error");
ASSERT_OR_PRINT (uri, error);
bson_t props;
ASSERT (mongoc_uri_get_mechanism_properties (uri, &props));
ASSERT_MATCH (&props, TEST_PROPS);
mongoc_uri_destroy (uri);
}

// With auth database.
{
mongoc_uri_t *const uri = mongoc_uri_new_with_error ("mongodb://localhost/db?" TEST_QUERY, &error);
ASSERT_NO_CAPTURED_LOGS ("mongoc_uri_new_with_error");
ASSERT_OR_PRINT (uri, error);
bson_t props;
ASSERT (mongoc_uri_get_mechanism_properties (uri, &props));
ASSERT_MATCH (&props, TEST_PROPS);
mongoc_uri_destroy (uri);
}

// With userinfo.
{
mongoc_uri_t *const uri = mongoc_uri_new_with_error ("mongodb://user:pass@localhost/db?" TEST_QUERY, &error);
ASSERT_NO_CAPTURED_LOGS ("mongoc_uri_new_with_error");
ASSERT_OR_PRINT (uri, error);
bson_t props;
ASSERT (mongoc_uri_get_mechanism_properties (uri, &props));
ASSERT_MATCH (&props, TEST_PROPS);
mongoc_uri_destroy (uri);
}

// With alternate hosts.
{
mongoc_uri_t *const uri =
mongoc_uri_new_with_error ("mongodb://user:pass@host1:27017,host2:27018/db?" TEST_QUERY, &error);
ASSERT_NO_CAPTURED_LOGS ("mongoc_uri_new_with_error");
ASSERT_OR_PRINT (uri, error);
bson_t props;
ASSERT (mongoc_uri_get_mechanism_properties (uri, &props));
ASSERT_MATCH (&props, TEST_PROPS);
mongoc_uri_destroy (uri);
}

capture_logs (false);

#undef TEST_QUERY
}

void
test_uri_install (TestSuite *suite)
{
Expand Down Expand Up @@ -3334,4 +3405,5 @@ test_uri_install (TestSuite *suite)
TestSuite_Add (suite, "/Uri/options_casing", test_casing_options);
TestSuite_Add (suite, "/Uri/parses_long_ipv6", test_parses_long_ipv6);
TestSuite_Add (suite, "/Uri/depr", test_uri_depr);
TestSuite_Add (suite, "/Uri/uri_in_options", test_uri_uri_in_options);
}