Skip to content

Commit 1a4ff79

Browse files
craig[bot]rafissarulajmanistevendanna
committed
155531: identmap: use case-insensitive matching r=rafiss a=rafiss 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. Epic: None 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. 156018: changefeedccl: skip TestChangefeedKafkaMessageTooLarge under duress r=andyyang890 a=arulajmani Closes #155333 Release note: None 156066: kvcoord: always check for buffered writes when checking for locks r=miraradeva a=stevendanna When reading through code for an unrelated reason we noted that there was a call checking for acquired locks but not buffered writes. This is concerning since the reasoning for why we need to check for acquired locks almost always implies we should also check for buffered writes. In the particular case of RollbackToSavepoint, I believe the omission has no impact. Namely: - Previous writes have been cleared from the buffer and thus aren't in danger of failing to be rolled back. - Subsequent writes will bump the sequence number and thus those writes are not in danger of being erroneously rolled back. - Subsequent reads cannot read the data that should have been rolled back because it will have been cleared from the buffer. Further, for real-world SQL transactions, we never have a buffered write without having at least 1 lock. However, for the avoidance fo doubt, we now check both and add a new method to avoid missing checks in the future. Fixes #155975 Release note: None 156081: kvnemesis: bump the timeout r=miraradeva a=arulajmani Medium -> large. Closes #155985 Closes #155852 Epic: none Release note: None Co-authored-by: Rafi Shamim <[email protected]> Co-authored-by: Arul Ajmani <[email protected]> Co-authored-by: Steven Danna <[email protected]>
5 parents d86ed05 + 3ed59a7 + d94eca5 + 1ae507a + 09969a4 commit 1a4ff79

File tree

9 files changed

+161
-25
lines changed

9 files changed

+161
-25
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/ccl/changefeedccl/changefeed_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11041,6 +11041,8 @@ func TestChangefeedKafkaMessageTooLarge(t *testing.T) {
1104111041
defer leaktest.AfterTest(t)()
1104211042
defer log.Scope(t).Close(t)
1104311043

11044+
skip.UnderDuress(t, "prone to overload")
11045+
1104411046
testFn := func(t *testing.T, s TestServer, f cdctest.TestFeedFactory) {
1104511047
if KafkaV2Enabled.Get(&s.Server.ClusterSettings().SV) {
1104611048
// This is already covered for the v2 sink in another test: TestKafkaSinkClientV2_Resize

pkg/kv/kvclient/kvcoord/txn_coord_sender.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -527,8 +527,7 @@ func (tc *TxnCoordSender) Send(
527527
return nil, pErr
528528
}
529529

530-
if ba.IsSingleEndTxnRequest() && !tc.interceptorAlloc.txnPipeliner.hasAcquiredLocks() &&
531-
!tc.interceptorAlloc.txnWriteBuffer.hasBufferedWrites() {
530+
if ba.IsSingleEndTxnRequest() && !tc.hasAcquiredLocksOrBufferedWritesLocked() {
532531
return nil, tc.finalizeNonLockingTxnLocked(ctx, ba)
533532
}
534533

@@ -1724,3 +1723,7 @@ func (tc *TxnCoordSender) hasPerformedWritesLocked() bool {
17241723
func (tc *TxnCoordSender) hasBufferedWritesLocked() bool {
17251724
return tc.interceptorAlloc.txnWriteBuffer.hasBufferedWrites()
17261725
}
1726+
1727+
func (tc *TxnCoordSender) hasAcquiredLocksOrBufferedWritesLocked() bool {
1728+
return tc.interceptorAlloc.txnPipeliner.hasAcquiredLocks() || tc.interceptorAlloc.txnWriteBuffer.hasBufferedWrites()
1729+
}

pkg/kv/kvclient/kvcoord/txn_coord_sender_savepoints.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,12 @@ func (tc *TxnCoordSender) CreateSavepoint(ctx context.Context) (kv.SavepointToke
8989
// We also increment the write sequence if any writes are buffered since
9090
// a buffered write will not appears as a lock acquisition in the
9191
// pipeliner. For SQL-generated transactions, this additional check is
92-
// unnecessary since we won't ever have buffered writes wihout also at
92+
// unnecessary since we won't ever have buffered writes without also at
9393
// least 1 locked row. However, for other types of KV transactions such
9494
// as those generated by KVNemesis we may have blind Put or Delete
9595
// requests that would be erroneously rolled back if we didn't increment
9696
// the sequence here.
97-
if tc.interceptorAlloc.txnPipeliner.hasAcquiredLocks() || tc.interceptorAlloc.txnWriteBuffer.hasBufferedWrites() {
97+
if tc.hasAcquiredLocksOrBufferedWritesLocked() {
9898
if err := tc.interceptorAlloc.txnSeqNumAllocator.stepWriteSeqLocked(ctx); err != nil {
9999
return nil, err
100100
}
@@ -160,7 +160,7 @@ func (tc *TxnCoordSender) RollbackToSavepoint(ctx context.Context, s kv.Savepoin
160160
// TODO(nvanbenschoten): once #113765 is resolved, we should make this
161161
// unconditional and push the write sequence increment into
162162
// txnSeqNumAllocator.rollbackToSavepointLocked.
163-
if tc.interceptorAlloc.txnPipeliner.hasAcquiredLocks() {
163+
if tc.hasAcquiredLocksOrBufferedWritesLocked() {
164164
tc.mu.txn.AddIgnoredSeqNumRange(
165165
enginepb.IgnoredSeqNumRange{
166166
Start: sp.seqNum, End: tc.interceptorAlloc.txnSeqNumAllocator.writeSeq,

pkg/kv/kvnemesis/BUILD.bazel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ go_library(
6262

6363
go_test(
6464
name = "kvnemesis_test",
65-
size = "medium",
65+
size = "large",
6666
srcs = [
6767
"applier_test.go",
6868
"engine_test.go",

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)