-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Remove an incomplete workaround for database connection_url formatting #22224
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -59,30 +59,19 @@ func (c *SQLConnectionProducer) Init(ctx context.Context, conf map[string]interf | |
return nil, fmt.Errorf("connection_url cannot be empty") | ||
} | ||
|
||
// Do not allow the username or password template pattern to be used as | ||
// part of the user-supplied username or password | ||
if strings.Contains(c.Username, "{{username}}") || | ||
strings.Contains(c.Username, "{{password}}") || | ||
strings.Contains(c.Password, "{{username}}") || | ||
strings.Contains(c.Password, "{{password}}") { | ||
|
||
return nil, fmt.Errorf("username and/or password cannot contain the template variables") | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the workaround being removed. |
||
|
||
// Don't escape special characters for MySQL password | ||
// Also don't escape special characters for the username and password if | ||
// the disable_escaping parameter is set to true | ||
// Don't escape special characters for the username and password if the | ||
// disable_escaping parameter is set to true. A user does this if the | ||
// connection_url is actually not a URL, but database-specific other | ||
// format, typically space or semicolon separated key=value pairs. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add clarity to comment on why disable_escaping exists. |
||
username := c.Username | ||
password := c.Password | ||
if !c.DisableEscaping { | ||
username = url.PathEscape(c.Username) | ||
} | ||
if (c.Type != "mysql") && !c.DisableEscaping { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This mysql special case can be deleted - the mysql plugin is no longer using this code at all. As can be seen in the first file touched in this PR, it now has its own forked copy. |
||
password = url.PathEscape(c.Password) | ||
} | ||
|
||
// QueryHelper doesn't do any SQL escaping, but if it starts to do so | ||
// then maybe we won't be able to use it to do URL substitution any more. | ||
// then maybe we won't be able to use it to do URL substitution anymore. | ||
c.ConnectionURL = dbutil.QueryHelper(c.ConnectionURL, map[string]string{ | ||
"username": username, | ||
"password": password, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,8 +8,6 @@ import ( | |
"net/url" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestSQLPasswordChars(t *testing.T) { | ||
|
@@ -22,6 +20,9 @@ func TestSQLPasswordChars(t *testing.T) { | |
{"postgres", "pass/word"}, | ||
{"postgres", "p@ssword"}, | ||
{"postgres", "pass\"word\""}, | ||
// Much to my surprise, CREATE USER "{{password}}" PASSWORD 'foo' worked. | ||
{"{{password}}", "foo"}, | ||
{"user", "{{username}}"}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Re-add test cases removed in #13414. |
||
} | ||
for _, tc := range testCases { | ||
t.Logf("username %q password %q", tc.Username, tc.Password) | ||
|
@@ -73,7 +74,7 @@ func TestSQLDisableEscaping(t *testing.T) { | |
{"ms'sq;l", "pass'wor;d", false}, | ||
} | ||
for _, tc := range testCases { | ||
t.Logf("username %q password %q disable_escaling %t", tc.Username, tc.Password, tc.DisableEscaping) | ||
t.Logf("username %q password %q disable_escaping %t", tc.Username, tc.Password, tc.DisableEscaping) | ||
|
||
sql := &SQLConnectionProducer{} | ||
ctx := context.Background() | ||
|
@@ -99,43 +100,3 @@ func TestSQLDisableEscaping(t *testing.T) { | |
} | ||
} | ||
} | ||
|
||
func TestSQLDisallowTemplates(t *testing.T) { | ||
testCases := []struct { | ||
Username string | ||
Password string | ||
}{ | ||
{"{{username}}", "pass"}, | ||
{"{{password}}", "pass"}, | ||
{"user", "{{username}}"}, | ||
{"user", "{{password}}"}, | ||
{"{{username}}", "{{password}}"}, | ||
{"abc{username}xyz", "123{password}789"}, | ||
{"abc{{username}}xyz", "123{{password}}789"}, | ||
{"abc{{{username}}}xyz", "123{{{password}}}789"}, | ||
} | ||
for _, disableEscaping := range []bool{true, false} { | ||
for _, tc := range testCases { | ||
t.Logf("username %q password %q disable_escaping %t", tc.Username, tc.Password, disableEscaping) | ||
|
||
sql := &SQLConnectionProducer{} | ||
ctx := context.Background() | ||
conf := map[string]interface{}{ | ||
"connection_url": "server=localhost;port=1433;user id={{username}};password={{password}};database=mydb;", | ||
"username": tc.Username, | ||
"password": tc.Password, | ||
"disable_escaping": disableEscaping, | ||
} | ||
_, err := sql.Init(ctx, conf, false) | ||
if disableEscaping { | ||
if err != nil { | ||
if !assert.EqualError(t, err, "username and/or password cannot contain the template variables") { | ||
t.Errorf("Init error on %q %q: %+v", tc.Username, tc.Password, err) | ||
} | ||
} else { | ||
assert.Equal(t, sql.ConnectionURL, "server=localhost;port=1433;user id=abc{username}xyz;password=123{password}789;database=mydb;") | ||
} | ||
} | ||
} | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove test that tests the workaround being removed. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,7 @@ package dbutil | |
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"regexp" | ||
"strings" | ||
|
||
"github.com/hashicorp/vault/sdk/database/dbplugin" | ||
|
@@ -18,13 +18,24 @@ var ( | |
ErrEmptyRotationStatement = errors.New("empty rotation statements") | ||
) | ||
|
||
// Query templates a query for us. | ||
func QueryHelper(tpl string, data map[string]string) string { | ||
for k, v := range data { | ||
tpl = strings.ReplaceAll(tpl, fmt.Sprintf("{{%s}}", k), v) | ||
} | ||
var queryHelperRegex = regexp.MustCompile(`{{[^{}]+}}`) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't have the original context so I won't do a full review, but if we do go with a regex I think we should be extremely prescriptive with it to more precisely match what we're looking for, e.g. here's a little Go playground test case for a key that has a So I'd rather see a regex that specifically looks for the keys we know how to replace, e.g. fmt.Sprintf(`{{(%s)}}`, strings.Join(keys, "|")) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The major point of this change is to do only one pass over the input string, not one per replaceable parameter, so the regex needs to stay as is. However, I recognise I should make it clearer in the documentation that replacement names may not contain { or } themselves. EDIT: Oops, responding from phone, and half the suggested code was off screen... I'll have a think about it. I'm not sure this is a case we really need to care about though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having thought on this some more, I am of the opinion that we should not make this change, because it makes the code more complex (and less efficient, though this is not a particularly a hotspot), only to support an edge case of placeholder keys containing { or } which they realistically never will. The keys here are all specified by the coders of plugins and never by users, and it would be mad to define a {{xxx}} substitution that contained internal { or } as part of the xxx - it would be a supremely confusing UI. Also, if we dynamically build the regex, we then need to account for quoting regex metacharacters in key names - the I do, though, see that I should document the limitation of not using { } within substitution keys, and add an example of it to the tests. Would that be an acceptable way to proceed? |
||
|
||
return tpl | ||
// QueryHelper evaluates a simple string template syntax by replacing {{value}} | ||
// placeholders with the values from the supplied data map. Despite the name, | ||
// it is NOT only used to template queries - it is also used for connection | ||
// URIs or DSNs. Since it has no idea of the specific syntax into which it is | ||
// templating, it does not perform any escaping. Unbalanced opening and closing | ||
// brace sequences are passed through as is, as are any placeholders for which | ||
// there is no key found in the map. | ||
func QueryHelper(tpl string, data map[string]string) string { | ||
return queryHelperRegex.ReplaceAllStringFunc(tpl, func(s string) string { | ||
replacement, ok := data[s[2:len(s)-2]] | ||
if ok { | ||
return replacement | ||
} else { | ||
return s | ||
} | ||
}) | ||
} | ||
|
||
// StatementCompatibilityHelper will populate the statements fields to support | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ import ( | |
"testing" | ||
|
||
"github.com/hashicorp/vault/sdk/database/dbplugin" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestStatementCompatibilityHelper(t *testing.T) { | ||
|
@@ -62,3 +63,45 @@ func TestStatementCompatibilityHelper(t *testing.T) { | |
t.Fatalf("mismatch: %#v, %#v", expectedStatements3, statements3) | ||
} | ||
} | ||
|
||
func TestQueryHelper(t *testing.T) { | ||
data := map[string]string{ | ||
// These are typical keys you find in a data map used with QueryHelper | ||
"username": "hello", | ||
"name": "hello", | ||
"password": "world", | ||
"expiration": "24h", | ||
} | ||
for _, tc := range []struct { | ||
tpl string | ||
expected string | ||
}{ | ||
{"", ""}, | ||
{"somedb://{{username}}:{{password}}@something", "somedb://hello:world@something"}, | ||
// Unknown placeholders pass through as is | ||
{"user={{name}} other={{unknown}}", "user=hello other={{unknown}}"}, | ||
// Various incorrect delimiters pass through as is | ||
{"{{{{{{{{", "{{{{{{{{"}, | ||
{"{{username}} {{incomplete", "hello {{incomplete"}, | ||
{"VALID UNTIL '{{expiration}}'; {{", "VALID UNTIL '24h'; {{"}, | ||
// This case tests whether `{{!{{password}}` successfully looks past the earlier unmatched {{ | ||
{"}}backwards{{!{{password}}!", "}}backwards{{!world!"}, | ||
} { | ||
assert.Equal(t, tc.expected, QueryHelper(tc.tpl, data), | ||
"template processing produced unexpected result") | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test passes before and after the change. |
||
|
||
// TestQueryHelper_Recursion confirms QueryHelper does not replace placeholders that were themselves added as part of | ||
// a replacement value. | ||
func TestQueryHelper_Recursion(t *testing.T) { | ||
data := map[string]string{ | ||
"a": "A{{a}}{{b}}{{c}}{{d}}", | ||
"b": "B{{a}}{{b}}{{c}}{{d}}", | ||
"c": "C{{a}}{{b}}{{c}}{{d}}", | ||
"d": "D{{a}}{{b}}{{c}}{{d}}", | ||
} | ||
assert.Equal(t, "A{{a}}{{b}}{{c}}{{d}}B{{a}}{{b}}{{c}}{{d}}C{{a}}{{b}}{{c}}{{d}}D{{a}}{{b}}{{c}}{{d}}", | ||
QueryHelper("{{a}}{{b}}{{c}}{{d}}", data), | ||
"template processing produced unexpected result") | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test fails when applied to the existing implementation of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Grammar fix. Also, note that this is a forked copy of the sdk's SQLConnectionProducer that did not contain the workaround.