Skip to content

Commit ea781a0

Browse files
committed
sql: support BYPASSRLS role option for RLS policies
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
1 parent d074347 commit ea781a0

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
@@ -2919,4 +2919,175 @@ DROP ROLE test_role1;
29192919
statement ok
29202920
DROP ROLE test_role2;
29212921

2922+
# Ensure that if a role has the BYPASSRLS option or privilege set, that it's
2923+
# exempt from policies.
2924+
subtest bypassrls
2925+
2926+
statement ok
2927+
CREATE TABLE bypassrls (id INT PRIMARY KEY, val TEXT);
2928+
2929+
statement ok
2930+
CREATE USER bypassrls_user;
2931+
2932+
statement ok
2933+
GRANT ALL ON bypassrls TO bypassrls_user;
2934+
2935+
statement ok
2936+
ALTER TABLE bypassrls ENABLE ROW LEVEL SECURITY;
2937+
2938+
statement ok
2939+
CREATE POLICY pol1 ON bypassrls TO bypassrls_user USING (val like 'visible: %');
2940+
2941+
statement ok
2942+
CREATE FUNCTION insert_policy_violation_as_session_user(id INT) RETURNS TABLE(id INT, description TEXT)
2943+
LANGUAGE SQL AS
2944+
$$
2945+
INSERT INTO bypassrls VALUES (id, 'hidden: value') RETURNING id, val
2946+
$$;
2947+
2948+
# This function is like the above, except it will always be run as admin since
2949+
# it uses SECURITY DEFINER.
2950+
statement ok
2951+
CREATE FUNCTION insert_policy_violation_as_admin(id INT) RETURNS TABLE(id INT, description TEXT)
2952+
LANGUAGE SQL AS
2953+
$$
2954+
INSERT INTO bypassrls VALUES (id, 'hidden: value') RETURNING id, val
2955+
$$ SECURITY DEFINER;
2956+
2957+
statement ok
2958+
INSERT INTO bypassrls VALUES (0, 'visible: 0'), (1, 'hidden: 1'), (2, 'visible: 2');
2959+
2960+
query IT
2961+
SELECT * FROM insert_policy_violation_as_admin(10);
2962+
----
2963+
10 hidden: value
2964+
2965+
query IT
2966+
SELECT * FROM insert_policy_violation_as_session_user(11);
2967+
----
2968+
11 hidden: value
2969+
2970+
query IT
2971+
SELECT * FROM bypassrls ORDER BY id;
2972+
----
2973+
0 visible: 0
2974+
1 hidden: 1
2975+
2 visible: 2
2976+
10 hidden: value
2977+
11 hidden: value
2978+
2979+
statement ok
2980+
SET ROLE bypassrls_user;
2981+
2982+
query IT
2983+
SELECT * FROM bypassrls ORDER BY id;
2984+
----
2985+
0 visible: 0
2986+
2 visible: 2
2987+
2988+
query IT
2989+
SELECT * FROM insert_policy_violation_as_admin(20);
2990+
----
2991+
20 hidden: value
2992+
2993+
statement error pq: new row violates row-level security policy for table "bypassrls"
2994+
SELECT * FROM insert_policy_violation_as_session_user(21);
2995+
2996+
statement ok
2997+
SET ROLE root;
2998+
2999+
statement error pq: conflicting role options
3000+
ALTER ROLE bypassrls_user BYPASSRLS NOBYPASSRLS;
3001+
3002+
statement ok
3003+
ALTER ROLE bypassrls_user BYPASSRLS;
3004+
3005+
statement ok
3006+
SET ROLE bypassrls_user;
3007+
3008+
query IT
3009+
SELECT * FROM bypassrls ORDER BY id;
3010+
----
3011+
0 visible: 0
3012+
1 hidden: 1
3013+
2 visible: 2
3014+
10 hidden: value
3015+
11 hidden: value
3016+
20 hidden: value
3017+
3018+
query IT
3019+
SELECT * FROM insert_policy_violation_as_admin(30);
3020+
----
3021+
30 hidden: value
3022+
3023+
query IT
3024+
SELECT * FROM insert_policy_violation_as_session_user(31);
3025+
----
3026+
31 hidden: value
3027+
3028+
statement ok
3029+
SET ROLE root
3030+
3031+
statement ok
3032+
ALTER ROLE bypassrls_user NOBYPASSRLS;
3033+
3034+
statement ok
3035+
GRANT SYSTEM BYPASSRLS TO bypassrls_user;
3036+
3037+
query TTB colnames
3038+
SELECT * FROM [SHOW SYSTEM GRANTS] WHERE privilege_type = 'BYPASSRLS';
3039+
----
3040+
grantee privilege_type is_grantable
3041+
bypassrls_user BYPASSRLS false
3042+
3043+
statement ok
3044+
SET ROLE bypassrls_user;
3045+
3046+
query IT
3047+
SELECT * FROM bypassrls ORDER BY id;
3048+
----
3049+
0 visible: 0
3050+
1 hidden: 1
3051+
2 visible: 2
3052+
10 hidden: value
3053+
11 hidden: value
3054+
20 hidden: value
3055+
30 hidden: value
3056+
31 hidden: value
3057+
3058+
query IT
3059+
SELECT * FROM insert_policy_violation_as_session_user(40);
3060+
----
3061+
40 hidden: value
3062+
3063+
statement ok
3064+
SET ROLE root;
3065+
3066+
statement ok
3067+
REVOKE SYSTEM BYPASSRLS FROM bypassrls_user;
3068+
3069+
statement ok
3070+
SET ROLE bypassrls_user;
3071+
3072+
query IT
3073+
SELECT * FROM bypassrls ORDER BY id;
3074+
----
3075+
0 visible: 0
3076+
2 visible: 2
3077+
3078+
statement error pq: new row violates row-level security policy for table "bypassrls"
3079+
SELECT * FROM insert_policy_violation_as_session_user(50);
3080+
3081+
statement ok
3082+
SET ROLE root;
3083+
3084+
statement ok
3085+
DROP FUNCTION insert_policy_violation_as_admin, insert_policy_violation_as_session_user
3086+
3087+
statement ok
3088+
DROP TABLE bypassrls;
3089+
3090+
statement ok
3091+
DROP USER bypassrls_user;
3092+
29223093
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
@@ -634,7 +634,8 @@ func TestMemoIsStale(t *testing.T) {
634634

635635
// User changes (with RLS)
636636
o.Memo().Metadata().SetRLSEnabled(evalCtx.SessionData().User(), true, /* admin */
637-
1 /* tableID */, false /* isTableOwnerAndNotForced */)
637+
1 /* tableID */, false, /* isTableOwnerAndNotForced */
638+
false /* bypassRLS */)
638639
notStale()
639640
evalCtx.SessionData().UserProto = newUser
640641
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)