Skip to content

Commit 5a27dd9

Browse files
author
gtr
committed
sql: fix VIEWACTIVITY privilege for ListSessions
Fixes cockroachdb#104354. Partially addresses cockroachdb#106588. Previously, when a non-admin user was given the `VIEWACTIVITY` privilege, they were able to see other users' sessions from the SQL shell but not from the UI. This commit fixes the ListSessions endpoint to check for the `VIEWACTIVITY` privilege in addition to the `VIEWACTIVITY` role when returning a response for the ListSessions endpoint. Release note (bug fix): users with the `VIEWACTIVITY` privilege should be able to see other users' sessions from both the CLI and the DB Console.
1 parent 93546c2 commit 5a27dd9

File tree

5 files changed

+171
-32
lines changed

5 files changed

+171
-32
lines changed

pkg/server/application_api/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,5 +85,6 @@ go_test(
8585
"@com_github_kr_pretty//:pretty",
8686
"@com_github_stretchr_testify//assert",
8787
"@com_github_stretchr_testify//require",
88+
"@org_golang_x_sync//errgroup",
8889
],
8990
)

pkg/server/application_api/sessions_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/cockroachdb/cockroach/pkg/util/log"
3535
"github.com/cockroachdb/errors"
3636
"github.com/stretchr/testify/require"
37+
"golang.org/x/sync/errgroup"
3738
)
3839

3940
func TestListSessionsSecurity(t *testing.T) {
@@ -104,6 +105,128 @@ func TestListSessionsSecurity(t *testing.T) {
104105
}
105106
}
106107

