Skip to content

Commit c27794a

Browse files
committed
sql: prevent auth failure when no roles are granted during sync
Previously, the EnsureUserOnlyBelongsToRoles function would construct and execute a GRANT statement even if the list of roles to grant was empty after filtering for roles that exist in the database. This was inadequate because it caused authentication to fail for users logging in via external providers (LDAP, JWT, OIDC). If a user belonged to external groups that did not map to any existing roles in CockroachDB, the function would generate an invalid SQL statement (GRANT TO <user>), resulting in a syntax error that blocked the user's session. To address this, this patch modifies EnsureUserOnlyBelongsToRoles to only build and execute the GRANT statement if at least one valid, existing role is being granted. This prevents the syntax error and allows users to log in successfully even if their external group memberships do not result in any new role grants. This commit also adds unit tests for `EnsureUserOnlyBelongsToRoles()`. Fixes: #143878 Release note (bug fix): Fixed a bug where database login could fail during LDAP, JWT, or OIDC authentication if the user's external group memberships did not correspond to any existing roles in the database. The login will now succeed, and no roles will be granted or revoked in this scenario.
1 parent 9644793 commit c27794a

File tree

2 files changed

+138
-6
lines changed

2 files changed

+138
-6
lines changed

pkg/sql/authorization.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -592,21 +592,27 @@ func EnsureUserOnlyBelongsToRoles(
592592
grantStmt := strings.Builder{}
593593
grantStmt.WriteString("GRANT ")
594594
addComma := false
595+
rolesWereAdded := false // Flag to track if any roles are actually added
595596
for _, role := range rolesToGrant {
596597
if roleExists, _ := RoleExists(ctx, txn, role); roleExists {
597598
if addComma {
598599
grantStmt.WriteString(", ")
599600
}
600601
grantStmt.WriteString(role.SQLIdentifier())
601602
addComma = true
603+
rolesWereAdded = true // At least one role was added
602604
}
603605
}
604-
grantStmt.WriteString(" TO ")
605-
grantStmt.WriteString(user.SQLIdentifier())
606-
if _, err := txn.Exec(
607-
ctx, "EnsureUserOnlyBelongsToRoles-grant", txn.KV(), grantStmt.String(),
608-
); err != nil {
609-
return err
606+
607+
// Only execute the GRANT statement if at least one existing role was added.
608+
if rolesWereAdded {
609+
grantStmt.WriteString(" TO ")
610+
grantStmt.WriteString(user.SQLIdentifier())
611+
if _, err := txn.Exec(
612+
ctx, "EnsureUserOnlyBelongsToRoles-grant", txn.KV(), grantStmt.String(),
613+
); err != nil {
614+
return err
615+
}
610616
}
611617
}
612618

pkg/sql/authorization_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"time"
1414

1515
"github.com/cockroachdb/cockroach/pkg/base"
16+
"github.com/cockroachdb/cockroach/pkg/security/username"
17+
"github.com/cockroachdb/cockroach/pkg/sql"
1618
"github.com/cockroachdb/cockroach/pkg/sql/catalog/descs"
1719
"github.com/cockroachdb/cockroach/pkg/sql/sessiondata"
1820
"github.com/cockroachdb/cockroach/pkg/testutils/serverutils"
@@ -163,3 +165,127 @@ COMMIT`,
163165
})
164166
}
165167
}
168+
169+
func TestEnsureUserOnlyBelongsToRoles(t *testing.T) {
170+
defer leaktest.AfterTest(t)()
171+
defer log.Scope(t).Close(t)
172+
173+
ctx := context.Background()
174+
s, db, _ := serverutils.StartServer(t, base.TestServerArgs{})
175+
defer s.Stopper().Stop(ctx)
176+
177+
execCfg := s.ExecutorConfig().(sql.ExecutorConfig)
178+
tdb := sqlutils.MakeSQLRunner(db)
179+
180+
// Helper function to create a username.SQLUsername from a string.
181+
makeRole := func(name string) username.SQLUsername {
182+
r, err := username.MakeSQLUsernameFromUserInput(name, username.PurposeValidation)
183+
require.NoError(t, err)
184+
return r
185+
}
186+
187+
// Helper function to get the current roles of a user and return them as a sorted string slice.
188+
getRoles := func(user username.SQLUsername) []string {
189+
rows := tdb.QueryStr(t, `SELECT role FROM system.role_members WHERE member = $1 ORDER BY role`, user.Normalized())
190+
actualRoles := make([]string, len(rows))
191+
for i, r := range rows {
192+
actualRoles[i] = r[0]
193+
}
194+
return actualRoles
195+
}
196+
197+
// Setup initial users and roles for the tests.
198+
tdb.Exec(t, `
199+
CREATE USER test_user;
200+
CREATE ROLE role1;
201+
CREATE ROLE role2;
202+
CREATE ROLE role3;
203+
CREATE ROLE role4;
204+
GRANT role1, role2 TO test_user;
205+
`)
206+
207+
testUser := makeRole("test_user")
208+
role1 := makeRole("role1")
209+
role2 := makeRole("role2")
210+
role3 := makeRole("role3")
211+
role4 := makeRole("role4")
212+
nonExistentRole := makeRole("non_existent_role")
213+
214+
t.Run("no role is granted or revoked", func(t *testing.T) {
215+
// The user currently has role1 and role2. Syncing with the same set should do nothing.
216+
desiredRoles := []username.SQLUsername{role1, role2}
217+
err := sql.EnsureUserOnlyBelongsToRoles(ctx, &execCfg, testUser, desiredRoles)
218+
require.NoError(t, err)
219+
require.Equal(t, []string{"role1", "role2"}, getRoles(testUser))
220+
})
221+
222+
t.Run("single role is granted", func(t *testing.T) {
223+
// Grant role3. User should now have role1, role2, role3.
224+
desiredRoles := []username.SQLUsername{role1, role2, role3}
225+
err := sql.EnsureUserOnlyBelongsToRoles(ctx, &execCfg, testUser, desiredRoles)
226+
require.NoError(t, err)
227+
require.Equal(t, []string{"role1", "role2", "role3"}, getRoles(testUser))
228+
})
229+
230+
t.Run("single role is revoked", func(t *testing.T) {
231+
// Revoke role1. User should now have role2, role3.
232+
desiredRoles := []username.SQLUsername{role2, role3}
233+
err := sql.EnsureUserOnlyBelongsToRoles(ctx, &execCfg, testUser, desiredRoles)
234+
require.NoError(t, err)
235+
require.Equal(t, []string{"role2", "role3"}, getRoles(testUser))
236+
})
237+
238+
t.Run("multiple roles are granted", func(t *testing.T) {
239+
// Grant role1 and role4. User should now have role1, role2, role3, role4.
240+
desiredRoles := []username.SQLUsername{role1, role2, role3, role4}
241+
err := sql.EnsureUserOnlyBelongsToRoles(ctx, &execCfg, testUser, desiredRoles)
242+
require.NoError(t, err)
243+
require.Equal(t, []string{"role1", "role2", "role3", "role4"}, getRoles(testUser))
244+
})
245+
246+
t.Run("multiple roles are revoked", func(t *testing.T) {
247+
// Revoke role3 and role4. User should now have role1, role2.
248+
desiredRoles := []username.SQLUsername{role1, role2}
249+
err := sql.EnsureUserOnlyBelongsToRoles(ctx, &execCfg, testUser, desiredRoles)
250+
require.NoError(t, err)
251+
require.Equal(t, []string{"role1", "role2"}, getRoles(testUser))
252+
})
253+
254+
t.Run("multiple roles are granted and revoked", func(t *testing.T) {
255+
// Revoke role1, grant role3. User should now have role2, role3.
256+
desiredRoles := []username.SQLUsername{role2, role3}
257+
err := sql.EnsureUserOnlyBelongsToRoles(ctx, &execCfg, testUser, desiredRoles)
258+
require.NoError(t, err)
259+
require.Equal(t, []string{"role2", "role3"}, getRoles(testUser))
260+
})
261+
262+
t.Run("grant includes non-existent roles", func(t *testing.T) {
263+
// Attempt to grant role4 and a non-existent role.
264+
// User should end up with role2, role3, role4. The non-existent role should be ignored.
265+
desiredRoles := []username.SQLUsername{role2, role3, role4, nonExistentRole}
266+
err := sql.EnsureUserOnlyBelongsToRoles(ctx, &execCfg, testUser, desiredRoles)
267+
require.NoError(t, err)
268+
require.Equal(t, []string{"role2", "role3", "role4"}, getRoles(testUser))
269+
})
270+
271+
t.Run("all desired roles are non-existent", func(t *testing.T) {
272+
// Attempt to sync with only a non-existent role.
273+
// All existing roles (role2, role3, role4) should be revoked, and nothing new granted.
274+
desiredRoles := []username.SQLUsername{nonExistentRole}
275+
err := sql.EnsureUserOnlyBelongsToRoles(ctx, &execCfg, testUser, desiredRoles)
276+
require.NoError(t, err)
277+
require.Empty(t, getRoles(testUser))
278+
})
279+
280+
t.Run("empty desired roles list revokes all", func(t *testing.T) {
281+
// First, grant some roles back to the user.
282+
tdb.Exec(t, "GRANT role1, role2 TO test_user")
283+
require.Equal(t, []string{"role1", "role2"}, getRoles(testUser))
284+
285+
// Now, sync with an empty list. All roles should be revoked.
286+
desiredRoles := []username.SQLUsername{}
287+
err := sql.EnsureUserOnlyBelongsToRoles(ctx, &execCfg, testUser, desiredRoles)
288+
require.NoError(t, err)
289+
require.Empty(t, getRoles(testUser))
290+
})
291+
}

0 commit comments

Comments
 (0)