Skip to content

Commit 37c1208

Browse files
craig[bot]spilchen
andcommitted
Merge #142479
142479: sql: support multiple RLS policies on a table r=spilchen a=spilchen Previously, row-level security (RLS) tables only ever enforced one policy. This change allows multiple policies to be applied. When multiple policies exist, their type determines how they are combined: - Permissive policies are combined using OR - Restrictive policies are combined using AND - At least one permissive policy is required; otherwise, the table is treated as deny-all (all rows are filtered out, or all new rows are rejected). The change affects both the filtering of existing rows (USING expression) and constraints on new rows (WITH CHECK expression). Epic: CRDB-45203 Release note: none Informs #136742 Co-authored-by: Matt Spilchen <[email protected]>
2 parents a7ecf5a + 5eb24f2 commit 37c1208

File tree

5 files changed

+330
-39
lines changed

5 files changed

+330
-39
lines changed

pkg/sql/logictest/testdata/logic_test/row_level_security

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1677,4 +1677,162 @@ DROP FUNCTION al_only;
16771677
statement ok
16781678
DROP SEQUENCE seq1;
16791679

1680+
subtest multiple_policies
1681+
1682+
statement ok
1683+
CREATE TABLE multip (key INT NOT NULL, value TEXT, FAMILY (key,value));
1684+
1685+
statement ok
1686+
ALTER TABLE multip ENABLE ROW LEVEL SECURITY;
1687+
1688+
# The policies will be combined as: (or1 || or2 || or3) && and1 && and2
1689+
statement ok
1690+
CREATE POLICY or1 ON multip AS PERMISSIVE USING (key = 1);
1691+
1692+
statement ok
1693+
CREATE POLICY or2 ON multip AS PERMISSIVE USING (key = 2);
1694+
1695+
statement ok
1696+
CREATE POLICY or3 ON multip AS PERMISSIVE USING (key = 3);
1697+
1698+
statement ok
1699+
CREATE POLICY and1 ON multip AS RESTRICTIVE USING (value not like '%sensitive%')
1700+
1701+
statement ok
1702+
CREATE POLICY and2 ON multip AS RESTRICTIVE USING (value not like '%confidential%')
1703+
1704+
statement ok
1705+
INSERT INTO multip VALUES
1706+
(0, 'key out of bounds'),
1707+
(1, 'okay'),
1708+
(1, 'sensitive - filtered out'),
1709+
(2, 'okay'),
1710+
(2, 'confidential - filtered out'),
1711+
(3, 'okay'),
1712+
(2, 'sensitive - filtered out'),
1713+
(4, 'key out of bounds'),
1714+
(4, 'confidential');
1715+
1716+
statement ok
1717+
CREATE USER multi_user;
1718+
1719+
statement ok
1720+
GRANT ALL ON multip TO multi_user;
1721+
1722+
statement ok
1723+
SET ROLE multi_user;
1724+
1725+
query IT
1726+
select * from multip ORDER BY key, value;
1727+
----
1728+
1 okay
1729+
2 okay
1730+
3 okay
1731+
1732+
query IT
1733+
select * from multip where key >= 0 ORDER BY key, value;
1734+
----
1735+
1 okay
1736+
2 okay
1737+
3 okay
1738+
1739+
statement ok
1740+
SET ROLE root
1741+
1742+
# Ensure that if only a restrictive policy exists that all rows will be rejected
1743+
# regardless of the policy expression.
1744+
statement ok
1745+
DROP POLICY or1 ON multip;
1746+
1747+
statement ok
1748+
DROP POLICY or2 ON multip;
1749+
1750+
statement ok
1751+
DROP POLICY or3 ON multip;
1752+
1753+
statement ok
1754+
SET ROLE multi_user;
1755+
1756+
query TT
1757+
SHOW CREATE multip;
1758+
----
1759+
multip CREATE TABLE public.multip (
1760+
key INT8 NOT NULL,
1761+
value STRING NULL,
1762+
rowid INT8 NOT VISIBLE NOT NULL DEFAULT unique_rowid(),
1763+
CONSTRAINT multip_pkey PRIMARY KEY (rowid ASC),
1764+
FAMILY fam_0_key_value_rowid (key, value, rowid)
1765+
);
1766+
ALTER TABLE public.multip ENABLE ROW LEVEL SECURITY;
1767+
CREATE POLICY and1 ON public.multip AS RESTRICTIVE FOR ALL TO public USING (value NOT LIKE '%sensitive%':::STRING);
1768+
CREATE POLICY and2 ON public.multip AS RESTRICTIVE FOR ALL TO public USING (value NOT LIKE '%confidential%':::STRING)
1769+
1770+
query IT
1771+
select * from multip ORDER BY key, value;
1772+
----
1773+
1774+
statement ok
1775+
SET ROLE root;
1776+
1777+
# Setup to verify the write policies.
1778+
statement ok
1779+
TRUNCATE TABLE multip;
1780+
1781+
statement ok
1782+
CREATE POLICY or1 ON multip AS PERMISSIVE USING (key = 1);
1783+
1784+
statement ok
1785+
CREATE POLICY or2 ON multip AS PERMISSIVE USING (key = 2);
1786+
1787+
statement ok
1788+
CREATE POLICY or3 ON multip AS PERMISSIVE USING (key = 3);
1789+
1790+
statement ok
1791+
SET ROLE multi_user;
1792+
1793+
statement error pq: new row violates row-level security policy for table "multip"
1794+
INSERT INTO multip VALUES (0, 'key out of bounds');
1795+
1796+
statement ok
1797+
INSERT INTO multip VALUES (1, 'okay');
1798+
1799+
statement error pq: new row violates row-level security policy for table "multip"
1800+
INSERT INTO multip VALUES (1, 'sensitive - filtered out');
1801+
1802+
statement ok
1803+
INSERT INTO multip VALUES (2, 'okay')
1804+
1805+
statement error pq: new row violates row-level security policy for table "multip"
1806+
INSERT INTO multip VALUES (2, 'confidential - filtered out')
1807+
1808+
statement ok
1809+
INSERT INTO multip VALUES (3, 'okay')
1810+
1811+
statement error pq: new row violates row-level security policy for table "multip"
1812+
INSERT INTO multip VALUES (2, 'sensitive - filtered out')
1813+
1814+
statement error pq: new row violates row-level security policy for table "multip"
1815+
INSERT INTO multip VALUES (4, 'key out of bounds')
1816+
1817+
statement error pq: new row violates row-level security policy for table "multip"
1818+
INSERT INTO multip VALUES (4, 'confidential');
1819+
1820+
statement ok
1821+
SET ROLE root;
1822+
1823+
query IT
1824+
select * FROM multip ORDER BY key, value;
1825+
----
1826+
1 okay
1827+
2 okay
1828+
3 okay
1829+
1830+
statement ok
1831+
DROP TABLE multip;
1832+
1833+
statement ok
1834+
DROP USER multi_user;
1835+
1836+
subtest multiple_insert_policies
1837+
16801838
subtest end

