Skip to content

Commit 4d0005f

Browse files
craig[bot]spilchen
andcommitted
Merge #143885
143885: sql: support BYPASSRLS role option for RLS policies r=spilchen a=spilchen Previously, attempts to use the BYPASSRLS role option with row-level security (RLS) policies resulted in an unimplemented error. This change lifts those restrictions and adds proper support for roles or system privileges with the BYPASSRLS flag, allowing them to bypass RLS policies on tables where it is enabled. Closes #136910 Epic: CRDB-11724 Release note: none Co-authored-by: Matt Spilchen <[email protected]>
2 parents 3a6c753 + ea781a0 commit 4d0005f

File tree

12 files changed

+261
-21
lines changed

12 files changed

+261
-21
lines changed

pkg/sql/authorization.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ type AuthorizationAccessor interface {
8989
// has a global privilege or the corresponding legacy role option.
9090
HasGlobalPrivilegeOrRoleOption(ctx context.Context, privilege privilege.Kind) (bool, error)
9191

92+
// UserHasGlobalPrivilegeOrRoleOption is like HasGlobalPrivilegeOrRoleOption,
93+
// except that it is for a specific user.
94+
UserHasGlobalPrivilegeOrRoleOption(ctx context.Context, privilege privilege.Kind, user username.SQLUsername) (bool, error)
95+
9296
// CheckGlobalPrivilegeOrRoleOption checks if the current user has a global privilege
9397
// or the corresponding legacy role option, and returns an error if the user does not.
9498
CheckGlobalPrivilegeOrRoleOption(ctx context.Context, privilege privilege.Kind) error
@@ -692,7 +696,15 @@ func (p *planner) CheckRoleOption(ctx context.Context, roleOption roleoption.Opt
692696
func (p *planner) HasGlobalPrivilegeOrRoleOption(
693697
ctx context.Context, privilege privilege.Kind,
694698
) (bool, error) {
695-
ok, err := p.HasPrivilege(ctx, syntheticprivilege.GlobalPrivilegeObject, privilege, p.User())
699+
return p.UserHasGlobalPrivilegeOrRoleOption(ctx, privilege, p.User())
700+
}
701+
702+
// UserHasGlobalPrivilegeOrRoleOption is like HasGlobalPrivilegeOrRoleOption, but
703+
// is for a specific user.
704+
func (p *planner) UserHasGlobalPrivilegeOrRoleOption(
705+
ctx context.Context, privilege privilege.Kind, user username.SQLUsername,
706+
) (bool, error) {
707+
ok, err := p.HasPrivilege(ctx, syntheticprivilege.GlobalPrivilegeObject, privilege, user)
696708
if err != nil {
697709
return false, err
698710
}
@@ -701,7 +713,7 @@ func (p *planner) HasGlobalPrivilegeOrRoleOption(
701713
}
702714
maybeRoleOptionName := string(privilege.DisplayName())
703715
if roleOption, ok := roleoption.ByName[maybeRoleOptionName]; ok {
704-
return p.HasRoleOption(ctx, roleOption)
716+
return p.UserHasRoleOption(ctx, user, roleOption)
705717
}
706718
return false, nil
707719
}

pkg/sql/logictest/testdata/logic_test/row_level_security

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2969,4 +2969,175 @@ DROP ROLE test_role1;
29692969
statement ok
29702970
DROP ROLE test_role2;
29712971

2972+
# Ensure that if a role has the BYPASSRLS option or privilege set, that it's
2973+
# exempt from policies.
2974+
subtest bypassrls
2975+
2976+
statement ok
2977+
CREATE TABLE bypassrls (id INT PRIMARY KEY, val TEXT);
2978+
2979+
statement ok
2980+
CREATE USER bypassrls_user;
2981+
2982+
statement ok
2983+
GRANT ALL ON bypassrls TO bypassrls_user;
2984+
2985+
statement ok
2986+
ALTER TABLE bypassrls ENABLE ROW LEVEL SECURITY;
2987+
2988+
statement ok
2989+
CREATE POLICY pol1 ON bypassrls TO bypassrls_user USING (val like 'visible: %');
2990+
2991+
statement ok
2992+
CREATE FUNCTION insert_policy_violation_as_session_user(id INT) RETURNS TABLE(id INT, description TEXT)
2993+
LANGUAGE SQL AS
2994+
$$
2995+
INSERT INTO bypassrls VALUES (id, 'hidden: value') RETURNING id, val
2996+
$$;
2997+
2998+
# This function is like the above, except it will always be run as admin since
2999+
# it uses SECURITY DEFINER.
3000+
statement ok
3001+
CREATE FUNCTION insert_policy_violation_as_admin(id INT) RETURNS TABLE(id INT, description TEXT)
3002+
LANGUAGE SQL AS
3003+
$$
3004+
INSERT INTO bypassrls VALUES (id, 'hidden: value') RETURNING id, val
3005+
$$ SECURITY DEFINER;
3006+
3007+
statement ok
3008+
INSERT INTO bypassrls VALUES (0, 'visible: 0'), (1, 'hidden: 1'), (2, 'visible: 2');
3009+
3010+
query IT
3011+
SELECT * FROM insert_policy_violation_as_admin(10);
3012+
----
3013+
10 hidden: value
3014+
3015+
query IT
3016+
SELECT * FROM insert_policy_violation_as_session_user(11);
3017+
----
3018+
11 hidden: value
3019+
3020+
query IT
3021+
SELECT * FROM bypassrls ORDER BY id;
3022+
----
3023+
0 visible: 0
3024+
1 hidden: 1
3025+
2 visible: 2
3026+
10 hidden: value
3027+
11 hidden: value
3028+
3029+
statement ok
3030+
SET ROLE bypassrls_user;
3031+
3032+
query IT
3033+
SELECT * FROM bypassrls ORDER BY id;
3034+
----
3035+
0 visible: 0
3036+
2 visible: 2
3037+
3038+
query IT
3039+
SELECT * FROM insert_policy_violation_as_admin(20);
3040+
----
3041+
20 hidden: value
3042+
3043+
statement error pq: new row violates row-level security policy for table "bypassrls"
3044+
SELECT * FROM insert_policy_violation_as_session_user(21);
3045+
3046+
statement ok
3047+
SET ROLE root;
3048+
3049+
statement error pq: conflicting role options
3050+
ALTER ROLE bypassrls_user BYPASSRLS NOBYPASSRLS;
3051+
3052+
statement ok
3053+
ALTER ROLE bypassrls_user BYPASSRLS;
3054+
3055+
statement ok
3056+
SET ROLE bypassrls_user;
3057+
3058+
query IT
3059+
SELECT * FROM bypassrls ORDER BY id;
3060+
----
3061+
0 visible: 0
3062+
1 hidden: 1
3063+
2 visible: 2
3064+
10 hidden: value
3065+
11 hidden: value
3066+
20 hidden: value
3067+
3068+
query IT
3069+
SELECT * FROM insert_policy_violation_as_admin(30);
3070+
----
3071+
30 hidden: value
3072+
3073+
query IT
3074+
SELECT * FROM insert_policy_violation_as_session_user(31);
3075+
----
3076+
31 hidden: value
3077+
3078+
statement ok
3079+
SET ROLE root
3080+
3081+
statement ok
3082+
ALTER ROLE bypassrls_user NOBYPASSRLS;
3083+
3084+
statement ok
3085+
GRANT SYSTEM BYPASSRLS TO bypassrls_user;
3086+
3087+
query TTB colnames
3088+
SELECT * FROM [SHOW SYSTEM GRANTS] WHERE privilege_type = 'BYPASSRLS';
3089+
----
3090+
grantee privilege_type is_grantable
3091+
bypassrls_user BYPASSRLS false
3092+
3093+
statement ok
3094+
SET ROLE bypassrls_user;
3095+
3096+
query IT
3097+
SELECT * FROM bypassrls ORDER BY id;
3098+
----
3099+
0 visible: 0
3100+
1 hidden: 1
3101+
2 visible: 2
3102+
10 hidden: value
3103+
11 hidden: value
3104+
20 hidden: value
3105+
30 hidden: value
3106+
31 hidden: value
3107+
3108+
query IT
3109+
SELECT * FROM insert_policy_violation_as_session_user(40);
3110+
----
3111+
40 hidden: value
3112+
3113+
statement ok
3114+
SET ROLE root;
3115+
3116+
statement ok
3117+
REVOKE SYSTEM BYPASSRLS FROM bypassrls_user;
3118+
3119+
statement ok
3120+
SET ROLE bypassrls_user;
3121+
3122+
query IT
3123+
SELECT * FROM bypassrls ORDER BY id;
3124+
----
3125+
0 visible: 0
3126+
2 visible: 2
3127+
3128+
statement error pq: new row violates row-level security policy for table "bypassrls"
3129+
SELECT * FROM insert_policy_violation_as_session_user(50);
3130+
3131+
statement ok
3132+
SET ROLE root;
3133+
3134+
statement ok
3135+
DROP FUNCTION insert_policy_violation_as_admin, insert_policy_violation_as_session_user
3136+
3137+
statement ok
3138+
DROP TABLE bypassrls;
3139+
3140+
statement ok
3141+
DROP USER bypassrls_user;
3142+
29723143
subtest end

pkg/sql/opt/cat/catalog.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,10 @@ type Catalog interface {
241241
// NOLOGIN instead of LOGIN.
242242
HasRoleOption(ctx context.Context, roleOption roleoption.Option) (bool, error)
243243

244+
// UserHasGlobalPrivilegeOrRoleOption returns a bool representing whether the given user
245+
// has a global privilege or the corresponding legacy role option.
246+
UserHasGlobalPrivilegeOrRoleOption(ctx context.Context, privilege privilege.Kind, user username.SQLUsername) (bool, error)
247+
244248
// FullyQualifiedName retrieves the fully qualified name of a data source.
245249
// Note that:
246250
// - this call may involve a database operation so it shouldn't be used in

pkg/sql/opt/exec/execbuilder/relational.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ func (b *Builder) maybeAnnotatePolicyInfo(node exec.Node, e memo.RelExpr) {
463463
policiesApplied, found := rlsMeta.PoliciesApplied[tabID]
464464
if found {
465465
val := exec.RLSPoliciesApplied{
466-
PoliciesSkippedForRole: rlsMeta.HasAdminRole || policiesApplied.NoForceExempt,
466+
PoliciesSkippedForRole: rlsMeta.HasAdminRole || policiesApplied.NoForceExempt || policiesApplied.BypassRLS,
467467
}
468468
if applyFilterExpr {
469469
val.Policies = policiesApplied.Filter

pkg/sql/opt/memo/memo_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,8 @@ func TestMemoIsStale(t *testing.T) {
639639

640640
// User changes (with RLS)
641641
o.Memo().Metadata().SetRLSEnabled(evalCtx.SessionData().User(), true, /* admin */
642-
1 /* tableID */, false /* isTableOwnerAndNotForced */)
642+
1 /* tableID */, false, /* isTableOwnerAndNotForced */
643+
false /* bypassRLS */)
643644
notStale()
644645
evalCtx.SessionData().UserProto = newUser
645646
stale()

pkg/sql/opt/metadata.go

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,10 +1187,13 @@ func (md *Metadata) TestingPrivileges() map[cat.StableID]privilegeBitmap {
11871187
// SetRLSEnabled will update the metadata to indicate we came across a table
11881188
// that had row-level security enabled.
11891189
func (md *Metadata) SetRLSEnabled(
1190-
user username.SQLUsername, isAdmin bool, tableID TableID, isTableOwnerAndNotForced bool,
1190+
user username.SQLUsername,
1191+
isAdmin bool,
1192+
tableID TableID,
1193+
isTableOwnerAndNotForced, bypassRLS bool,
11911194
) {
11921195
md.rlsMeta.MaybeInit(user, isAdmin)
1193-
md.rlsMeta.AddTableUse(tableID, isTableOwnerAndNotForced)
1196+
md.rlsMeta.AddTableUse(tableID, isTableOwnerAndNotForced, bypassRLS)
11941197
}
11951198

11961199
// ClearRLSEnabled will clear out the initialized state for the rls meta. This
@@ -1231,8 +1234,26 @@ func (md *Metadata) checkRLSDependencies(
12311234
return false, nil
12321235
}
12331236

1234-
// We do not check for specific policy changes. Any time a policy is modified
1235-
// on a table, a new version of the table descriptor is created. The metadata
1236-
// dependency check already accounts for changes in the table descriptor version.
1237+
// Check if the current user has a role option/privilege that changed
1238+
// affecting the exemption of policies.
1239+
for i := range md.tables {
1240+
table := &md.tables[i]
1241+
policiesApplied, ok := md.rlsMeta.PoliciesApplied[table.MetaID]
1242+
if !ok {
1243+
continue
1244+
}
1245+
bypassRLS, err := optCatalog.UserHasGlobalPrivilegeOrRoleOption(ctx, privilege.BYPASSRLS, md.rlsMeta.User)
1246+
if err != nil {
1247+
return false, err
1248+
}
1249+
if bypassRLS != policiesApplied.BypassRLS {
1250+
return false, nil
1251+
}
1252+
}
1253+
1254+
// We do not check for specific policy changes or exemption due to FORCE RLS.
1255+
// Any time a policy or table attribute such as forced is modified on a table,
1256+
// a new version of the table descriptor is created. The metadata dependency
1257+
// check already accounts for changes in the table descriptor version.
12371258
return true, nil
12381259
}

pkg/sql/opt/optbuilder/row_level_security.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/cockroachdb/cockroach/pkg/sql/opt/cat"
1818
"github.com/cockroachdb/cockroach/pkg/sql/opt/memo"
1919
"github.com/cockroachdb/cockroach/pkg/sql/parser"
20+
"github.com/cockroachdb/cockroach/pkg/sql/privilege"
2021
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
2122
"github.com/cockroachdb/cockroach/pkg/sql/types"
2223
"github.com/cockroachdb/cockroach/pkg/util/intsets"
@@ -42,9 +43,14 @@ func (b *Builder) addRowLevelSecurityFilter(
4243
if err != nil {
4344
panic(err)
4445
}
45-
b.factory.Metadata().SetRLSEnabled(b.checkPrivilegeUser, isAdmin, tabMeta.MetaID, isOwnerAndNotForced)
46+
bypassRLS, err := b.catalog.UserHasGlobalPrivilegeOrRoleOption(b.ctx, privilege.BYPASSRLS, b.checkPrivilegeUser)
47+
if err != nil {
48+
panic(err)
49+
}
50+
b.factory.Metadata().SetRLSEnabled(b.checkPrivilegeUser, isAdmin, tabMeta.MetaID,
51+
isOwnerAndNotForced, bypassRLS)
4652
// Check if RLS filtering is exempt.
47-
if isAdmin || isOwnerAndNotForced {
53+
if isAdmin || isOwnerAndNotForced || bypassRLS {
4854
return
4955
}
5056

@@ -225,8 +231,12 @@ func (r *optRLSConstraintBuilder) genExpression(ctx context.Context) (string, []
225231
if err != nil {
226232
panic(err)
227233
}
228-
r.md.SetRLSEnabled(r.user, isAdmin, r.tabMeta.MetaID, isOwnerAndNotForced)
229-
if isAdmin || isOwnerAndNotForced {
234+
bypassRLS, err := r.oc.UserHasGlobalPrivilegeOrRoleOption(ctx, privilege.BYPASSRLS, r.user)
235+
if err != nil {
236+
panic(err)
237+
}
238+
r.md.SetRLSEnabled(r.user, isAdmin, r.tabMeta.MetaID, isOwnerAndNotForced, bypassRLS)
239+
if isAdmin || isOwnerAndNotForced || bypassRLS {
230240
// Return a constraint check that always passes.
231241
return "true", nil
232242
}

pkg/sql/opt/row_level_security.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,13 @@ func (r *RowLevelSecurityMeta) Clear() {
5454
// AddTableUse indicates that an RLS-enabled table was encountered while
5555
// building the query plan. If any policies are in use, they will be added
5656
// via the AddPolicyUse call.
57-
func (r *RowLevelSecurityMeta) AddTableUse(tableID TableID, isTableOwnerAndNotForced bool) {
57+
func (r *RowLevelSecurityMeta) AddTableUse(
58+
tableID TableID, isTableOwnerAndNotForced, bypassRLS bool,
59+
) {
5860
if _, found := r.PoliciesApplied[tableID]; !found {
5961
r.PoliciesApplied[tableID] = PoliciesApplied{
6062
NoForceExempt: isTableOwnerAndNotForced,
63+
BypassRLS: bypassRLS,
6164
Filter: PolicyIDSet{},
6265
Check: PolicyIDSet{},
6366
}
@@ -87,6 +90,11 @@ type PoliciesApplied struct {
8790
// NoForceExempt is true if the policies were exempt because they were the
8891
// table owner and force RLS wasn't set.
8992
NoForceExempt bool
93+
// BypassRLS is true if the policies were bypassed because the user had the
94+
// BYPASSRLS option/privilege. There is an argument to combine the two bools
95+
// for exemption. However, the memo staleness checks have different handling
96+
// for when these exemptions change, so they need to be stored separately.
97+
BypassRLS bool
9098
// Filter is the set of policy IDs that were applied to filter out existing
9199
// rows. The USING expression in each policy is used to derive the filter.
92100
Filter PolicyIDSet
@@ -99,6 +107,7 @@ type PoliciesApplied struct {
99107
func (p *PoliciesApplied) Copy() PoliciesApplied {
100108
return PoliciesApplied{
101109
NoForceExempt: p.NoForceExempt,
110+
BypassRLS: p.BypassRLS,
102111
Filter: p.Filter.Copy(),
103112
Check: p.Check.Copy(),
104113
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,13 @@ func (tc *Catalog) HasRoleOption(ctx context.Context, roleOption roleoption.Opti
336336
return true, nil
337337
}
338338

339+
// UserHasGlobalPrivilegeOrRoleOption is part of the cat.Catalog interface.
340+
func (tc *Catalog) UserHasGlobalPrivilegeOrRoleOption(
341+
ctx context.Context, privilege privilege.Kind, user username.SQLUsername,
342+
) (bool, error) {
343+
return false, nil
344+
}
345+
339346
// FullyQualifiedName is part of the cat.Catalog interface.
340347
func (tc *Catalog) FullyQualifiedName(
341348
ctx context.Context, ds cat.DataSource,

pkg/sql/opt_catalog.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,13 @@ func (oc *optCatalog) HasRoleOption(
470470
return oc.planner.HasRoleOption(ctx, roleOption)
471471
}
472472

473+
// UserHasGlobalPrivilegeOrRoleOption is part of the cat.Catalog interface.
474+
func (oc *optCatalog) UserHasGlobalPrivilegeOrRoleOption(
475+
ctx context.Context, privilege privilege.Kind, user username.SQLUsername,
476+
) (bool, error) {
477+
return oc.planner.UserHasGlobalPrivilegeOrRoleOption(ctx, privilege, user)
478+
}
479+
473480
// FullyQualifiedName is part of the cat.Catalog interface.
474481
func (oc *optCatalog) FullyQualifiedName(
475482
ctx context.Context, ds cat.DataSource,

0 commit comments

Comments
 (0)