Skip to content

Commit 3ed59a7

Browse files
committed
identmap: use case-insensitive matching
All usernames in CockroachDB are normalized to lower-case. Previously, the identity map would not account for this, and would use a case sensitive match to check the identities to be mapped. Release note (bug fix): The username remapping functionality specified by the server.identity_map.configuration cluster setting now matches identities and usernames with a case-insensitive comparison.
1 parent 49f3056 commit 3ed59a7

File tree

5 files changed

+150
-19
lines changed

5 files changed

+150
-19
lines changed

pkg/acceptance/compose/gss/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ services:
2424
timeout: 10s
2525
retries: 25
2626
psql:
27-
image: us-east1-docker.pkg.dev/crl-ci-images/cockroach/acceptance-gss-psql:20241009-173040
27+
image: us-east1-docker.pkg.dev/crl-ci-images/cockroach/acceptance-gss-psql:20251023-150915
2828
user: "${UID}:${GID}"
2929
depends_on:
3030
cockroach:

pkg/acceptance/compose/gss/psql/gss_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,12 @@ func TestGSS(t *testing.T) {
109109
user: "tester",
110110
gssErr: "",
111111
},
112-
// Verify case-sensitivity.
112+
// Verify names are matched without case-sensitivity.
113113
{
114114
conf: `host all all all gss map=demo`,
115115
identMap: `demo /^(.*)@MY.EX$ \1`,
116116
user: "tester",
117-
gssErr: `system identity "[email protected]" did not map to a database role`,
117+
gssErr: ``,
118118
},
119119
// Validating the use of "map" as a filter.
120120
{

pkg/sql/pgwire/identmap/ident_map.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,12 @@ func From(r io.Reader) (*Conf, error) {
8585
var sysPattern *regexp.Regexp
8686
var err error
8787
if sysName := parts[2]; sysName[0] == '/' {
88-
sysPattern, err = regexp.Compile(sysName[1:])
88+
// Use case-insensitive matching since system identities (e.g., certificate CNs)
89+
// are normalized to lowercase before being passed to the user map.
90+
sysPattern, err = regexp.Compile("(?i)" + sysName[1:])
8991
} else {
90-
sysPattern, err = regexp.Compile("^" + regexp.QuoteMeta(sysName) + "$")
92+
// Use case-insensitive matching for literal patterns as well.
93+
sysPattern, err = regexp.Compile("(?i)^" + regexp.QuoteMeta(sysName) + "$")
9194
}
9295
if err != nil {
9396
return nil, errors.Wrapf(err, "unable to parse line %d", lineNo)
@@ -143,16 +146,19 @@ func (c *Conf) Map(mapName, systemIdentity string) ([]username.SQLUsername, bool
143146
var names []username.SQLUsername
144147
seen := make(map[string]bool)
145148
for _, elt := range elts {
146-
if n := elt.substitute(systemIdentity); n != "" && !seen[n] {
149+
if n := elt.substitute(systemIdentity); n != "" {
147150
// We're returning this as a for-validation username since a
148151
// pattern-based mapping could still result in invalid characters
149152
// being incorporated into the input.
150153
u, err := username.MakeSQLUsernameFromUserInput(n, username.PurposeValidation)
151154
if err != nil {
152155
return nil, true, err
153156
}
154-
names = append(names, u)
155-
seen[n] = true
157+
normalized := u.Normalized()
158+
if !seen[normalized] {
159+
names = append(names, u)
160+
seen[normalized] = true
161+
}
156162
}
157163
}
158164
return names, true, nil

pkg/sql/pgwire/identmap/ident_map_test.go

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ func TestIdentityMapElement(t *testing.T) {
1818
exactMatch := func(sysName, dbname string) element {
1919
return element{
2020
dbUser: dbname,
21-
pattern: regexp.MustCompile("^" + regexp.QuoteMeta(sysName) + "$"),
21+
pattern: regexp.MustCompile("(?i)^" + regexp.QuoteMeta(sysName) + "$"),
2222
substituteAt: -1,
2323
}
2424
}
2525
regexMatch := func(sysName, dbName string) element {
2626
return element{
2727
dbUser: dbName,
28-
pattern: regexp.MustCompile(sysName),
28+
pattern: regexp.MustCompile("(?i)" + sysName),
2929
substituteAt: strings.Index(dbName, `\1`),
3030
}
3131
}
@@ -65,6 +65,33 @@ func TestIdentityMapElement(t *testing.T) {
6565
principal: "[email protected]",
6666
expected: "",
6767
},
68+
// Case-insensitive exact match tests.
69+
{
70+
elt: exactMatch("CaRlItO", "carl"),
71+
principal: "carlito",
72+
expected: "carl",
73+
},
74+
{
75+
elt: exactMatch("carlito", "carl"),
76+
principal: "CARLITO",
77+
expected: "carl",
78+
},
79+
{
80+
elt: exactMatch("CaRlItO", "carl"),
81+
principal: "CaRlItO",
82+
expected: "carl",
83+
},
84+
// Case-insensitive regex match tests.
85+
{
86+
elt: regexMatch("^(.*)@CockroachLabs.com$", `\1`),
87+
principal: "[email protected]",
88+
expected: "carl",
89+
},
90+
{
91+
elt: regexMatch("^(.*)@cockroachlabs.com$", `\1`),
92+
principal: "[email protected]",
93+
expected: "Carl",
94+
},
6895
}
6996

7097
for idx, tc := range tcs {
@@ -130,3 +157,101 @@ bar [email protected] carl # Duplicate behavior
130157
_, mapFound, _ = m.Map("bar", "something")
131158
a.True(mapFound)
132159
}
160+
161+
func TestIdentityMapCaseInsensitive(t *testing.T) {
162+
a := assert.New(t)
163+
data := `
164+
# Test case-insensitive matching for both literal and regex patterns.
165+
# This is important because certificate CNs are normalized to lowercase
166+
# before being passed to the user map.
167+
certmap AbC123DeF456 cert_user
168+
certmap /^([A-Z0-9]+)@example.com$ user_\1
169+
emailmap /^(.*)@COMPANY.COM$ \1
170+
`
171+
172+
m, err := From(strings.NewReader(data))
173+
if !a.NoError(err) {
174+
return
175+
}
176+
177+
// Test literal pattern matching is case-insensitive.
178+
elts, _, err := m.Map("certmap", "abc123def456")
179+
if a.NoError(err) && a.Len(elts, 1) {
180+
a.Equal("cert_user", elts[0].Normalized())
181+
}
182+
183+
// Test uppercase version also matches.
184+
elts, _, err = m.Map("certmap", "ABC123DEF456")
185+
if a.NoError(err) && a.Len(elts, 1) {
186+
a.Equal("cert_user", elts[0].Normalized())
187+
}
188+
189+
// Test mixed case also matches.
190+
elts, _, err = m.Map("certmap", "AbC123dEf456")
191+
if a.NoError(err) && a.Len(elts, 1) {
192+
a.Equal("cert_user", elts[0].Normalized())
193+
}
194+
195+
// Test regex pattern matching is case-insensitive with substitution.
196+
elts, _, err = m.Map("certmap", "[email protected]")
197+
if a.NoError(err) && a.Len(elts, 1) {
198+
a.Equal("user_abc123", elts[0].Normalized())
199+
}
200+
201+
// Test uppercase email also matches. Note that the captured group is lowercased
202+
// during SQL username normalization.
203+
elts, _, err = m.Map("certmap", "[email protected]")
204+
if a.NoError(err) && a.Len(elts, 1) {
205+
a.Equal("user_abc123", elts[0].Normalized())
206+
}
207+
208+
// Test pattern with uppercase domain matches lowercase input.
209+
elts, _, err = m.Map("emailmap", "[email protected]")
210+
if a.NoError(err) && a.Len(elts, 1) {
211+
a.Equal("john.doe", elts[0].Normalized())
212+
}
213+
214+
// Test pattern with uppercase domain matches uppercase input.
215+
// The captured username is normalized to lowercase.
216+
elts, _, err = m.Map("emailmap", "[email protected]")
217+
if a.NoError(err) && a.Len(elts, 1) {
218+
a.Equal("jane.doe", elts[0].Normalized())
219+
}
220+
}
221+
222+
func TestIdentityMapCaseInsensitiveDeduplication(t *testing.T) {
223+
a := assert.New(t)
224+
data := `
225+
# Test that deduplication works correctly with case-insensitive usernames.
226+
# If multiple rules produce usernames that normalize to the same value,
227+
# only the first one should be returned.
228+
testmap [email protected] carl
229+
testmap /^(.*)@cockroachlabs.com$ \1
230+
`
231+
232+
m, err := From(strings.NewReader(data))
233+
if !a.NoError(err) {
234+
return
235+
}
236+
237+
// When we pass in [email protected] (note the different casing):
238+
// - First rule matches and produces: carl
239+
// - Second rule matches and produces: Carl (which normalizes to carl)
240+
// We should only get one result since they normalize to the same username.
241+
elts, _, err := m.Map("testmap", "[email protected]")
242+
if a.NoError(err) && a.Len(elts, 1) {
243+
a.Equal("carl", elts[0].Normalized())
244+
}
245+
246+
// Also test with lowercase input to verify both rules match but deduplicate.
247+
elts, _, err = m.Map("testmap", "[email protected]")
248+
if a.NoError(err) && a.Len(elts, 1) {
249+
a.Equal("carl", elts[0].Normalized())
250+
}
251+
252+
// Test with a completely uppercase input.
253+
elts, _, err = m.Map("testmap", "[email protected]")
254+
if a.NoError(err) && a.Len(elts, 1) {
255+
a.Equal("carl", elts[0].Normalized())
256+
}
257+
}

pkg/sql/pgwire/testdata/auth/identity_map

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ host all all all cert-password map=testing
4848
# testing testuser2 carl # Cert that doesn't correspond to a db user
4949
# testing [email protected] carl # Cert with a non-SQL principal baked in
5050
# Active configuration:
51-
# map-name system-username database-username
52-
testing ^testuser$ carl
53-
testing (.*)@cockroachlabs.com \1 # substituteAt=0
54-
testing ^testuser$ another_carl
55-
testing ^will_be_carl$ carl
56-
testing ^testuser2$ carl
57-
testing ^testuser@example\.com$ carl
51+
# map-name system-username database-username
52+
testing (?i)^testuser$ carl
53+
testing (?i)(.*)@cockroachlabs.com \1 # substituteAt=0
54+
testing (?i)^testuser$ another_carl
55+
testing (?i)^will_be_carl$ carl
56+
testing (?i)^testuser2$ carl
57+
testing (?i)^testuser@example\.com$ carl
5858

5959
sql
6060
CREATE USER carl WITH PASSWORD 'doggo';
@@ -274,7 +274,7 @@ host all all all cert-password map=testing
274274
# testing testuser root # Exact remapping
275275
# Active configuration:
276276
# map-name system-username database-username
277-
testing ^testuser$ root
277+
testing (?i)^testuser$ root
278278

279279
connect user=testuser database=mydb
280280
----
@@ -303,7 +303,7 @@ host all all all cert-password map=testing
303303
# testing testuser node # Exact remapping
304304
# Active configuration:
305305
# map-name system-username database-username
306-
testing ^testuser$ node
306+
testing (?i)^testuser$ node
307307

308308
connect user=testuser database=mydb
309309
----

0 commit comments

Comments
 (0)