Skip to content

Commit 19e9987

Browse files
gaultierory-bot
authored andcommitted
chore: add helpers for Kratos OEL to support various databases
GitOrigin-RevId: 1e03731811f8ccc8d0e2b617598ec04a2d24d577
1 parent 822ea26 commit 19e9987

File tree

1 file changed

+76
-0
lines changed

1 file changed

+76
-0
lines changed

oryx/sqlxx/sqlxx.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ package sqlxx
55

66
import (
77
"fmt"
8+
"net/url"
89
"reflect"
910
"slices"
1011
"strings"
1112

1213
"github.com/jmoiron/sqlx/reflectx"
14+
"github.com/pkg/errors"
1315
)
1416

1517
// GetDBFieldNames extracts all database field names from a struct based on the `db` tags using sqlx.
@@ -108,3 +110,77 @@ func OnConflictDoNothing(dialect string, columnNoop string) string {
108110
return ` ON CONFLICT DO NOTHING `
109111
}
110112
}
113+
114+
// ExtractSchemeFromDSN returns the scheme (e.g. `mysql`, `postgres`, etc) component in a DSN string,
115+
// as well as the remaining part of the DSN after the scheme separator.
116+
// It is an error to not have a scheme present.
117+
// This makes sense in the context of a DSN to be able to identify which database is in use.
118+
func ExtractSchemeFromDSN(dsn string) (string, string, error) {
119+
scheme, afterSchemeSeparator, schemeSeparatorFound := strings.Cut(dsn, "://")
120+
if !schemeSeparatorFound {
121+
return "", "", errors.New("invalid DSN: missing scheme separator")
122+
}
123+
if scheme == "" {
124+
return "", "", errors.New("invalid DSN: empty scheme")
125+
}
126+
127+
return scheme, afterSchemeSeparator, nil
128+
}
129+
130+
// ReplaceSchemeInDSN replaces the scheme (e.g. `mysql`, `postgres`, etc) in a DSN string with another one.
131+
// This is necessary for example when using `cockroach` as a scheme, but using the postgres driver to connect to the database,
132+
// and this driver only accepts `postgres` as a scheme.
133+
func ReplaceSchemeInDSN(dsn string, newScheme string) (string, error) {
134+
_, afterSchemeSeparator, err := ExtractSchemeFromDSN(dsn)
135+
if err != nil {
136+
return "", errors.WithStack(err)
137+
}
138+
139+
return newScheme + "://" + afterSchemeSeparator, nil
140+
}
141+
142+
// DSNRedacted parses a database DSN and returns a redacted form as a string.
143+
// It replaces any password with "xxxxx" just like `url.Redacted()`.
144+
// Only the password is redacted, not the username.
145+
// This function is necessary because MySQL uses a DSN format not compatible with `url.Parse`.
146+
// Additionally and as a consequence of the point above, the scheme is expected to be present and non-empty.
147+
// This function is less strict that `url.Parse` in the case of MySQL.
148+
// It also does not escape any characters in the username, whereas `url.String()`/`url.Redacted` does.
149+
func DSNRedacted(dsn string) (string, error) {
150+
scheme, afterSchemeSeparator, err := ExtractSchemeFromDSN(dsn)
151+
if err != nil {
152+
return "", errors.WithStack(err)
153+
}
154+
155+
// If this is not MySQL, we simply delegate the work to `url.Parse`.
156+
if scheme != "mysql" {
157+
u, err := url.Parse(dsn)
158+
if err != nil {
159+
return "", errors.WithStack(err)
160+
}
161+
return u.Redacted(), nil
162+
}
163+
164+
// MySQL has a weird DSN syntax not conforming to a standard URL, of the form:
165+
// `[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]`
166+
// We only need to parse up to `@` in order to redact the password. The rest is left as-is.
167+
168+
usernamePassword, afterUsernamePassword, usernamePasswordSeparatorFound := strings.Cut(afterSchemeSeparator, "@")
169+
if !usernamePasswordSeparatorFound {
170+
afterUsernamePassword = afterSchemeSeparator
171+
}
172+
173+
username, password, hasPassword := strings.Cut(usernamePassword, ":")
174+
// We only insert a redacted password in the final result if a password was provided in the input.
175+
// This behavior matches the one of `url.Redacted()`.
176+
if hasPassword {
177+
password = ":xxxxx"
178+
}
179+
180+
res := scheme + "://"
181+
if usernamePasswordSeparatorFound {
182+
res += username + password + "@"
183+
}
184+
res += afterUsernamePassword
185+
return res, nil
186+
}

0 commit comments

Comments
 (0)