forked from cockroachdb/cockroach
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
139570: sql: Apply USING expression to up to one RLS policy r=spilchen a=spilchen _Stacked PR_ ## sql: Add RLS policy information to the optimizer catalog Row-level security (RLS) policies include expressions that govern table access during query execution. While policy information was previously added to the table descriptor, this commit integrates it into the optimizer catalog. The cat.Table interface has been updated to expose policy details, and a new cat.Policy interface has been introduced to facilitate interaction with individual policies. Epic: CRDB-11724 Informs cockroachdb#136717 Release note: None ## sql: Add RLS statements to the opt test Row-level security (RLS) introduced new SQL grammar to manage policies and enable RLS. This commit implements those statements in the optimizer test catalog, allowing for optbuilder tests that utilize RLS features. Newly supported statements: - **CREATE POLICY:** Creates a new policy for a table. - **DROP POLICY:** Removes an existing policy from a table. - **ALTER TABLE .. {ENABLE|DISABLE} ROW LEVEL SECURITY:** Toggles RLS enforcement for a table. - **CREATE USER:** Adds support for creating users in the test catalog. RLS policies are bypassed for admin members. - **SET ROLE:** Enables changing the current user in optbuilder tests. The test catalog now tracks the current user and the catalog's users. Epic: CRDB-11724 Informs cockroachdb#136717 Release note: None ## sql: Apply RLS USING expressions during reads Row-level security (RLS) allows you to define a USING expression in a policy, which is applied to all read operations on tables with RLS enabled. A policy specifies conditions, such as roles and SQL commands, that determine when it applies. This commit adds support for incorporating a USING expression from at most one permissive policy into a query. Currently, the table attribute to enable RLS in the table descriptor is not yet implemented. As a result, RLS effects are only observable in queries executed via the optimizer test catalog. All code paths that drive scans (e.g., SELECT, UPDATE, DELETE) now use cat.PolicyCommandScope to determine which SQL operations RLS policies apply to. Certain operations, such as foreign key checks and cascades, are explicitly exempt from RLS enforcement. Epic: CRDB-11724 Informs cockroachdb#136717 Release note: None Co-authored-by: Matt Spilchen <[email protected]>
- Loading branch information
Showing
21 changed files
with
805 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
// Copyright 2025 The Cockroach Authors. | ||
// | ||
// Use of this software is governed by the CockroachDB Software License | ||
// included in the /LICENSE file. | ||
|
||
package cat | ||
|
||
import ( | ||
"github.com/cockroachdb/cockroach/pkg/security/username" | ||
"github.com/cockroachdb/cockroach/pkg/sql/catalog/catpb" | ||
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree" | ||
) | ||
|
||
// PolicyCommandScope specifies the scope of SQL commands to which a policy applies. | ||
// It determines whether a policy is enforced for specific operations or if an operation | ||
// is exempt from row-level security. The operations checked must align with the policy | ||
// commands defined in the CREATE POLICY SQL syntax. | ||
type PolicyCommandScope uint8 | ||
|
||
const ( | ||
// PolicyScopeSelect indicates that the policy applies to SELECT operations. | ||
PolicyScopeSelect PolicyCommandScope = iota | ||
// PolicyScopeInsert indicates that the policy applies to INSERT operations. | ||
PolicyScopeInsert | ||
// PolicyScopeUpdate indicates that the policy applies to UPDATE operations. | ||
PolicyScopeUpdate | ||
// PolicyScopeDelete indicates that the policy applies to DELETE operations. | ||
PolicyScopeDelete | ||
// PolicyScopeExempt indicates that the operation is exempt from row-level security policies. | ||
PolicyScopeExempt | ||
) | ||
|
||
// Policy defines an interface for a row-level security (RLS) policy on a table. | ||
// Policies use expressions to filter rows during read operations and/or restrict | ||
// new rows during write operations. | ||
type Policy struct { | ||
// Name is the name of the policy. The name is unique within a table | ||
// and cannot be qualified. | ||
Name tree.Name | ||
// UsingExpr is the optional filter expression evaluated on rows during | ||
// read operations. If the policy does not define a USING expression, this is | ||
// an empty string. | ||
UsingExpr string | ||
// WithCheckExpr is the optional validation expression applied to new rows | ||
// during write operations. If the policy does not define a WITH CHECK expression, | ||
// this is an empty string. | ||
WithCheckExpr string | ||
// Command is the command that the policy was defined for. | ||
Command catpb.PolicyCommand | ||
// roles are the roles the applies to. If the policy applies to all roles (aka | ||
// public), this will be nil. | ||
roles map[string]struct{} | ||
} | ||
|
||
// Policies contains the policies for a single table. | ||
type Policies struct { | ||
Permissive []Policy | ||
Restrictive []Policy | ||
} | ||
|
||
// InitRoles builds up the list of roles in the policy. | ||
func (p *Policy) InitRoles(roleNames []string) { | ||
if len(roleNames) == 0 { | ||
p.roles = nil | ||
return | ||
} | ||
roles := make(map[string]struct{}) | ||
for _, r := range roleNames { | ||
if r == username.PublicRole { | ||
// If the public role is defined, there is no need to check the | ||
// remaining roles since the policy applies to everyone. We will clear | ||
// out the roles map to signal that all roles apply. | ||
roles = nil | ||
break | ||
} | ||
roles[r] = struct{}{} | ||
} | ||
p.roles = roles | ||
} | ||
|
||
// AppliesToRole checks whether the policy applies to the give role. | ||
func (p *Policy) AppliesToRole(user username.SQLUsername) bool { | ||
// If no roles are specified, assume the policy applies to all users (public role). | ||
if p.roles == nil { | ||
return true | ||
} | ||
_, found := p.roles[user.Normalized()] | ||
return found | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
// Copyright 2025 The Cockroach Authors. | ||
// | ||
// Use of this software is governed by the CockroachDB Software License | ||
// included in the /LICENSE file. | ||
|
||
package optbuilder | ||
|
||
import ( | ||
"github.com/cockroachdb/cockroach/pkg/sql/catalog/catpb" | ||
"github.com/cockroachdb/cockroach/pkg/sql/opt" | ||
"github.com/cockroachdb/cockroach/pkg/sql/opt/cat" | ||
"github.com/cockroachdb/cockroach/pkg/sql/opt/memo" | ||
"github.com/cockroachdb/cockroach/pkg/sql/parser" | ||
"github.com/cockroachdb/cockroach/pkg/sql/sem/tree" | ||
"github.com/cockroachdb/cockroach/pkg/sql/types" | ||
"github.com/cockroachdb/errors" | ||
) | ||
|
||
// addRowLevelSecurityFilter adds a filter based on the expressions of | ||
// applicable RLS policies. If RLS is enabled but no policies are applicable, | ||
// all rows will be filtered out. | ||
func (b *Builder) addRowLevelSecurityFilter( | ||
tabMeta *opt.TableMeta, tableScope *scope, cmdScope cat.PolicyCommandScope, | ||
) { | ||
if !tabMeta.Table.IsRowLevelSecurityEnabled() || cmdScope == cat.PolicyScopeExempt { | ||
return | ||
} | ||
|
||
// Admin users are exempt from any RLS filtering. | ||
isAdmin, err := b.catalog.HasAdminRole(b.ctx) | ||
if err != nil { | ||
panic(err) | ||
} | ||
if isAdmin { | ||
return | ||
} | ||
|
||
scalar := b.buildRowLevelSecurityUsingExpression(tabMeta, tableScope, cmdScope) | ||
tableScope.expr = b.factory.ConstructSelect(tableScope.expr, | ||
memo.FiltersExpr{b.factory.ConstructFiltersItem(scalar)}) | ||
} | ||
|
||
// buildRowLevelSecurityUsingExpression generates a scalar expression for read | ||
// operations by combining all applicable RLS policies. An expression is always | ||
// returned; if no policies apply, a 'false' expression is returned. | ||
func (b *Builder) buildRowLevelSecurityUsingExpression( | ||
tabMeta *opt.TableMeta, tableScope *scope, cmdScope cat.PolicyCommandScope, | ||
) opt.ScalarExpr { | ||
for i := 0; i < tabMeta.Table.PolicyCount(tree.PolicyTypePermissive); i++ { | ||
policy := tabMeta.Table.Policy(tree.PolicyTypePermissive, i) | ||
|
||
if !policy.AppliesToRole(b.checkPrivilegeUser) || !b.policyAppliesToCommandScope(policy, cmdScope) { | ||
continue | ||
} | ||
strExpr := policy.UsingExpr | ||
if strExpr == "" { | ||
continue | ||
} | ||
parsedExpr, err := parser.ParseExpr(strExpr) | ||
if err != nil { | ||
panic(err) | ||
} | ||
typedExpr := tableScope.resolveType(parsedExpr, types.Any) | ||
scalar := b.buildScalar(typedExpr, tableScope, nil, nil, nil) | ||
// TODO(136742): Apply multiple RLS policies. | ||
return scalar | ||
} | ||
|
||
// TODO(136742): Add support for restrictive policies. | ||
|
||
// If no permissive policies apply, filter out all rows by adding a "false" expression. | ||
return memo.FalseSingleton | ||
} | ||
|
||
// policyAppliesToCommandScope checks whether a given PolicyCommandScope applies | ||
// to the specified policy. It returns true if the policy is applicable and | ||
// false otherwise. | ||
func (b *Builder) policyAppliesToCommandScope( | ||
policy cat.Policy, cmdScope cat.PolicyCommandScope, | ||
) bool { | ||
if cmdScope == cat.PolicyScopeExempt { | ||
return true | ||
} | ||
cmd := policy.Command | ||
switch cmd { | ||
case catpb.PolicyCommand_ALL: | ||
return true | ||
case catpb.PolicyCommand_SELECT: | ||
return cmdScope == cat.PolicyScopeSelect | ||
case catpb.PolicyCommand_INSERT: | ||
return cmdScope == cat.PolicyScopeInsert | ||
case catpb.PolicyCommand_UPDATE: | ||
return cmdScope == cat.PolicyScopeUpdate | ||
case catpb.PolicyCommand_DELETE: | ||
return cmdScope == cat.PolicyScopeDelete | ||
default: | ||
panic(errors.AssertionFailedf("unknown policy command %v", cmd)) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.