Skip to content

Commit 3c68169

Browse files
authored
Merge pull request #149747 from shriramters/blathers/backport-release-25.2-149638
release-25.2: sql: prevent auth failure when no roles are granted during sync
2 parents a82c2cc + 9d3ca00 commit 3c68169

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)