Skip to content

Commit 027bdb4

Browse files
craig[bot]Dedej-Bergin
andcommitted
Merge #144867
144867: pkg/sql: fix child role does not inherit parent role table access r=Dedej-Bergin a=Dedej-Bergin This change ensures that policies correctly respect role inheritance. Inside the `policy.go` `AppliesToRole` method a call to GetRolesForMember was added to check all inherited roles for the member also. Fixes: #144780 Epic: CRDB-11724 Release note: None. Co-authored-by: Bergin Dedej <[email protected]>
2 parents 3f1bae1 + a930197 commit 027bdb4

File tree

7 files changed

+166
-9
lines changed

7 files changed

+166
-9
lines changed

pkg/sql/logictest/testdata/logic_test/row_level_security

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4288,4 +4288,114 @@ alter_policy_table_locked CREATE TABLE public.alter_policy_table_locked (
42884288
) WITH (schema_locked = true);
42894289
CREATE POLICY p_sel ON public.alter_policy_table_locked AS PERMISSIVE FOR ALL TO public WITH CHECK (false)
42904290

4291+
subtest bug_fix_policy_respects_inheritance_#144780
4292+
4293+
statement ok
4294+
CREATE ROLE parent_role
4295+
4296+
statement ok
4297+
CREATE ROLE child_role
4298+
4299+
statement ok
4300+
GRANT parent_role TO child_role
4301+
4302+
statement ok
4303+
CREATE TABLE employees (id SERIAL PRIMARY KEY, name TEXT, department TEXT)
4304+
4305+
statement ok
4306+
INSERT INTO employees VALUES (1, 'Alice', 'Engineering')
4307+
4308+
statement ok
4309+
ALTER TABLE employees ENABLE ROW LEVEL SECURITY
4310+
4311+
statement ok
4312+
CREATE POLICY parent_policy_select ON employees FOR SELECT TO parent_role USING (true)
4313+
4314+
statement ok
4315+
CREATE POLICY parent_policy_insert ON employees FOR INSERT TO parent_role WITH CHECK (department IN ('Engineering', 'Sales'))
4316+
4317+
statement ok
4318+
CREATE POLICY parent_policy_update ON employees FOR UPDATE TO parent_role USING (false)
4319+
4320+
statement ok
4321+
GRANT SELECT, INSERT, UPDATE ON employees TO parent_role
4322+
4323+
# Test with role inheritance - child_role should inherit parent_role's permissions.
4324+
statement ok
4325+
SET ROLE child_role
4326+
4327+
query ITT
4328+
SELECT * FROM employees
4329+
----
4330+
1 Alice Engineering
4331+
4332+
# Test INSERT with role inheritance - should succeed for allowed department.
4333+
statement ok
4334+
INSERT INTO employees (id, name, department) VALUES (2, 'Bob', 'Engineering')
4335+
4336+
# Test INSERT with role inheritance - should succeed for another allowed department.
4337+
statement ok
4338+
INSERT INTO employees (id, name, department) VALUES (3, 'Carol', 'Sales')
4339+
4340+
# Test INSERT with role inheritance - should fail for disallowed department.
4341+
statement error pq: new row violates row-level security policy for table "employees"
4342+
INSERT INTO employees (id, name, department) VALUES (4, 'Dave', 'Finance')
4343+
4344+
statement ok
4345+
UPDATE employees SET name = 'Robert' WHERE name = 'Bob'
4346+
4347+
query ITT
4348+
SELECT * FROM employees ORDER BY id
4349+
----
4350+
1 Alice Engineering
4351+
2 Bob Engineering
4352+
3 Carol Sales
4353+
4354+
statement ok
4355+
RESET ROLE
4356+
4357+
# Now revoke the parent role and ensure permissions don't apply anymore.
4358+
statement ok
4359+
REVOKE parent_role FROM child_role
4360+
4361+
statement ok
4362+
GRANT ALL ON employees TO child_role
4363+
4364+
statement ok
4365+
SET ROLE child_role
4366+
4367+
# Should see no rows because the policy doesn't apply anymore.
4368+
query ITT
4369+
SELECT * FROM employees
4370+
----
4371+
4372+
# All write operations should fail now.
4373+
statement error pq: new row violates row-level security policy for table "employees"
4374+
INSERT INTO employees (id, name, department) VALUES (3, 'Eve', 'Engineering')
4375+
4376+
# Test UPDATE with role inheritance - should succeed,
4377+
# but the update will not go as the default with ENABLED ROW LEVEL SECURITY.
4378+
statement ok
4379+
UPDATE employees SET name = 'Alice 2.0' WHERE name = 'Alice'
4380+
4381+
statement ok
4382+
RESET ROLE
4383+
4384+
# Can see that no change was made by the update.
4385+
query ITT
4386+
SELECT * FROM employees ORDER BY id
4387+
----
4388+
1 Alice Engineering
4389+
2 Bob Engineering
4390+
3 Carol Sales
4391+
4392+
statement ok
4393+
DROP TABLE employees CASCADE
4394+
4395+
statement ok
4396+
DROP ROLE child_role
4397+
4398+
statement ok
4399+
DROP ROLE parent_role
4400+
42914401
subtest end