108+
// TestListSessionsPrivileges tests that the VIEWACTIVITY and VIEWACTIVITYREDACTED privileges
109+
// are respected when listing sessions, particularly for other users' sessions.
110+
func TestListSessionsPrivileges(t *testing.T) {
111+
defer leaktest.AfterTest(t)()
112+
defer log.Scope(t).Close(t)
113+
114+
// Skip under stress race as the sleep query might finish before the stress race can finish.
115+
skip.UnderStressRace(t, "list sessions privileges")
116+
117+
ts, sqlDB, _ := serverutils.StartServer(t, base.TestServerArgs{})
118+
defer ts.Stopper().Stop(context.Background())
119+
endpoint := "sessions"
120+
appName := "test_sessions_privileges"
121+
user := apiconstants.TestingUserNameNoAdmin().Normalized()
122+
sleepQuery := "SELECT pg_sleep(3000)"
123+
sleepQueryRedacted := "SELECT pg_sleep(_)"
124+
125+
serverSQL := sqlutils.MakeSQLRunner(sqlDB)
126+
serverSQL.Exec(t, fmt.Sprintf(`SET application_name = "%s"`, appName))
127+
queryCtx, cancel := context.WithCancel(context.Background())
128+
129+
// Run a sleep query as root in another goroutine to make sure that root's session has one
130+
// active query while we list sessions. This sleep query will be cancelled at the end of the
131+
// test.
132+
var g errgroup.Group
133+
g.Go(func() error {
134+
_, err := serverSQL.DB.ExecContext(queryCtx, sleepQuery)
135+
if strings.Contains(err.Error(), "canceled") && strings.Contains(queryCtx.Err().Error(), "canceled") {
136+
// Both errors contain the "canceled" substring, this means the query was
137+
// canceled as expected.
138+
return nil
139+
}
140+
t.Errorf("unexpected error: %v", err)
141+
return err
142+
})
143+
144+
// We test all combinations of VIEWACTIVITY and VIEWACTIVITYREDACTED. We could also start
145+
// by granting all privileges and then revoking them one by one, but we want to keep the
146+
// tests as isolated as possible. If a non-admin user has neither privilege, they should
147+
// not see root's session. If a non-admin user has VIEWACTIVITY, they should see root's
148+
// session with the full query. If a non-admin user has VIEWACTIVITYREDACTED, they should
149+
// see root's session with the redacted query. If a non-admin user has both privileges,
150+
// VIEWACTIVITYREDACTED should take precedence.
151+
testCases := []struct {
152+
grantViewActivity bool
153+
grantViewActivityRedacted bool
154+
expectedQuery string
155+
}{
156+
{false, false, ""},
157+
{false, true, sleepQueryRedacted},
158+
{true, false, sleepQuery},
159+
{true, true, sleepQueryRedacted},
160+
}
161+
162+
// Filters sessions by appName.
163+
filterSessions := func(sessions []serverpb.Session) []serverpb.Session {
164+
var filteredSessions []serverpb.Session
165+
for _, s := range sessions {
166+
if s.ApplicationName == appName {
167+
filteredSessions = append(filteredSessions, s)
168+
}
169+
}
170+
return filteredSessions
171+
}
172+
173+
for _, tc := range testCases {
174+
if tc.grantViewActivity {
175+
serverSQL.Exec(t, fmt.Sprintf(`GRANT SYSTEM VIEWACTIVITY TO %s`, user))
176+
}
177+
if tc.grantViewActivityRedacted {
178+
serverSQL.Exec(t, fmt.Sprintf(`GRANT SYSTEM VIEWACTIVITYREDACTED TO %s`, user))
179+
}
180+
181+
var response serverpb.ListSessionsResponse
182+
err := srvtestutils.GetStatusJSONProtoWithAdminOption(ts, endpoint, &response, false)
183+
184+
if err != nil {
185+
t.Errorf("unexpected failure listing sessions from %s; error: %v; response errors: %v",
186+
endpoint, err, response.Errors)
187+
}
188+
189+
filteredSessions := filterSessions(response.Sessions)
190+
numberOfSessions := len(filteredSessions)
191+
192+
// A non-admin user with no privileges should not see any other users' sessions.
193+
if !tc.grantViewActivity && !tc.grantViewActivityRedacted {
194+
if numberOfSessions != 0 {
195+
t.Errorf("expected 0 sessions, but got %d", numberOfSessions)
196+
}
197+
continue
198+
}
199+
200+
// A non-admin user with at least one of the privileges should see other users' sessions.
201+
if numberOfSessions != 1 {
202+
t.Errorf("expected 1 session, but got %d", numberOfSessions)
203+
} else {
204+
session := filteredSessions[0]
205+
numberOfActiveQueries := len(session.ActiveQueries)
206+
if numberOfActiveQueries != 1 {
207+
t.Errorf("expected 1 active query, but got %d", numberOfActiveQueries)
208+
} else {
209+
activeQuery := session.ActiveQueries[0].Sql
210+
if activeQuery != tc.expectedQuery {
211+
t.Errorf("expected active query to be %s, but got %s", tc.expectedQuery, activeQuery)
212+
}
213+
}
214+
}
215+
216+
// Only revoke the privilege if we granted it in this test case.
217+
if tc.grantViewActivity {
218+
serverSQL.Exec(t, fmt.Sprintf(`REVOKE SYSTEM VIEWACTIVITY FROM %s`, user))
219+
}
220+
if tc.grantViewActivityRedacted {
221+
serverSQL.Exec(t, fmt.Sprintf(`REVOKE SYSTEM VIEWACTIVITYREDACTED FROM %s`, user))
222+
}
223+
}
224+
225+
// Cancel the query so that the test can finish.
226+
cancel()
227+
_ = g.Wait()
228+
}
229+
107230
func TestStatusCancelSessionGatewayMetadataPropagation(t *testing.T) {
108231
defer leaktest.AfterTest(t)()
109232
defer log.Scope(t).Close(t)

pkg/server/privchecker/api.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ type SQLPrivilegeChecker interface {
7777

7878
// HasGlobalPrivilege is a convenience wrapper
7979
HasGlobalPrivilege(ctx context.Context, user username.SQLUsername, privilege privilege.Kind) (bool, error)
80+
81+
// HasPrivilegeOrRoleOption is a convenience wrapper
82+
HasPrivilegeOrRoleOption(ctx context.Context, user username.SQLUsername, priv privilege.Kind) (bool, error)
8083
}
8184

8285
// NewChecker constructs a new CheckerForRPCHandlers.

pkg/server/privchecker/privchecker.go

Lines changed: 37 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -67,14 +67,11 @@ func (c *adminPrivilegeChecker) RequireViewActivityPermission(ctx context.Contex
6767
if isAdmin {
6868
return nil
6969
}
70-
if hasView, err := c.HasGlobalPrivilege(ctx, userName, privilege.VIEWACTIVITY); err != nil {
70+
hasView, err := c.HasPrivilegeOrRoleOption(ctx, userName, privilege.VIEWACTIVITY)
71+
if err != nil {
7172
return srverrors.ServerError(ctx, err)
72-
} else if hasView {
73-
return nil
7473
}
75-
if hasView, err := c.HasRoleOption(ctx, userName, roleoption.VIEWACTIVITY); err != nil {
76-
return srverrors.ServerError(ctx, err)
77-
} else if hasView {
74+
if hasView {
7875
return nil
7976
}
8077
return grpcstatus.Errorf(
@@ -93,24 +90,18 @@ func (c *adminPrivilegeChecker) RequireViewActivityOrViewActivityRedactedPermiss
9390
if isAdmin {
9491
return nil
9592
}
96-
if hasView, err := c.HasGlobalPrivilege(ctx, userName, privilege.VIEWACTIVITY); err != nil {
93+
hasView, err := c.HasPrivilegeOrRoleOption(ctx, userName, privilege.VIEWACTIVITY)
94+
if err != nil {
9795
return srverrors.ServerError(ctx, err)
98-
} else if hasView {
99-
return nil
10096
}
101-
if hasViewRedacted, err := c.HasGlobalPrivilege(ctx, userName, privilege.VIEWACTIVITYREDACTED); err != nil {
102-
return srverrors.ServerError(ctx, err)
103-
} else if hasViewRedacted {
97+
if hasView {
10498
return nil
10599
}
106-
if hasView, err := c.HasRoleOption(ctx, userName, roleoption.VIEWACTIVITY); err != nil {
100+
hasViewRedacted, err := c.HasPrivilegeOrRoleOption(ctx, userName, privilege.VIEWACTIVITYREDACTED)
101+
if err != nil {
107102
return srverrors.ServerError(ctx, err)
108-
} else if hasView {
109-
return nil
110103
}
111-
if hasViewRedacted, err := c.HasRoleOption(ctx, userName, roleoption.VIEWACTIVITYREDACTED); err != nil {
112-
return srverrors.ServerError(ctx, err)
113-
} else if hasViewRedacted {
104+
if hasViewRedacted {
114105
return nil
115106
}
116107
return grpcstatus.Errorf(
@@ -129,24 +120,18 @@ func (c *adminPrivilegeChecker) RequireViewClusterSettingOrModifyClusterSettingP
129120
if isAdmin {
130121
return nil
131122
}
132-
if hasView, err := c.HasGlobalPrivilege(ctx, userName, privilege.VIEWCLUSTERSETTING); err != nil {
123+
hasView, err := c.HasPrivilegeOrRoleOption(ctx, userName, privilege.VIEWCLUSTERSETTING)
124+
if err != nil {
133125
return srverrors.ServerError(ctx, err)
134-
} else if hasView {
135-
return nil
136126
}
137-
if hasModify, err := c.HasGlobalPrivilege(ctx, userName, privilege.MODIFYCLUSTERSETTING); err != nil {
138-
return srverrors.ServerError(ctx, err)
139-
} else if hasModify {
127+
if hasView {
140128
return nil
141129
}
142-
if hasView, err := c.HasRoleOption(ctx, userName, roleoption.VIEWCLUSTERSETTING); err != nil {
130+
hasModify, err := c.HasPrivilegeOrRoleOption(ctx, userName, privilege.MODIFYCLUSTERSETTING)
131+
if err != nil {
143132
return srverrors.ServerError(ctx, err)
144-
} else if hasView {
145-
return nil
146133
}
147-
if hasModify, err := c.HasRoleOption(ctx, userName, roleoption.MODIFYCLUSTERSETTING); err != nil {
148-
return srverrors.ServerError(ctx, err)
149-
} else if hasModify {
134+
if hasModify {
150135
return nil
151136
}
152137
return grpcstatus.Errorf(
@@ -307,6 +292,28 @@ func (c *adminPrivilegeChecker) HasRoleOption(
307292
return bool(dbDatum), nil
308293
}
309294

295+
// HasPrivilegeOrRoleOption is a helper function which calls both HasGlobalPrivilege and HasRoleOption.
296+
func (c *adminPrivilegeChecker) HasPrivilegeOrRoleOption(
297+
ctx context.Context, username username.SQLUsername, privilege privilege.Kind,
298+
) (bool, error) {
299+
if privilegeName, err := c.HasGlobalPrivilege(ctx, username, privilege); err != nil {
300+
return false, err
301+
} else if privilegeName {
302+
return true, nil
303+
}
304+
privName := privilege.String()
305+
roleOption, ok := roleoption.ByName[privName]
306+
if !ok {
307+
return false, nil
308+
}
309+
if hasRoleOption, err := c.HasRoleOption(ctx, username, roleOption); err != nil {
310+
return false, err
311+
} else if hasRoleOption {
312+
return true, nil
313+
}
314+
return false, nil
315+
}
316+
310317
// HasGlobalPrivilege is a helper function which calls
311318
// CheckPrivilege and returns a true/false based on the returned
312319
// result.

pkg/server/status.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,12 @@ func (b *baseStatusServer) getLocalSessions(
158158
return nil, srverrors.ServerError(ctx, err)
159159
}
160160

161-
hasViewActivityRedacted, err := b.privilegeChecker.HasRoleOption(ctx, sessionUser, roleoption.VIEWACTIVITYREDACTED)
161+
hasViewActivityRedacted, err := b.privilegeChecker.HasPrivilegeOrRoleOption(ctx, sessionUser, privilege.VIEWACTIVITYREDACTED)
162162
if err != nil {
163163
return nil, srverrors.ServerError(ctx, err)
164164
}
165165

166-
hasViewActivity, err := b.privilegeChecker.HasRoleOption(ctx, sessionUser, roleoption.VIEWACTIVITY)
166+
hasViewActivity, err := b.privilegeChecker.HasPrivilegeOrRoleOption(ctx, sessionUser, privilege.VIEWACTIVITY)
167167
if err != nil {
168168
return nil, srverrors.ServerError(ctx, err)
169169
}
@@ -204,6 +204,11 @@ func (b *baseStatusServer) getLocalSessions(
204204
}
205205
}
206206

207+
// At this point, we have decided if we are going to show all sessions so we
208+
// can set the username to the session user if it is undefined.
209+
if reqUsername.Undefined() {
210+
reqUsername = sessionUser
211+
}
207212
reqUserNameNormalized := reqUsername.Normalized()
208213

209214
userSessions := make([]serverpb.Session, 0, len(sessions)+len(closedSessions))

0 commit comments

Comments
 (0)