pkg/sql/opt/exec/explain/emit.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,14 +1404,14 @@ func (e *emitter) emitPolicies(ob *OutputBuilder, table cat.Table, n *Node) {
14041404
} else {
14051405
var sb strings.Builder
14061406
policies := table.Policies()
1407-
// TODO(136742): Add support for restrictive policies.
1408-
for i := range policies.Permissive {
1409-
policy := policies.Permissive[i]
1410-
if applied.Policies.Contains(policy.ID) {
1411-
if sb.Len() > 0 {
1412-
sb.WriteString(", ")
1407+
for _, grp := range [][]cat.Policy{policies.Permissive, policies.Restrictive} {
1408+
for _, policy := range grp {
1409+
if applied.Policies.Contains(policy.ID) {
1410+
if sb.Len() > 0 {
1411+
sb.WriteString(", ")
1412+
}
1413+
sb.WriteString(policy.Name.Normalize())
14131414
}
1414-
sb.WriteString(policy.Name.Normalize())
14151415
}
14161416
}
14171417
ob.AddField("policies", sb.String())

pkg/sql/opt/exec/explain/testdata/row_level_security

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,13 @@ exec-ddl
137137
CREATE POLICY r2 on t1 AS RESTRICTIVE USING (true);
138138
----
139139

140-
# TODO(136742): We currently only support at most one permissive policy. Update
141-
# this when we support multiple and restrictive policies.
142140
plan
143141
select * from t1
144142
----
145143
• scan
146144
table: t1@t1_pkey
147145
spans: FULL SCAN
148-
policies: policy 1
146+
policies: policy 1, p2, p3, r1, r2
149147

150148
# Show policy information where an index scan is used for one table
151149
# ----------------------------------------------------------------------
@@ -162,7 +160,7 @@ select t1.c1 from t1, t2 where t1.c1 = t2.c1 and t1.c1 = 1;
162160
├── • scan
163161
│ table: t1@t1_idx
164162
│ spans: 1+ spans
165-
│ policies: policy 1
163+
│ policies: policy 1, p2, p3, r1, r2
166164
167165
└── • filter
168166
│ filter: c1 = _
@@ -268,7 +266,7 @@ UPDATE writer SET key = key * 11;
268266
│ table: writer
269267
│ set: key
270268
│ auto commit
271-
│ policies: p_update1
269+
│ policies: p_update1, p_update2
272270
273271
└── • render
274272
@@ -285,7 +283,7 @@ UPDATE writer SET key = key * 15 WHERE value = 'some-val' or value = 'other-val'
285283
│ table: writer
286284
│ set: key
287285
│ auto commit
288-
│ policies: p_update1
286+
│ policies: p_update1, p_update2
289287
290288
└── • render
291289
@@ -304,7 +302,7 @@ UPDATE writer SET value = 'updated' WHERE key between 1 and 100;
304302
│ table: writer
305303
│ set: value
306304
│ auto commit
307-
│ policies: p_update1
305+
│ policies: p_update1, p_update2
308306
309307
└── • render
310308
@@ -321,7 +319,7 @@ UPDATE writer SET value = value WHERE key = 5;
321319
│ table: writer
322320
│ set: value
323321
│ auto commit
324-
│ policies: p_update1
322+
│ policies: p_update1, p_update2
325323
326324
└── • render
327325

pkg/sql/opt/optbuilder/row_level_security.go

Lines changed: 62 additions & 24 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/sem/tree"
2021
"github.com/cockroachdb/cockroach/pkg/sql/types"
2122
"github.com/cockroachdb/cockroach/pkg/util/intsets"
2223
"github.com/cockroachdb/errors"
@@ -54,32 +55,55 @@ func (b *Builder) buildRowLevelSecurityUsingExpression(
5455
tabMeta *opt.TableMeta, tableScope *scope, cmdScope cat.PolicyCommandScope,
5556
) opt.ScalarExpr {
5657
var policiesUsed opt.PolicyIDSet
58+
var combinedExpr tree.TypedExpr
5759
policies := tabMeta.Table.Policies()
58-
for _, policy := range policies.Permissive {
60+
61+
// Create a closure to handle building the expression for one policy.
62+
buildForPolicy := func(policy cat.Policy, restrictive bool) {
5963
if !policy.AppliesToRole(b.checkPrivilegeUser) || !b.policyAppliesToCommandScope(policy, cmdScope) {
60-
continue
64+
return
6165
}
6266
strExpr := policy.UsingExpr
6367
if strExpr == "" {
64-
continue
68+
return
6569
}
6670
policiesUsed.Add(policy.ID)
6771
parsedExpr, err := parser.ParseExpr(strExpr)
6872
if err != nil {
6973
panic(err)
7074
}
7175
typedExpr := tableScope.resolveType(parsedExpr, types.AnyElement)
72-
scalar := b.buildScalar(typedExpr, tableScope, nil, nil, nil)
73-
// TODO(136742): Apply multiple RLS policies.
74-
b.factory.Metadata().GetRLSMeta().AddPoliciesUsed(tabMeta.MetaID, policiesUsed, true /* applyFilterExpr */)
75-
return scalar
76+
if combinedExpr != nil {
77+
// Restrictive policies are combined using AND, while permissive
78+
// policies are combined using OR.
79+
if restrictive {
80+
combinedExpr = tree.NewTypedAndExpr(combinedExpr, typedExpr)
81+
} else {
82+
combinedExpr = tree.NewTypedOrExpr(combinedExpr, typedExpr)
83+
}
84+
} else {
85+
combinedExpr = typedExpr
86+
}
7687
}
7788

78-
// TODO(136742): Add support for restrictive policies.
89+
for _, policy := range policies.Permissive {
90+
buildForPolicy(policy, false /* restrictive */)
91+
}
92+
if combinedExpr == nil {
93+
// If no permissive policies apply, filter out all rows by adding a "false" expression.
94+
b.factory.Metadata().GetRLSMeta().NoPoliciesApplied = true
95+
return memo.FalseSingleton
96+
}
97+
for _, policy := range policies.Restrictive {
98+
buildForPolicy(policy, true /* restrictive */)
99+
}
79100

80-
// If no permissive policies apply, filter out all rows by adding a "false" expression.
81-
b.factory.Metadata().GetRLSMeta().NoPoliciesApplied = true
82-
return memo.FalseSingleton
101+
// We should have already exited early if there were no permissive policies.
102+
if combinedExpr == nil {
103+
panic(errors.AssertionFailedf("at least one applicable policy should have been found"))
104+
}
105+
b.factory.Metadata().GetRLSMeta().AddPoliciesUsed(tabMeta.MetaID, policiesUsed, true /* applyFilterExpr */)
106+
return b.buildScalar(combinedExpr, tableScope, nil, nil, nil)
83107
}
84108

85109
// policyAppliesToCommandScope checks whether a given PolicyCommandScope applies
@@ -155,20 +179,22 @@ func (r *optRLSConstraintBuilder) genExpression(ctx context.Context) (string, []
155179
}
156180

157181
var policiesUsed opt.PolicyIDSet
158-
for i := range r.tab.Policies().Permissive {
159-
p := &r.tab.Policies().Permissive[i]
182+
policies := r.tabMeta.Table.Policies()
160183

161-
if !p.AppliesToRole(r.user) || !r.policyAppliesToCommand(p, r.isUpdate) {
162-
continue
184+
// Create a closure to handle building the expression for one policy.
185+
buildForPolicy := func(p cat.Policy, restrictive bool) {
186+
if !p.AppliesToRole(r.user) || !r.policyAppliesToCommand(&p, r.isUpdate) {
187+
return
163188
}
164189
policiesUsed.Add(p.ID)
190+
165191
var expr string
166192
// If the WITH CHECK expression is missing, we default to the USING
167193
// expression. If both are missing, then this policy doesn't apply and can
168194
// be skipped.
169195
if p.WithCheckExpr == "" {
170196
if p.UsingExpr == "" {
171-
continue
197+
return
172198
}
173199
expr = p.UsingExpr
174200
for _, id := range p.UsingColumnIDs {
@@ -181,25 +207,37 @@ func (r *optRLSConstraintBuilder) genExpression(ctx context.Context) (string, []
181207
}
182208
}
183209
if sb.Len() != 0 {
184-
sb.WriteString(" OR ")
210+
if restrictive {
211+
sb.WriteString(" AND ")
212+
} else {
213+
sb.WriteString(" OR ")
214+
}
215+
} else {
216+
sb.WriteString("(") // Add the outer parenthesis that surrounds all permissive policies
185217
}
186218
sb.WriteString("(")
187219
sb.WriteString(expr)
188220
sb.WriteString(")")
189-
// TODO(136742): Add support for multiple policies.
190-
r.md.GetRLSMeta().AddPoliciesUsed(r.tabMeta.MetaID, policiesUsed, false /* applyFilterExpr */)
191-
break
192221
}
193222

194-
// TODO(136742): Add support for restrictive policies.
195-
196-
// If no policies apply, then we will add a false check as nothing is allowed
197-
// to be written.
223+
for _, policy := range policies.Permissive {
224+
buildForPolicy(policy, false /* restrictive */)
225+
}
226+
// If no permissive policies apply, then we will add a false check as
227+
// nothing is allowed to be written.
198228
if sb.Len() == 0 {
199229
r.md.GetRLSMeta().NoPoliciesApplied = true
200230
return "false", nil
201231
}
232+
sb.WriteString(")") // Close the outer parenthesis that surrounds all permissive policies
233+
for _, policy := range policies.Restrictive {
234+
buildForPolicy(policy, true /* restrictive */)
235+
}
202236

237+
if sb.Len() == 0 {
238+
panic(errors.AssertionFailedf("at least one applicable policy should have been included"))
239+
}
240+
r.md.GetRLSMeta().AddPoliciesUsed(r.tabMeta.MetaID, policiesUsed, false /* applyFilterExpr */)
203241
return sb.String(), colIDs.Ordered()
204242
}
205243

0 commit comments

Comments
 (0)