pkg/sql/opt/cat/catalog.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@ type Catalog interface {
236236
// UserHasAdminRole checks if the specified user has admin privileges.
237237
UserHasAdminRole(ctx context.Context, user username.SQLUsername) (bool, error)
238238

239+
// UserIsMemberOfAnyRole checks if the specified user is a member of any of the given roles,
240+
// either directly or indirectly through role inheritance.
241+
UserIsMemberOfAnyRole(ctx context.Context, user username.SQLUsername, roles map[username.SQLUsername]struct{}) (bool, error)
242+
239243
// HasRoleOption converts the roleoption to its SQL column name and checks if
240244
// the user belongs to a role where the option has value true. Requires a
241245
// valid transaction to be open.

pkg/sql/opt/cat/policy.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
package cat
77

88
import (
9+
"context"
10+
911
"github.com/cockroachdb/cockroach/pkg/security/username"
1012
"github.com/cockroachdb/cockroach/pkg/sql/catalog/catpb"
1113
"github.com/cockroachdb/cockroach/pkg/sql/catalog/descpb"
@@ -61,7 +63,7 @@ type Policy struct {
6163
Command catpb.PolicyCommand
6264
// roles are the roles the applies to. If the policy applies to all roles (aka
6365
// public), this will be nil.
64-
roles map[string]struct{}
66+
roles map[username.SQLUsername]struct{}
6567
}
6668

6769
// Policies contains the policies for a single table.
@@ -76,7 +78,7 @@ func (p *Policy) InitRoles(roleNames []string) {
7678
p.roles = nil
7779
return
7880
}
79-
roles := make(map[string]struct{})
81+
roles := make(map[username.SQLUsername]struct{})
8082
for _, r := range roleNames {
8183
if r == username.PublicRole {
8284
// If the public role is defined, there is no need to check the
@@ -85,17 +87,23 @@ func (p *Policy) InitRoles(roleNames []string) {
8587
roles = nil
8688
break
8789
}
88-
roles[r] = struct{}{}
90+
roleUsername := username.MakeSQLUsernameFromPreNormalizedString(r)
91+
roles[roleUsername] = struct{}{}
8992
}
9093
p.roles = roles
9194
}
9295

93-
// AppliesToRole checks whether the policy applies to the give role.
94-
func (p *Policy) AppliesToRole(user username.SQLUsername) bool {
96+
// AppliesToRole checks whether the policy applies to the given role.
97+
func (p *Policy) AppliesToRole(ctx context.Context, cat Catalog, user username.SQLUsername) bool {
9598
// If no roles are specified, assume the policy applies to all users (public role).
9699
if p.roles == nil {
97100
return true
98101
}
99-
_, found := p.roles[user.Normalized()]
100-
return found
102+
103+
// Check if the user belongs to any of the roles in the policy
104+
belongs, err := cat.UserIsMemberOfAnyRole(ctx, user, p.roles)
105+
if err != nil {
106+
panic(err)
107+
}
108+
return belongs
101109
}

pkg/sql/opt/optbuilder/mutation_builder.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1150,7 +1150,7 @@ func (mb *mutationBuilder) genPolicyExpr(
11501150

11511151
// Create a closure to handle building the expression for one policy.
11521152
buildForPolicy := func(p cat.Policy, combineScalars func(opt.ScalarExpr, opt.ScalarExpr) opt.ScalarExpr) {
1153-
if !p.AppliesToRole(mb.b.checkPrivilegeUser) || !policyAppliesToCommandScope(p, cmdScope) {
1153+
if !p.AppliesToRole(mb.b.ctx, mb.b.catalog, mb.b.checkPrivilegeUser) || !policyAppliesToCommandScope(p, cmdScope) {
11541154
return
11551155
}
11561156
policiesUsed.Add(p.ID)

pkg/sql/opt/optbuilder/row_level_security.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func (b *Builder) buildRowLevelSecurityUsingExpressionForCommand(
117117

118118
// Create a closure to handle building the expression for one policy.
119119
buildForPolicy := func(policy cat.Policy, restrictive bool) {
120-
if !policy.AppliesToRole(b.checkPrivilegeUser) || !policyAppliesToCommandScope(policy, cmdScope) {
120+
if !policy.AppliesToRole(b.ctx, b.catalog, b.checkPrivilegeUser) || !policyAppliesToCommandScope(policy, cmdScope) {
121121
return
122122
}
123123
strExpr := policy.UsingExpr

pkg/sql/opt/testutils/testcat/test_catalog.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,16 @@ func (tc *Catalog) UserHasAdminRole(ctx context.Context, user username.SQLUserna
343343
return roleMembership.isMemberOfAdminRole, nil
344344
}
345345

346+
// UserIsMemberOfAnyRole is part of the cat.Catalog interface.
347+
func (tc *Catalog) UserIsMemberOfAnyRole(
348+
ctx context.Context, user username.SQLUsername, roles map[username.SQLUsername]struct{},
349+
) (bool, error) {
350+
if _, found := roles[user]; found {
351+
return true, nil
352+
}
353+
return false, nil
354+
}
355+
346356
// HasRoleOption is part of the cat.Catalog interface.
347357
func (tc *Catalog) HasRoleOption(ctx context.Context, roleOption roleoption.Option) (bool, error) {
348358
return true, nil

pkg/sql/opt_catalog.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,31 @@ func (oc *optCatalog) UserHasAdminRole(
489489
return oc.planner.UserHasAdminRole(ctx, user)
490490
}
491491

492+
// UserIsMemberOfAnyRole is part of the cat.Catalog interface.
493+
func (oc *optCatalog) UserIsMemberOfAnyRole(
494+
ctx context.Context, user username.SQLUsername, roles map[username.SQLUsername]struct{},
495+
) (bool, error) {
496+
// First check if the user directly matches any of the roles
497+
if _, found := roles[user]; found {
498+
return true, nil
499+
}
500+
501+
// Get all roles the user belongs to
502+
memberRoles, err := oc.planner.MemberOfWithAdminOption(ctx, user)
503+
if err != nil {
504+
return false, err
505+
}
506+
507+
// Check if any of the target roles are in the user's roles
508+
for role := range roles {
509+
if _, isMember := memberRoles[role]; isMember {
510+
return true, nil
511+
}
512+
}
513+
514+
return false, nil
515+
}
516+
492517
// HasRoleOption is part of the cat.Catalog interface.
493518
func (oc *optCatalog) HasRoleOption(
494519
ctx context.Context, roleOption roleoption.Option,

0 commit comments

Comments
 (0)