Skip to content

Commit 9e3bbd3

Browse files
authored
CDRIVER-5580 support embedded URI in connection string options (#1914)
* Delimit auth mechanism properties by comma first * Add regression tests for URI and auth mech prop parsing * Summarize NEWS entry for authentication-related credentials validation and errors
1 parent f1e2b54 commit 9e3bbd3

File tree

3 files changed

+200
-91
lines changed

3 files changed

+200
-91
lines changed

NEWS

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,12 @@ Unreleased (2.0.0)
1010
* `mongoc_server_description_host` changes the return type from `mongoc_host_list_t *` to `const mongoc_host_list_t *`.
1111
* URI authentication credentials validation (only applicable during creation of a new `mongoc_uri_t` object from a connection string):
1212
* The requirement that a username is non-empty when specified is now enforced regardless of authentication mechanism.
13-
* `authMechanism` is now validated and returns a client error for invalid or unsupported values.
14-
* `authSource` is now validated and returns a client error for invalid or unsupported values for the specified `authMechanism`.
1513
* `authSource` is now correctly defaulted to `"$external"` for MONGODB-AWS (instead of the database name or `"admin"`).
16-
* The requirement that a password is provided is now enforced when the authentication mechanism is specified for:
17-
* PLAIN
18-
* SCRAM-SHA-1
19-
* SCRAM-SHA-256
20-
* The requirement that neither or both a username and password is provided (optionally with a `AWS_SESSION_TOKEN`) is now enforced for MONGODB-AWS.
21-
* `authMechanismProperties` is now prohibited (instead of ignored) when the authentication mechanism is specified for:
22-
* PLAIN
23-
* SCRAM-SHA-1
24-
* SCRAM-SHA-256
25-
* MONGODB-X509
26-
* `authMechanismProperties` is now validated and returns a client error for invalid or unsupported fields when the authentication mechanism is specified for:
27-
* GSSAPI: supported fields are SERVICE_NAME, CANONICALIZE_HOST_NAME, SERVICE_REALM, and SERVICE_HOST.
28-
* MONGODB-AWS: supported fields are AWS_SESSION_TOKEN.
14+
* `authMechanism` is now validated and returns a client error for invalid or unsupported values.
15+
* 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`).
16+
* `authMechanismProperties` now correctly supports `':'` within property values.
17+
* Old behavior: `authMechanismProperties=A:B,C:D:E,F:G` is parsed as `{'A': 'B', 'C': 'D:E,F:G'}`.
18+
* New behavior: `authMechanismProperties=A:B,C:D:E,F:G` is parsed as `{'A': 'B': 'C': 'D:E', 'F': 'G'}`.
2919
* Calling `mongoc_bulk_operation_execute` on the same `mongoc_bulk_operation_t` repeatedly is an error. Previously this was only discouraged in documentation.
3020

3121
## Removals

src/libmongoc/src/mongoc/mongoc-uri.c

Lines changed: 123 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -516,25 +516,46 @@ mongoc_uri_parse_database (mongoc_uri_t *uri, const char *str, const char **end)
516516
static bool
517517
mongoc_uri_parse_auth_mechanism_properties (mongoc_uri_t *uri, const char *str)
518518
{
519-
char *field;
520-
char *value;
521519
const char *end_scan;
522-
bson_t properties;
523520

524-
bson_init (&properties);
521+
bson_t properties = BSON_INITIALIZER;
525522

526-
/* build up the properties document */
527-
while ((field = scan_to_unichar (str, ':', "&", &end_scan))) {
523+
// Key-value pairs are delimited by ','.
524+
for (char *kvp; (kvp = scan_to_unichar (str, ',', "", &end_scan)); bson_free (kvp)) {
528525
str = end_scan + 1;
529-
if (!(value = scan_to_unichar (str, ',', ":&", &end_scan))) {
530-
value = bson_strdup (str);
531-
str = "";
532-
} else {
533-
str = end_scan + 1;
526+
527+
char *const key = scan_to_unichar (kvp, ':', "", &end_scan);
528+
529+
// Found delimiter: split into key and value.
530+
if (key) {
531+
char *const value = bson_strdup (end_scan + 1);
532+
BSON_APPEND_UTF8 (&properties, key, value);
533+
bson_free (key);
534+
bson_free (value);
535+
}
536+
537+
// No delimiter: entire string is the key. Use empty string as value.
538+
else {
539+
BSON_APPEND_UTF8 (&properties, kvp, "");
540+
}
541+
}
542+
543+
// Last (or only) pair.
544+
if (*str != '\0') {
545+
char *const key = scan_to_unichar (str, ':', "", &end_scan);
546+
547+
// Found delimiter: split into key and value.
548+
if (key) {
549+
char *const value = bson_strdup (end_scan + 1);
550+
BSON_APPEND_UTF8 (&properties, key, value);
551+
bson_free (key);
552+
bson_free (value);
553+
}
554+
555+
// No delimiter: entire string is the key. Use empty string as value.
556+
else {
557+
BSON_APPEND_UTF8 (&properties, str, "");
534558
}
535-
bson_append_utf8 (&properties, field, -1, value, -1);
536-
bson_free (field);
537-
bson_free (value);
538559
}
539560

540561
/* append our auth properties to our credentials */
@@ -1846,100 +1867,126 @@ mongoc_uri_parse_before_slash (mongoc_uri_t *uri, const char *before_slash, bson
18461867
static bool
18471868
mongoc_uri_parse (mongoc_uri_t *uri, const char *str, bson_error_t *error)
18481869
{
1870+
BSON_ASSERT_PARAM (uri);
18491871
BSON_ASSERT_PARAM (str);
18501872

1851-
char *before_slash = NULL;
1852-
const char *tmp;
1873+
const size_t str_len = strlen (str);
18531874

1854-
if (!bson_utf8_validate (str, strlen (str), false /* allow_null */)) {
1875+
if (!bson_utf8_validate (str, str_len, false /* allow_null */)) {
18551876
MONGOC_URI_ERROR (error, "%s", "Invalid UTF-8 in URI");
1856-
goto error;
1877+
return false;
18571878
}
18581879

1880+
// Save for later.
1881+
const char *const str_end = str + str_len;
1882+
1883+
// Parse and remove scheme and its delimiter.
1884+
// e.g. "mongodb://user:pass@host1:27017,host2:27018/database?key1=value1&key2=value2"
1885+
// ~~~~~~~~~~
18591886
if (!mongoc_uri_parse_scheme (uri, str, &str)) {
18601887
MONGOC_URI_ERROR (error, "%s", "Invalid URI Schema, expecting 'mongodb://' or 'mongodb+srv://'");
1861-
goto error;
1888+
return false;
18621889
}
1890+
// str -> "user:pass@host1:27017,host2:27018/database?key1=value1&key2=value2"
18631891

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

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

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

1883-
before_slash = bson_strndup (str, userpass_len + hosts_len);
1884-
}
1907+
if (userinfo) {
1908+
cursor = tmp + 1; // Consume userinfo delimiter.
1909+
bson_free (userinfo);
18851910
}
1886-
1887-
bson_free (userpass);
1888-
bson_free (hosts);
18891911
}
1912+
// cursor -> "host1:27017,host2:27018/database?key1=value1&key2=value2"
18901913

1891-
if (!before_slash) {
1892-
before_slash = bson_strdup (str);
1893-
str += strlen (before_slash);
1894-
} else {
1895-
str = tmp;
1914+
// Find either the optional auth database delimiter or the query delimiter.
1915+
// e.g. "host1:27017,host2:27018/database?key1=value1&key2=value2"
1916+
// ^
1917+
// e.g. "host1:27017,host2:27018?key1=value1&key2=value2"
1918+
// ^
1919+
{
1920+
const char *tmp;
1921+
1922+
// Only ':', '[', and ']' are permitted among RFC-3986 gen-delims (":/?#[]@") in hostinfo.
1923+
const char *const terminators = "/?#@";
1924+
1925+
char *hostinfo;
1926+
1927+
// Optional auth delimiter is present.
1928+
if ((hostinfo = scan_to_unichar (cursor, '/', terminators, &tmp))) {
1929+
cursor = tmp; // Include the delimiter.
1930+
bson_free (hostinfo);
1931+
}
1932+
1933+
// Query delimiter is present.
1934+
else if ((hostinfo = scan_to_unichar (cursor, '?', terminators, &tmp))) {
1935+
cursor = tmp; // Include the delimiter.
1936+
bson_free (hostinfo);
1937+
}
1938+
1939+
// Neither delimiter is present. Entire rest of string is part of hostinfo.
1940+
else {
1941+
cursor = str_end; // Jump to end of string.
1942+
BSON_ASSERT (*cursor == '\0');
1943+
}
18961944
}
1945+
// cursor -> "/database?key1=value1&key2=value2"
18971946

1898-
if (!mongoc_uri_parse_before_slash (uri, before_slash, error)) {
1899-
goto error;
1947+
// Parse "userhosts". e.g. "user:pass@host1:27017,host2:27018"
1948+
{
1949+
char *const userhosts = bson_strndup (str, (size_t) (cursor - str));
1950+
const bool ret = mongoc_uri_parse_before_slash (uri, userhosts, error);
1951+
bson_free (userhosts);
1952+
if (!ret) {
1953+
return false;
1954+
}
19001955
}
19011956

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

1904-
if (*str) {
1905-
// Check for valid end of hostname delimeter (skip slash if necessary)
1906-
if (*str != '/' && *str != '?') {
1907-
MONGOC_URI_ERROR (error, "%s", "Expected end of hostname delimiter");
1908-
goto error;
1909-
}
1961+
// Parse the auth database.
1962+
if (*cursor == '/') {
1963+
++cursor; // Consume the delimiter.
19101964

1911-
if (*str == '/') {
1912-
// Try to parse database.
1913-
str++;
1914-
if (*str) {
1915-
if (!mongoc_uri_parse_database (uri, str, &str)) {
1965+
// No auth database may be present even if the delimiter is present.
1966+
// e.g. "mongodb://localhost:27017/"
1967+
if (*cursor != '\0') {
1968+
if (!mongoc_uri_parse_database (uri, cursor, &cursor)) {
19161969
MONGOC_URI_ERROR (error, "%s", "Invalid database name in URI");
1917-
goto error;
1970+
return false;
19181971
}
19191972
}
19201973
}
19211974

1922-
if (*str == '?') {
1923-
// Try to parse options.
1924-
str++;
1925-
if (*str) {
1926-
if (!mongoc_uri_parse_options (uri, str, false /* from DNS */, error)) {
1927-
goto error;
1975+
// Parse the query options.
1976+
if (*cursor == '?') {
1977+
++cursor; // Consume the delimiter.
1978+
1979+
// No options may be present even if the delimiter is present.
1980+
// e.g. "mongodb://localhost:27017?"
1981+
if (*cursor != '\0') {
1982+
if (!mongoc_uri_parse_options (uri, cursor, false /* from DNS */, error)) {
1983+
return false;
19281984
}
19291985
}
19301986
}
19311987
}
19321988

1933-
if (!mongoc_uri_finalize (uri, error)) {
1934-
goto error;
1935-
}
1936-
1937-
bson_free (before_slash);
1938-
return true;
1939-
1940-
error:
1941-
bson_free (before_slash);
1942-
return false;
1989+
return mongoc_uri_finalize (uri, error);
19431990
}
19441991

19451992

src/libmongoc/tests/test-mongoc-uri.c

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,14 @@ test_mongoc_uri_new (void)
239239
ASSERT_CMPSTR (mongoc_uri_get_auth_mechanism (uri), "SCRAM-SHA-1");
240240
mongoc_uri_destroy (uri);
241241

242+
/* should recognize many reserved characters in the userpass for backward compatibility */
243+
uri = mongoc_uri_new ("mongodb://user?#[]:pass?#[]@localhost?" MONGOC_URI_AUTHMECHANISM "=SCRAM-SHA-1");
244+
ASSERT (uri);
245+
ASSERT_CMPSTR (mongoc_uri_get_username (uri), "user?#[]");
246+
ASSERT_CMPSTR (mongoc_uri_get_password (uri), "pass?#[]");
247+
ASSERT_CMPSTR (mongoc_uri_get_auth_mechanism (uri), "SCRAM-SHA-1");
248+
mongoc_uri_destroy (uri);
249+
242250
/* should fail on invalid escaped characters */
243251
capture_logs (true);
244252
uri = mongoc_uri_new ("mongodb://u%ser:pwd@localhost:27017");
@@ -3306,6 +3314,69 @@ test_uri_depr (void)
33063314
}
33073315
}
33083316

3317+
// Additional slashes and commas for embedded URIs given to connection options.
3318+
// e.g. authMechanismProperties=TOKEN_RESOURCE=mongodb://foo,ENVIRONMENT=azure
3319+
// ^^ ^
3320+
static void
3321+
test_uri_uri_in_options (void)
3322+
{
3323+
#define TEST_QUERY MONGOC_URI_AUTHMECHANISMPROPERTIES "=TOKEN_RESOURCE:mongodb://token-resource,ENVIRONMENT:azure"
3324+
#define TEST_PROPS "{'TOKEN_RESOURCE': 'mongodb://token-resource', 'ENVIRONMENT': 'azure'}"
3325+
3326+
capture_logs (true);
3327+
3328+
bson_error_t error;
3329+
3330+
// Simple.
3331+
{
3332+
mongoc_uri_t *const uri = mongoc_uri_new_with_error ("mongodb://localhost?" TEST_QUERY, &error);
3333+
ASSERT_NO_CAPTURED_LOGS ("mongoc_uri_new_with_error");
3334+
ASSERT_OR_PRINT (uri, error);
3335+
bson_t props;
3336+
ASSERT (mongoc_uri_get_mechanism_properties (uri, &props));
3337+
ASSERT_MATCH (&props, TEST_PROPS);
3338+
mongoc_uri_destroy (uri);
3339+
}
3340+
3341+
// With auth database.
3342+
{
3343+
mongoc_uri_t *const uri = mongoc_uri_new_with_error ("mongodb://localhost/db?" TEST_QUERY, &error);
3344+
ASSERT_NO_CAPTURED_LOGS ("mongoc_uri_new_with_error");
3345+
ASSERT_OR_PRINT (uri, error);
3346+
bson_t props;
3347+
ASSERT (mongoc_uri_get_mechanism_properties (uri, &props));
3348+
ASSERT_MATCH (&props, TEST_PROPS);
3349+
mongoc_uri_destroy (uri);
3350+
}
3351+
3352+
// With userinfo.
3353+
{
3354+
mongoc_uri_t *const uri = mongoc_uri_new_with_error ("mongodb://user:pass@localhost/db?" TEST_QUERY, &error);
3355+
ASSERT_NO_CAPTURED_LOGS ("mongoc_uri_new_with_error");
3356+
ASSERT_OR_PRINT (uri, error);
3357+
bson_t props;
3358+
ASSERT (mongoc_uri_get_mechanism_properties (uri, &props));
3359+
ASSERT_MATCH (&props, TEST_PROPS);
3360+
mongoc_uri_destroy (uri);
3361+
}
3362+
3363+
// With alternate hosts.
3364+
{
3365+
mongoc_uri_t *const uri =
3366+
mongoc_uri_new_with_error ("mongodb://user:pass@host1:27017,host2:27018/db?" TEST_QUERY, &error);
3367+
ASSERT_NO_CAPTURED_LOGS ("mongoc_uri_new_with_error");
3368+
ASSERT_OR_PRINT (uri, error);
3369+
bson_t props;
3370+
ASSERT (mongoc_uri_get_mechanism_properties (uri, &props));
3371+
ASSERT_MATCH (&props, TEST_PROPS);
3372+
mongoc_uri_destroy (uri);
3373+
}
3374+
3375+
capture_logs (false);
3376+
3377+
#undef TEST_QUERY
3378+
}
3379+
33093380
void
33103381
test_uri_install (TestSuite *suite)
33113382
{
@@ -3334,4 +3405,5 @@ test_uri_install (TestSuite *suite)
33343405
TestSuite_Add (suite, "/Uri/options_casing", test_casing_options);
33353406
TestSuite_Add (suite, "/Uri/parses_long_ipv6", test_parses_long_ipv6);
33363407
TestSuite_Add (suite, "/Uri/depr", test_uri_depr);
3408+
TestSuite_Add (suite, "/Uri/uri_in_options", test_uri_uri_in_options);
33373409
}

0 commit comments

Comments
 (0)