Skip to content

Commit

Permalink
sql: prevent dropped user from accessing the cluster
Browse files Browse the repository at this point in the history
Release note (bug fix): Previously if a user was logged in while a
different session dropped that user, the dropped user would still
inherit privileges from the `public` role. Now, CockroachDB checks that
the user exists before allowing it to inherit privileges from the
`public` role.

In addition, any active web sessions are now revoked when a user is dropped.
  • Loading branch information
rafiss committed Jun 5, 2023
1 parent cd9da14 commit 749f10d
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 0 deletions.
10 changes: 10 additions & 0 deletions pkg/sql/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,16 @@ func (p *planner) HasPrivilege(

// Check if the 'public' pseudo-role has privileges.
if privs.CheckPrivilege(username.PublicRoleName(), privilegeKind) {
// Before returning true, make sure the user actually exists.
// We only need to check for existence here, since a dropped user will not
// appear in the role hierarchy, so it cannot pass the privilege check made
// later in this function. The RoleExists call performs a system table
// lookup, so it's better not to do it in the general case.
if exists, err := p.RoleExists(ctx, user); err != nil {
return false, err
} else if !exists {
return false, pgerror.Newf(pgcode.UndefinedObject, "role %s was concurrently dropped", user)
}
return true, nil
}

Expand Down
11 changes: 11 additions & 0 deletions pkg/sql/drop_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,17 @@ func (n *DropRoleNode) startExec(params runParams) error {
}
numRoleSettingsRowsDeleted += rowsDeleted

_, err = params.p.InternalSQLTxn().ExecEx(
params.ctx,
opName,
params.p.txn,
sessiondata.NodeUserSessionDataOverride,
`UPDATE system.web_sessions SET "revokedAt" = now() WHERE username = $1 AND "revokedAt" IS NULL;`,
normalizedUsername,
)
if err != nil {
return err
}
}

// Bump role-related table versions to force a refresh of membership/auth
Expand Down
1 change: 1 addition & 0 deletions pkg/sql/tests/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ go_test(
"autocommit_extended_protocol_test.go",
"bank_test.go",
"copy_file_upload_test.go",
"drop_role_concurrent_session_test.go",
"empty_query_test.go",
"enum_test.go",
"hash_sharded_test.go",
Expand Down
96 changes: 96 additions & 0 deletions pkg/sql/tests/drop_role_concurrent_session_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright 2023 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package tests

import (
"context"
"net/url"
"testing"
"time"

"github.com/cockroachdb/cockroach/pkg/base"
"github.com/cockroachdb/cockroach/pkg/testutils/serverutils"
"github.com/cockroachdb/cockroach/pkg/testutils/sqlutils"
"github.com/cockroachdb/cockroach/pkg/util/leaktest"
"github.com/cockroachdb/cockroach/pkg/util/log"
"github.com/cockroachdb/cockroach/pkg/util/timeutil"
"github.com/jackc/pgx/v4"
"github.com/stretchr/testify/require"
)

// Test that if a user is dropped while a session is active, the session can
// no longer access objects in the cluster.
func TestDropRoleConcurrentSession(t *testing.T) {
defer leaktest.AfterTest(t)()
defer log.Scope(t).Close(t)

ctx := context.Background()
s, rootDB, _ := serverutils.StartServer(t, base.TestServerArgs{})
defer s.Stopper().Stop(ctx)

_, err := rootDB.Exec(`CREATE USER testuser`)
require.NoError(t, err)
_, err = rootDB.Exec(`CREATE ROLE testrole`)
require.NoError(t, err)
_, err = rootDB.Exec(`GRANT testrole TO testuser`)
require.NoError(t, err)
_, err = rootDB.Exec(`CREATE TABLE inherit_from_public (a INT PRIMARY KEY)`)
require.NoError(t, err)
_, err = rootDB.Exec(`INSERT INTO inherit_from_public VALUES (1)`)
require.NoError(t, err)
_, err = rootDB.Exec(`GRANT SELECT ON inherit_from_public TO public`)
require.NoError(t, err)
_, err = rootDB.Exec(`CREATE TABLE inherit_from_role (a INT PRIMARY KEY)`)
require.NoError(t, err)
_, err = rootDB.Exec(`INSERT INTO inherit_from_role VALUES (2)`)
require.NoError(t, err)
_, err = rootDB.Exec(`GRANT SELECT ON inherit_from_role TO testrole`)
require.NoError(t, err)

// Start a session as testuser.
testUserURL, cleanupFunc := sqlutils.PGUrl(t, s.ServingSQLAddr(), "TestDropRoleConcurrentSession", url.User("testuser"))
defer cleanupFunc()
testuserConn, err := pgx.Connect(ctx, testUserURL.String())
defer func() { _ = testuserConn.Close(ctx) }()

// Also create a web session for testuser.
_, err = rootDB.Exec(`INSERT INTO system.web_sessions ("hashedSecret", username, "expiresAt", user_id)
VALUES ($1, $2, $3, (SELECT user_id FROM system.users WHERE username = $2))`,
"secret", "testuser", timeutil.Now().Add(24*time.Hour))

// Verify that testuser can access the cluster.
var i int
err = testuserConn.QueryRow(ctx, `SELECT * FROM inherit_from_public`).Scan(&i)
require.NoError(t, err)
require.Equal(t, 1, i)
err = testuserConn.QueryRow(ctx, `SELECT * FROM inherit_from_role`).Scan(&i)
require.NoError(t, err)
require.Equal(t, 2, i)
_, err = testuserConn.Exec(ctx, "CREATE TABLE new1(a INT PRIMARY KEY)")
require.NoError(t, err)
_, err = testuserConn.Exec(ctx, "DROP TABLE new1")
require.NoError(t, err)

// Drop testuser, and verify that it no longer has access.
_, err = rootDB.Exec(`DROP USER testuser`)
require.NoError(t, err)
err = testuserConn.QueryRow(ctx, `SELECT * FROM inherit_from_public`).Scan(&i)
require.ErrorContains(t, err, "role testuser was concurrently dropped")
err = testuserConn.QueryRow(ctx, `SELECT * FROM inherit_from_role`).Scan(&i)
require.ErrorContains(t, err, "role testuser was concurrently dropped")
_, err = testuserConn.Exec(ctx, "CREATE TABLE new2(a INT PRIMARY KEY)")
require.ErrorContains(t, err, "role testuser was concurrently dropped")

var revokedAt time.Time
err = rootDB.QueryRow(`SELECT "revokedAt" FROM system.web_sessions WHERE username = $1`, "testuser").Scan(&revokedAt)
require.NoError(t, err)
require.Less(t, revokedAt, timeutil.Now())
}

0 comments on commit 749f10d

Please sign in to comment.